mirror of
https://github.com/cline/cline.git
synced 2026-09-06 12:28:08 +08:00
Compare commits
257 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c66407e967 | |||
| 19a80f9623 | |||
| faa5e4dd61 | |||
| 3a44bcf784 | |||
| ccba2ed8f8 | |||
| 0fc2f474a4 | |||
| 0ce66efc19 | |||
| e231cfd3e5 | |||
| 10f8637fb8 | |||
| bf985064b9 | |||
| 922183793f | |||
| 2c02547088 | |||
| df65f5f5dd | |||
| 542ce36a80 | |||
| 6baac79138 | |||
| 6a2172e156 | |||
| 3ca529dc62 | |||
| 7728c0304c | |||
| f837ecefeb | |||
| 2d5dcf6a14 | |||
| fcf6734949 | |||
| d44fd44a81 | |||
| d103f8ee62 | |||
| 3d34de4908 | |||
| dcd275480f | |||
| aea06ce659 | |||
| 65f582bb0d | |||
| f8796ce69a | |||
| 4d29e1f4bd | |||
| 499c7dfb9f | |||
| 57b36e16b8 | |||
| c891d24ef6 | |||
| 3f5b6f6e33 | |||
| dda8a6c829 | |||
| 0959438db8 | |||
| a838cf9a61 | |||
| 4e1dab8d4b | |||
| 93b6c937a5 | |||
| f555b83c7c | |||
| 21d4ec13e1 | |||
| 38e51bcf73 | |||
| f40ff25d1e | |||
| 29c01eb7d3 | |||
| 682fce458e | |||
| 34e87af5f1 | |||
| eaa76512fc | |||
| df7b458229 | |||
| 560cefecca | |||
| 12dbf1556b | |||
| b4149b5a18 | |||
| cee959eeec | |||
| 46121094d7 | |||
| 6fe1015310 | |||
| bb902f9b6b | |||
| dfeb57d3af | |||
| 9232d752b2 | |||
| d1a097cbd4 | |||
| a02eb40c31 | |||
| c87fb06f33 | |||
| eedae086a3 | |||
| c1d589f6be | |||
| d7e9ead730 | |||
| 9df45fb216 | |||
| d6184e9dac | |||
| d01b99463e | |||
| 3525592c61 | |||
| 1913b69ca5 | |||
| aa714f28e1 | |||
| eef04542d2 | |||
| 602d25075b | |||
| ea2f4080f8 | |||
| 10cde3671e | |||
| 43e939666d | |||
| ff0eb8fa77 | |||
| db4f6a72c3 | |||
| 39980e9be7 | |||
| 9b5cefd69d | |||
| 30a4a0e1f1 | |||
| e5d616b0bf | |||
| 5d8172a25f | |||
| 916660a390 | |||
| 4506eb39a6 | |||
| 202e3a6d05 | |||
| a229b8ce9d | |||
| d58c947c41 | |||
| 9449f5dcd1 | |||
| 7bc02926b4 | |||
| 5e65ea04c9 | |||
| e14fd78767 | |||
| 8c5f8b253b | |||
| db463dc1ef | |||
| a13aba4bd4 | |||
| 568932492b | |||
| 35a64a3666 | |||
| 3d19123e0b | |||
| 4f6605ccdb | |||
| 408c0887ac | |||
| b4e67afcba | |||
| 1c9da770a8 | |||
| 5f57c1f2f3 | |||
| f14542d61f | |||
| 131b245023 | |||
| c2809491b4 | |||
| 87670a37b8 | |||
| 0f1240063c | |||
| 45b13dd775 | |||
| a2749782ff | |||
| 1127e2a33c | |||
| 25ea46aa8d | |||
| b0446f7bac | |||
| f8e4bfdbeb | |||
| 9b30efefbe | |||
| 82f1f79b3e | |||
| 00506c0fcf | |||
| 45e1f58bbc | |||
| cd3f3e2150 | |||
| ab8376fee4 | |||
| ff7e5c8866 | |||
| 26be9469c4 | |||
| a925e672ed | |||
| 2491b469e7 | |||
| 3fd1f16acb | |||
| 0a14f0bad5 | |||
| d72127550d | |||
| 97fa8cffb9 | |||
| d6f51b4525 | |||
| c846bc4455 | |||
| efaa0d057e | |||
| ceeb33fdc5 | |||
| f938c40539 | |||
| 65ac7ec3f7 | |||
| f80552f92d | |||
| ac4ca51ced | |||
| 04f8c294c2 | |||
| a6e109718e | |||
| f9025426b8 | |||
| 97abdc500b | |||
| 3d64ab298d | |||
| ccdbe3a4a9 | |||
| 000b4af458 | |||
| a01471f469 | |||
| d7788c2fd6 | |||
| d6be431cb8 | |||
| 4c844cbafa | |||
| 9289d73a81 | |||
| 19e5edbc25 | |||
| a20a2444b3 | |||
| cb02277b45 | |||
| 220603e530 | |||
| e40e6ddbcd | |||
| ec3bd56903 | |||
| c126804bb0 | |||
| ee50e9710a | |||
| 95acb1e95c | |||
| e587115d62 | |||
| 9c840fc89c | |||
| b7519a3669 | |||
| 691cf91a4d | |||
| 24d3bfbccb | |||
| 8927879d93 | |||
| 5f57415785 | |||
| c3f7fa3386 | |||
| e95cb30ebd | |||
| 15b269c8f2 | |||
| 31443f2a86 | |||
| 2d05dfd986 | |||
| 7722234761 | |||
| 09c4202cb2 | |||
| 31eb212bee | |||
| cc166f4144 | |||
| 5dea79c65f | |||
| 217162ccba | |||
| a98269f6d5 | |||
| aab26e9a38 | |||
| 796eac558c | |||
| 3873908efb | |||
| a6e812824c | |||
| a4d1ae1af4 | |||
| fa2797051d | |||
| 10315663b2 | |||
| 338116fbd4 | |||
| 89603d3e92 | |||
| a3dc4471e3 | |||
| 510bb72431 | |||
| 141a302521 | |||
| faae5f22a3 | |||
| 0cbd6b321f | |||
| c134291ba0 | |||
| 9b66eac9fe | |||
| c756470c51 | |||
| b059b3ac1e | |||
| 7ae437fb94 | |||
| d67c7e36da | |||
| 584af64334 | |||
| e3b4b2c816 | |||
| 267b60c09e | |||
| ee06c0811c | |||
| b8024497f2 | |||
| e3cfb405fb | |||
| 8e12bdb00d | |||
| 7d67000d1f | |||
| 283d7d6928 | |||
| 83a28001be | |||
| 66d5a320b5 | |||
| 95e1555361 | |||
| a99648884a | |||
| 0434b5c772 | |||
| e534c3dbc7 | |||
| add2f85053 | |||
| 69ff71b051 | |||
| f096d73813 | |||
| 96540ddf1e | |||
| 7da74bd9d8 | |||
| 2a894d8cf1 | |||
| 8793c2c6c5 | |||
| 9e76cfd44a | |||
| 9c8254cf92 | |||
| 57ae7c05d6 | |||
| e8a2e88fa0 | |||
| 7b6a3d25e5 | |||
| 82eba4d668 | |||
| 9812dd40c4 | |||
| c29816c310 | |||
| 8938e02d8e | |||
| fce7f1bea3 | |||
| fbd249d387 | |||
| 43200007cf | |||
| 4d406fd989 | |||
| 68ab0cff25 | |||
| 407788ee64 | |||
| 41b2ad2b71 | |||
| 1cc1c26585 | |||
| 18e51dff21 | |||
| acd84e53a8 | |||
| 427e1edcfd | |||
| db3b356bb0 | |||
| b37f688755 | |||
| 5585c684e9 | |||
| 075e3171a8 | |||
| 53604c9413 | |||
| 8ae39e24be | |||
| 88c75a06f2 | |||
| 4f2d5b15bf | |||
| f9e310f338 | |||
| 7f292bc995 | |||
| 88a07de5fb | |||
| fc60f6e55e | |||
| 6cfdd4fb90 | |||
| a31b394282 | |||
| 3866ae3449 | |||
| 2a0558de80 | |||
| 989be56f4b | |||
| 05093529bb | |||
| fb613ef5c8 | |||
| f6e64ccb93 | |||
| 8e4ab703c8 | |||
| ef8d49c768 |
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
Add Bedrock prompt caching support (optional).
|
||||
|
||||
This feature protected under checkbox because it is not yet rolled out to everyone, and if you will try to send cache headers, and its not enabled for you, you will get error.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed problem with “win+shift+a” shortcut not working in Windows
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Better Openrouter error typing and throwing more detailed messages
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add webview to lint job
|
||||
+517
@@ -0,0 +1,517 @@
|
||||
# Cline Extension Architecture & Development Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
Cline is a VSCode extension that provides AI assistance through a combination of a core extension backend and a React-based webview frontend. The extension is built with TypeScript and follows a modular architecture pattern.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
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]
|
||||
end
|
||||
|
||||
subgraph Storage
|
||||
TaskStorage[Task Storage<br/>Per-Task Files & History]
|
||||
CheckpointSystem[Git-based Checkpoints]
|
||||
end
|
||||
end
|
||||
|
||||
%% Core Extension Data Flow
|
||||
ExtensionEntry --> ClineProvider
|
||||
ClineProvider --> ClineClass
|
||||
ClineClass --> GlobalState
|
||||
ClineClass --> SecretsStorage
|
||||
ClineClass --> TaskStorage
|
||||
ClineClass --> CheckpointSystem
|
||||
|
||||
%% Webview Data Flow
|
||||
WebviewApp --> ExtStateContext
|
||||
ExtStateContext --> ReactComponents
|
||||
|
||||
%% Bidirectional Communication
|
||||
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 ClineProvider fill:#bfb,stroke:#333,stroke-width:2px
|
||||
```
|
||||
|
||||
## Definitions
|
||||
|
||||
- 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 `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 `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
|
||||
|
||||
The `ExtensionStateContext` in `webview-ui/src/context/ExtensionStateContext.tsx` provides React components with access to the extension's state. It uses a context provider pattern and maintains local state for UI components. The context includes:
|
||||
|
||||
- Extension version
|
||||
- Messages
|
||||
- Task history
|
||||
- Theme
|
||||
- API configurations
|
||||
- MCP servers
|
||||
- Marketplace catalog
|
||||
- Workspace file paths
|
||||
|
||||
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`).
|
||||
|
||||
## Core Extension (Cline.ts)
|
||||
|
||||
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 Cline {
|
||||
async initiateTaskLoop(userContent: UserContent, isNewTask: boolean) {
|
||||
while (!this.abort) {
|
||||
// 1. Make API request and stream response
|
||||
const stream = this.attemptApiRequest()
|
||||
|
||||
// 2. Parse and present content blocks
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
case "text":
|
||||
// Parse into content blocks
|
||||
this.assistantMessageContent = parseAssistantMessage(chunk.text)
|
||||
// Present blocks to user
|
||||
await this.presentAssistantMessage()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Wait for tool execution to complete
|
||||
await pWaitFor(() => this.userMessageContentReady)
|
||||
|
||||
// 4. Continue loop with tool result
|
||||
const recDidEndLoop = await this.recursivelyMakeClineRequests(
|
||||
this.userMessageContent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Message Streaming System
|
||||
|
||||
The streaming system handles real-time updates and partial content:
|
||||
|
||||
```typescript
|
||||
class Cline {
|
||||
async presentAssistantMessage() {
|
||||
// Handle streaming locks to prevent race conditions
|
||||
if (this.presentAssistantMessageLocked) {
|
||||
this.presentAssistantMessageHasPendingUpdates = true
|
||||
return
|
||||
}
|
||||
this.presentAssistantMessageLocked = true
|
||||
|
||||
// Present current content block
|
||||
const block = this.assistantMessageContent[this.currentStreamingContentIndex]
|
||||
|
||||
// Handle different types of content
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
await this.say("text", content, undefined, block.partial)
|
||||
break
|
||||
case "tool_use":
|
||||
// Handle tool execution
|
||||
break
|
||||
}
|
||||
|
||||
// Move to next block if complete
|
||||
if (!block.partial) {
|
||||
this.currentStreamingContentIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Execution Flow
|
||||
|
||||
Tools follow a strict execution pattern:
|
||||
|
||||
```typescript
|
||||
class Cline {
|
||||
async executeToolWithApproval(block: ToolBlock) {
|
||||
// 1. Check auto-approval settings
|
||||
if (this.shouldAutoApproveTool(block.name)) {
|
||||
await this.say("tool", message)
|
||||
this.consecutiveAutoApprovedRequestsCount++
|
||||
} else {
|
||||
// 2. Request user approval
|
||||
const didApprove = await askApproval("tool", message)
|
||||
if (!didApprove) {
|
||||
this.didRejectTool = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Execute tool
|
||||
const result = await this.executeTool(block)
|
||||
|
||||
// 4. Save checkpoint
|
||||
await this.saveCheckpoint()
|
||||
|
||||
// 5. Return result to API
|
||||
return result
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling & Recovery
|
||||
|
||||
The system includes robust error handling:
|
||||
|
||||
```typescript
|
||||
class Cline {
|
||||
async handleError(action: string, error: Error) {
|
||||
// 1. Check if task was abandoned
|
||||
if (this.abandoned) return
|
||||
|
||||
// 2. Format error message
|
||||
const errorString = `Error ${action}: ${error.message}`
|
||||
|
||||
// 3. Present error to user
|
||||
await this.say("error", errorString)
|
||||
|
||||
// 4. Add error to tool results
|
||||
pushToolResult(formatResponse.toolError(errorString))
|
||||
|
||||
// 5. Cleanup resources
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.browserSession.closeBrowser()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### API Request & Token Management
|
||||
|
||||
The Cline class handles API requests with built-in retry, streaming, and token management:
|
||||
|
||||
```typescript
|
||||
class Cline {
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
// 1. Wait for MCP servers to connect
|
||||
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 totalTokens = (tokensIn || 0) + (tokensOut || 0)
|
||||
|
||||
// Truncate conversation if approaching context limit
|
||||
if (totalTokens >= maxAllowedSize) {
|
||||
this.conversationHistoryDeletedRange = getNextTruncationRange(
|
||||
this.apiConversationHistory,
|
||||
this.conversationHistoryDeletedRange,
|
||||
totalTokens / 2 > maxAllowedSize ? "quarter" : "half"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Handle streaming with automatic retry
|
||||
try {
|
||||
this.isWaitingForFirstChunk = true
|
||||
const firstChunk = await iterator.next()
|
||||
yield firstChunk.value
|
||||
this.isWaitingForFirstChunk = false
|
||||
|
||||
// Stream remaining chunks
|
||||
yield* iterator
|
||||
} catch (error) {
|
||||
// 4. Error handling with retry
|
||||
if (isOpenRouter && !this.didAutomaticallyRetryFailedApiRequest) {
|
||||
await delay(1000)
|
||||
this.didAutomaticallyRetryFailedApiRequest = true
|
||||
yield* this.attemptApiRequest(previousApiReqIndex)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. Ask user to retry if automatic retry failed
|
||||
const { response } = await this.ask(
|
||||
"api_req_failed",
|
||||
this.formatErrorWithStatusCode(error)
|
||||
)
|
||||
if (response === "yesButtonClicked") {
|
||||
await this.say("api_req_retried")
|
||||
yield* this.attemptApiRequest(previousApiReqIndex)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key features:
|
||||
|
||||
1. **Context Window Management**
|
||||
- Tracks token usage across requests
|
||||
- Automatically truncates conversation when needed
|
||||
- Preserves important context while freeing space
|
||||
- Handles different model context sizes
|
||||
|
||||
2. **Streaming Architecture**
|
||||
- Real-time chunk processing
|
||||
- Partial content handling
|
||||
- Race condition prevention
|
||||
- Error recovery during streaming
|
||||
|
||||
3. **Error Handling**
|
||||
- Automatic retry for transient failures
|
||||
- User-prompted retry for persistent issues
|
||||
- Detailed error reporting
|
||||
- State cleanup on failure
|
||||
|
||||
4. **Token Tracking**
|
||||
- Per-request token counting
|
||||
- Cumulative usage tracking
|
||||
- Cost calculation
|
||||
- Cache hit monitoring
|
||||
|
||||
### Task State & Resumption
|
||||
|
||||
The Cline class provides robust task state management and resumption capabilities:
|
||||
|
||||
```typescript
|
||||
class Cline {
|
||||
async resumeTaskFromHistory() {
|
||||
// 1. Load saved state
|
||||
this.clineMessages = await this.getSavedClineMessages()
|
||||
this.apiConversationHistory = await this.getSavedApiConversationHistory()
|
||||
|
||||
// 2. Handle interrupted tool executions
|
||||
const lastMessage = this.apiConversationHistory[this.apiConversationHistory.length - 1]
|
||||
if (lastMessage.role === "assistant") {
|
||||
const toolUseBlocks = content.filter(block => block.type === "tool_use")
|
||||
if (toolUseBlocks.length > 0) {
|
||||
// Add interrupted tool responses
|
||||
const toolResponses = toolUseBlocks.map(block => ({
|
||||
type: "tool_result",
|
||||
tool_use_id: block.id,
|
||||
content: "Task was interrupted before this tool call could be completed."
|
||||
}))
|
||||
modifiedOldUserContent = [...toolResponses]
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Notify about interruption
|
||||
const agoText = this.getTimeAgoText(lastMessage?.ts)
|
||||
newUserContent.push({
|
||||
type: "text",
|
||||
text: `[TASK RESUMPTION] This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context.`
|
||||
})
|
||||
|
||||
// 4. Resume task execution
|
||||
await this.initiateTaskLoop(newUserContent, false)
|
||||
}
|
||||
|
||||
private async saveTaskState() {
|
||||
// Save conversation history
|
||||
await this.saveApiConversationHistory()
|
||||
await this.saveClineMessages()
|
||||
|
||||
// Create checkpoint
|
||||
const commitHash = await this.checkpointTracker?.commit()
|
||||
|
||||
// Update task history
|
||||
await this.providerRef.deref()?.updateTaskHistory({
|
||||
id: this.taskId,
|
||||
ts: lastMessage.ts,
|
||||
task: taskMessage.text,
|
||||
// ... other metadata
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key aspects of task state management:
|
||||
|
||||
1. **Task Persistence**
|
||||
- Each task has a unique ID and dedicated storage directory
|
||||
- Conversation history is saved after each message
|
||||
- File changes are tracked through Git-based checkpoints
|
||||
- Terminal output and browser state are preserved
|
||||
|
||||
2. **State Recovery**
|
||||
- Tasks can be resumed from any point
|
||||
- Interrupted tool executions are handled gracefully
|
||||
- File changes can be restored from checkpoints
|
||||
- Context is preserved across VSCode sessions
|
||||
|
||||
3. **Workspace Synchronization**
|
||||
- File changes are tracked through Git
|
||||
- Checkpoints are created after tool executions
|
||||
- State can be restored to any checkpoint
|
||||
- Changes can be compared between checkpoints
|
||||
|
||||
4. **Error Recovery**
|
||||
- Failed API requests can be retried
|
||||
- Interrupted tool executions are marked
|
||||
- Resources are cleaned up properly
|
||||
- User is notified of state changes
|
||||
|
||||
## Data Flow & State Management
|
||||
|
||||
### Core Extension Role
|
||||
|
||||
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
|
||||
- Handles task-specific state persistence
|
||||
- Manages checkpoint creation and restoration
|
||||
|
||||
### Terminal Management
|
||||
|
||||
The Cline class manages terminal instances and command execution:
|
||||
|
||||
```typescript
|
||||
class Cline {
|
||||
async executeCommandTool(command: string): Promise<[boolean, ToolResponse]> {
|
||||
// 1. Get or create terminal
|
||||
const terminalInfo = await this.terminalManager.getOrCreateTerminal(cwd)
|
||||
terminalInfo.terminal.show()
|
||||
|
||||
// 2. Execute command with output streaming
|
||||
const process = this.terminalManager.runCommand(terminalInfo, command)
|
||||
|
||||
// 3. Handle real-time output
|
||||
let result = ""
|
||||
process.on("line", (line) => {
|
||||
result += line + "\n"
|
||||
if (!didContinue) {
|
||||
sendCommandOutput(line)
|
||||
} else {
|
||||
this.say("command_output", line)
|
||||
}
|
||||
})
|
||||
|
||||
// 4. Wait for completion or user feedback
|
||||
let completed = false
|
||||
process.once("completed", () => {
|
||||
completed = true
|
||||
})
|
||||
|
||||
await process
|
||||
|
||||
// 5. Return result
|
||||
if (completed) {
|
||||
return [false, `Command executed.\n${result}`]
|
||||
} else {
|
||||
return [
|
||||
false,
|
||||
`Command is still running in the user's terminal.\n${result}\n\nYou will be updated on the terminal status and new output in the future.`
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key features:
|
||||
1. **Terminal Instance Management**
|
||||
- Multiple terminal support
|
||||
- Terminal state tracking (busy/inactive)
|
||||
- Process cooldown monitoring
|
||||
- Output history per terminal
|
||||
|
||||
2. **Command Execution**
|
||||
- Real-time output streaming
|
||||
- User feedback handling
|
||||
- Process state monitoring
|
||||
- Error recovery
|
||||
|
||||
### Browser Session Management
|
||||
|
||||
The Cline class handles browser automation through Puppeteer:
|
||||
|
||||
```typescript
|
||||
class Cline {
|
||||
async executeBrowserAction(action: BrowserAction): Promise<BrowserActionResult> {
|
||||
switch (action) {
|
||||
case "launch":
|
||||
// 1. Launch browser with fixed resolution
|
||||
await this.browserSession.launchBrowser()
|
||||
return await this.browserSession.navigateToUrl(url)
|
||||
|
||||
case "click":
|
||||
// 2. Handle click actions with coordinates
|
||||
return await this.browserSession.click(coordinate)
|
||||
|
||||
case "type":
|
||||
// 3. Handle keyboard input
|
||||
return await this.browserSession.type(text)
|
||||
|
||||
case "close":
|
||||
// 4. Clean up resources
|
||||
return await this.browserSession.closeBrowser()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key aspects:
|
||||
1. **Browser Control**
|
||||
- Fixed 900x600 resolution window
|
||||
- Single instance per task lifecycle
|
||||
- Automatic cleanup on task completion
|
||||
- Console log capture
|
||||
|
||||
2. **Interaction Handling**
|
||||
- Coordinate-based clicking
|
||||
- Keyboard input simulation
|
||||
- Screenshot capture
|
||||
- Error recovery
|
||||
|
||||
## 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 exists in the src/ folder
|
||||
- Use proper typing for all state and messages
|
||||
- Handle errors and edge cases
|
||||
- Test state persistence across webview reloads
|
||||
- Follow the established patterns for consistency
|
||||
- Place new code in appropriate directories
|
||||
- Maintain clear separation of concerns
|
||||
- Install dependencies in correct package.json
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to the Cline extension are welcome! Please follow these guidelines:
|
||||
|
||||
When adding new tools or API providers, follow the existing patterns in the `src/integrations/` and `src/api/providers/` directories, respectively. Ensure that your code is well-documented and includes appropriate error handling.
|
||||
|
||||
The `.clineignore` file allows users to specify files and directories that Cline should not access. When implementing new features, respect the `.clineignore` rules and ensure that your code does not attempt to read or modify ignored files.
|
||||
+1
-1
@@ -1 +1 @@
|
||||
* @saoudrizwan @ocasta181 @NightTrek @pashpashpash
|
||||
* @saoudrizwan @ocasta181 @NightTrek @pashpashpash @dcbartlett
|
||||
|
||||
@@ -28,10 +28,7 @@ updates:
|
||||
patterns:
|
||||
- "*"
|
||||
ignore:
|
||||
# Ignore CRA and related packages that often have false positives
|
||||
- dependency-name: "react-scripts"
|
||||
- dependency-name: "@testing-library/*"
|
||||
- dependency-name: "web-vitals"
|
||||
- dependency-name: "*"
|
||||
update-types:
|
||||
- "version-update:semver-major"
|
||||
|
||||
+3
-1
@@ -7,4 +7,6 @@ tmp
|
||||
|
||||
.DS_Store
|
||||
|
||||
pnpm-lock.yaml
|
||||
pnpm-lock.yaml
|
||||
|
||||
.clineignore
|
||||
Vendored
+5
-1
@@ -11,7 +11,11 @@
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+59
-4
@@ -5,7 +5,7 @@
|
||||
"tasks": [
|
||||
{
|
||||
"label": "watch",
|
||||
"dependsOn": ["npm: build:webview", "npm: watch:tsc", "npm: watch:esbuild"],
|
||||
"dependsOn": ["npm: build:webview", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild"],
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
@@ -23,7 +23,47 @@
|
||||
"label": "npm: build:webview",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never"
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev:webview",
|
||||
"group": "build",
|
||||
"problemMatcher": [
|
||||
{
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": ".",
|
||||
"file": 1,
|
||||
"location": 2,
|
||||
"message": 3
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": ".",
|
||||
"endsPattern": "."
|
||||
}
|
||||
}
|
||||
],
|
||||
"isBackground": true,
|
||||
"label": "npm: dev:webview",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -35,7 +75,8 @@
|
||||
"label": "npm: watch:esbuild",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never"
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -47,7 +88,8 @@
|
||||
"label": "npm: watch:tsc",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never"
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -65,6 +107,19 @@
|
||||
"label": "tasks: watch-tests",
|
||||
"dependsOn": ["npm: watch", "npm: watch-tests"],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "stop",
|
||||
"command": "echo ${input:terminate}",
|
||||
"type": "shell"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "terminate",
|
||||
"type": "command",
|
||||
"command": "workbench.action.tasks.terminate",
|
||||
"args": "terminateAll"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ demo.gif
|
||||
# Ignore all webview-ui files except the build directory (https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/frameworks/hello-world-react-cra/.vscodeignore)
|
||||
webview-ui/src/**
|
||||
webview-ui/public/**
|
||||
webview-ui/scripts/**
|
||||
webview-ui/index.html
|
||||
webview-ui/README.md
|
||||
webview-ui/package.json
|
||||
|
||||
+49
-1
@@ -1,5 +1,53 @@
|
||||
# Changelog
|
||||
|
||||
## [3.5.1]
|
||||
|
||||
- Add timeout option to MCP servers
|
||||
- Add Gemini Flash models to Vertex provider (thanks @jpaodev!)
|
||||
- Add prompt caching support for AWS Bedrock provider (thanks @buger!)
|
||||
- Add AskSage provider (thanks @swhite24!)
|
||||
|
||||
## [3.5.0]
|
||||
|
||||
- Add 'Enable extended thinking' option for Claude 3.7 Sonnet, with ability to set different budgets for Plan and Act modes
|
||||
- Add support for rich MCP responses with automatic image previews, website thumbnails, and WolframAlpha visualizations
|
||||
- Add language preference option in Advanced Settings
|
||||
- Add xAI Provider Integration with support for all Grok models (thanks @andrewmonostate!)
|
||||
- Fix issue with Linux XDG pointing to incorrect path for Document folder (thanks @jonatkinson!)
|
||||
|
||||
## [3.4.10]
|
||||
|
||||
- Add support for GPT-4.5 preview model
|
||||
|
||||
## [3.4.9]
|
||||
|
||||
- Add toggle to let users opt-in to anonymous telemetry and error reporting
|
||||
|
||||
## [3.4.6]
|
||||
|
||||
- Add support for Claude 3.7 Sonnet
|
||||
|
||||
## [3.4.0]
|
||||
|
||||
- Introducing MCP Marketplace! You can now discover and install the best MCP servers right from within the extension, with new servers added regularly
|
||||
- Add mermaid diagram support in Plan mode! You can now see visual representations of mermaid code blocks in chat, and click on them to see an expanded view
|
||||
- Use more visual checkpoints indicators after editing files & running commands
|
||||
- Create a checkpoint at the beginning of each task to easily revert to the initial state
|
||||
- Add 'Terminal' context mention to reference the active terminal's contents
|
||||
- Add 'Git Commits' context mention to reference current working changes or specific commits (thanks @mrubens!)
|
||||
- Send current textfield contents as additional feedback when toggling from Plan to Act Mode, or when hitting 'Approve' button
|
||||
- Add advanced configuration options for OpenAI Compatible (context window, max output, pricing, etc.)
|
||||
- Add Alibaba Qwen 2.5 coder models, VL models, and DeepSeek-R1/V3 support
|
||||
- Improve support for AWS Bedrock Profiles
|
||||
- Fix Mistral provider support for non-codestral models
|
||||
- Add advanced setting to disable browser tool
|
||||
- Add advanced setting to set chromium executable path for browser tool
|
||||
|
||||
## [3.3.2]
|
||||
|
||||
- Fix bug where OpenRouter requests would periodically not return cost/token stats, leading to context window limit errors
|
||||
- Make checkpoints more visible and keep track of restored checkpoints
|
||||
|
||||
## [3.3.0]
|
||||
|
||||
- Add .clineignore to block Cline from accessing specified file patterns
|
||||
@@ -29,7 +77,7 @@
|
||||
|
||||
## [3.2.10]
|
||||
|
||||
- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct
|
||||
- Improve support for DeepSeek-R1 (deepseek-reasoner) model for OpenRouter, OpenAI-compatible, and DeepSeek direct (thanks @Szpadel!)
|
||||
- Show Reasoning tokens for models that support it
|
||||
- Fix issues with switching models between Plan/Act modes
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md
|
||||
|
||||
Meet Cline, an AI assistant that can use your **CLI** a**N**d **E**ditor.
|
||||
|
||||
Thanks to [Claude 3.5 Sonnet's agentic coding capabilities](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
|
||||
Thanks to [Claude 3.7 Sonnet's agentic coding capabilities](https://www.anthropic.com/claude/sonnet), Cline can handle complex software development tasks step-by-step. With tools that let him create & edit files, explore large projects, use the browser, and execute terminal commands (after you grant permission), he can assist you in ways that go beyond code completion or tech support. Cline can even use the Model Context Protocol (MCP) to create new tools and extend his own capabilities. While autonomous AI scripts traditionally run in sandboxed environments, this extension provides a human-in-the-loop GUI to approve every file change and terminal command, providing a safe and accessible way to explore the potential of agentic AI.
|
||||
|
||||
1. Enter your task and add images to convert mockups into functional apps or fix bugs with screenshots.
|
||||
2. Cline starts by analyzing your file structure & source code ASTs, running regex searches, and reading relevant files to get up to speed in existing projects. By carefully managing what information is added to context, Cline can provide valuable assistance even for large, complex projects without overwhelming the context window.
|
||||
|
||||
+20
-3
@@ -42,8 +42,8 @@ Cline functions solely as a client-side VS Code extension that facilitates commu
|
||||
1. **Local-Only Processing**:
|
||||
|
||||
- All operations happen on your local machine
|
||||
- No central servers or data collection
|
||||
- No telemetry or usage statistics gathered
|
||||
- No central servers or data collection by default
|
||||
- Anonymous telemetry and usage statistics are only collected if you explicitly opt in
|
||||
- No account creation required
|
||||
|
||||
2. **API Key Security**:
|
||||
@@ -73,7 +73,8 @@ When you request assistance:
|
||||
|
||||
- Error logs are processed locally
|
||||
- No automatic error reporting to Cline
|
||||
- You control what information to include when reporting issues
|
||||
- Optional anonymous telemetry and error reporting via PostHog if you opt in
|
||||
- You control what information to include when manually reporting issues
|
||||
|
||||
## Children's Privacy
|
||||
|
||||
@@ -90,6 +91,22 @@ We will post any changes to this policy on our GitHub repository. Significant ch
|
||||
- You can inspect exactly what data is being sent to AI providers
|
||||
- Enterprise users can implement additional access controls through VS Code
|
||||
|
||||
## Telemetry & Usage Statistics
|
||||
|
||||
If you choose to opt in to anonymous telemetry:
|
||||
|
||||
- Basic usage statistics and error reports are collected via PostHog
|
||||
- A stable, anonymous identifier (VS Code's `machineId`) is used to understand unique usage patterns
|
||||
- This identifier is not linked to any personal information
|
||||
- It helps us understand how features are used across sessions
|
||||
- It cannot be used to identify you personally
|
||||
- All data is anonymized and cannot be linked to individual users
|
||||
- No code content or sensitive information is ever included
|
||||
- You can opt out at any time through:
|
||||
- VS Code Settings > Cline > Enable Telemetry
|
||||
- VS Code Settings > Telemetry > Telemetry Level (setting this to anything other than "all" will disable Cline's telemetry)
|
||||
- Collected data helps us improve the extension's functionality and stability
|
||||
|
||||
## Contact Us
|
||||
|
||||
For privacy-related questions or concerns:
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ Welcome to the Cline documentation - your comprehensive guide to using and exten
|
||||
|
||||
- **Interested in contributing?** We welcome your input:
|
||||
- Feel free to submit a pull request
|
||||
- [Contribution Guidelines](CONTRIBUTING.md)
|
||||
- [Contribution Guidelines](../CONTRIBUTING.md)
|
||||
|
||||
## Additional Resources
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Cline Extension Architecture
|
||||
|
||||
This directory contains architectural documentation for the Cline VSCode extension.
|
||||
|
||||
## Extension Architecture Diagram
|
||||
|
||||
The [extension-architecture.mmd](./extension-architecture.mmd) file contains a Mermaid diagram showing the high-level architecture of the Cline extension. The diagram illustrates:
|
||||
|
||||
1. **Core Extension**
|
||||
- Extension entry point and main classes
|
||||
- State management through VSCode's global state and secrets storage
|
||||
- Core business logic in the Cline class
|
||||
|
||||
2. **Webview UI**
|
||||
- React-based user interface
|
||||
- State management through ExtensionStateContext
|
||||
- Component hierarchy
|
||||
|
||||
3. **Storage**
|
||||
- Task-specific storage for history and state
|
||||
- Git-based checkpoint system for file changes
|
||||
|
||||
4. **Data Flow**
|
||||
- Core extension data flow between components
|
||||
- Webview UI data flow
|
||||
- Bidirectional communication between core and webview
|
||||
|
||||
## Viewing the Diagram
|
||||
|
||||
To view the diagram:
|
||||
1. Install a Mermaid diagram viewer extension in VSCode
|
||||
2. Open extension-architecture.mmd
|
||||
3. Use the extension's preview feature to render the diagram
|
||||
|
||||
You can also view the diagram on GitHub, which has built-in Mermaid rendering support.
|
||||
|
||||
## Color Scheme
|
||||
|
||||
The diagram uses a high-contrast color scheme for better visibility:
|
||||
- Pink (#ff0066): Global state and secrets storage components
|
||||
- Blue (#0066ff): Extension state context
|
||||
- Green (#00cc66): Cline provider
|
||||
- All components use white text for maximum readability
|
||||
@@ -0,0 +1,41 @@
|
||||
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]
|
||||
end
|
||||
|
||||
subgraph Storage
|
||||
TaskStorage[Task Storage<br/>Per-Task Files & History]
|
||||
CheckpointSystem[Git-based Checkpoints]
|
||||
end
|
||||
end
|
||||
|
||||
%% Core Extension Data Flow
|
||||
ExtensionEntry --> ClineProvider
|
||||
ClineProvider --> ClineClass
|
||||
ClineClass --> GlobalState
|
||||
ClineClass --> SecretsStorage
|
||||
ClineClass --> TaskStorage
|
||||
ClineClass --> CheckpointSystem
|
||||
|
||||
%% Webview Data Flow
|
||||
WebviewApp --> ExtStateContext
|
||||
ExtStateContext --> ReactComponents
|
||||
|
||||
%% Bidirectional Communication
|
||||
ClineProvider <-->|postMessage| ExtStateContext
|
||||
|
||||
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
|
||||
@@ -0,0 +1,54 @@
|
||||
### .clineignore Support
|
||||
|
||||
To give you more control over which files are accessible to Cline, we've implemented `.clineignore` functionality, similar to `.gitignore`. This allows you to specify files and directories that Cline should **not** access or process. This is useful for:
|
||||
|
||||
* **Privacy:** Preventing Cline from accessing sensitive or private files in your workspace.
|
||||
* **Performance:** Excluding large directories or files that are irrelevant to your tasks, potentially improving the efficiency of Cline.
|
||||
* **Context Management:** Focusing Cline's attention on the relevant parts of your project.
|
||||
|
||||
**How to use `.clineignore`**
|
||||
|
||||
1. **Create a `.clineignore` file:** In the root directory of your workspace (the same level as your `.vscode` folder, or the top level folder you opened in VS Code), create a new file named `.clineignore`.
|
||||
|
||||
2. **Define ignore patterns:** Open the `.clineignore` file and specify the patterns for files and directories you want Cline to ignore. The syntax is the same as `.gitignore`:
|
||||
|
||||
* Each line in the file represents a pattern.
|
||||
* **Standard glob patterns are supported:**
|
||||
* `*` matches zero or more characters
|
||||
* `?` matches one character
|
||||
* `[]` matches a character range
|
||||
* `**` matches any number of directories and subdirectories.
|
||||
|
||||
* **Directory patterns:** Append `/` to the end of a pattern to specify a directory.
|
||||
* **Negation patterns:** Start a pattern with `!` to negate (un-ignore) a previously ignored pattern.
|
||||
* **Comments:** Start a line with `#` to add comments.
|
||||
|
||||
**Example `.clineignore` file:**
|
||||
|
||||
```
|
||||
# Ignore log files
|
||||
*.log
|
||||
|
||||
# Ignore the entire 'node_modules' directory
|
||||
node_modules/
|
||||
|
||||
# Ignore all files in the 'temp' directory and its subdirectories
|
||||
temp/**
|
||||
|
||||
# But DO NOT ignore 'important.log' even if it's in the root
|
||||
!important.log
|
||||
|
||||
# Ignore any file named 'secret.txt' in any subdirectory
|
||||
**/secret.txt
|
||||
```
|
||||
|
||||
3. **Cline respects your `.clineignore`:** Once you save the `.clineignore` file, Cline will automatically recognize and apply these rules.
|
||||
|
||||
* **File Access Control:** Cline will not be able to read the content of ignored files using tools like `read_file`. If you attempt to use a tool on an ignored file, Cline will inform you that access is blocked due to `.clineignore` settings.
|
||||
* **File Listing:** When you ask Cline to list files in a directory (e.g., using `list_files`), ignored files and directories will still be listed, but they will be marked with a **🔒** symbol next to their name to indicate that they are ignored. This helps you understand which files Cline can and cannot interact with.
|
||||
|
||||
4. **Dynamic Updates:** Cline monitors your `.clineignore` file for changes. If you modify, create, or delete your `.clineignore` file, Cline will automatically update its ignore rules without needing to restart VS Code or the extension.
|
||||
|
||||
**In Summary**
|
||||
|
||||
The `.clineignore` file provides a powerful and flexible way to control Cline's access to your workspace files, enhancing privacy, performance, and context management. By leveraging familiar `.gitignore` syntax, you can easily tailor Cline's focus to the most relevant parts of your projects.
|
||||
@@ -0,0 +1,153 @@
|
||||
# Cline's Memory Bank
|
||||
|
||||
I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.
|
||||
|
||||
## Memory Bank Structure
|
||||
|
||||
The Memory Bank consists of required core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
PB[projectbrief.md] --> PC[productContext.md]
|
||||
PB --> SP[systemPatterns.md]
|
||||
PB --> TC[techContext.md]
|
||||
|
||||
PC --> AC[activeContext.md]
|
||||
SP --> AC
|
||||
TC --> AC
|
||||
|
||||
AC --> P[progress.md]
|
||||
```
|
||||
|
||||
### Core Files (Required)
|
||||
1. `projectbrief.md`
|
||||
- Foundation document that shapes all other files
|
||||
- Created at project start if it doesn't exist
|
||||
- Defines core requirements and goals
|
||||
- Source of truth for project scope
|
||||
|
||||
2. `productContext.md`
|
||||
- Why this project exists
|
||||
- Problems it solves
|
||||
- How it should work
|
||||
- User experience goals
|
||||
|
||||
3. `activeContext.md`
|
||||
- Current work focus
|
||||
- Recent changes
|
||||
- Next steps
|
||||
- Active decisions and considerations
|
||||
|
||||
4. `systemPatterns.md`
|
||||
- System architecture
|
||||
- Key technical decisions
|
||||
- Design patterns in use
|
||||
- Component relationships
|
||||
|
||||
5. `techContext.md`
|
||||
- Technologies used
|
||||
- Development setup
|
||||
- Technical constraints
|
||||
- Dependencies
|
||||
|
||||
6. `progress.md`
|
||||
- What works
|
||||
- What's left to build
|
||||
- Current status
|
||||
- Known issues
|
||||
|
||||
### Additional Context
|
||||
Create additional files/folders within memory-bank/ when they help organize:
|
||||
- Complex feature documentation
|
||||
- Integration specifications
|
||||
- API documentation
|
||||
- Testing strategies
|
||||
- Deployment procedures
|
||||
|
||||
## Core Workflows
|
||||
|
||||
### Plan Mode
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Start] --> ReadFiles[Read Memory Bank]
|
||||
ReadFiles --> CheckFiles{Files Complete?}
|
||||
|
||||
CheckFiles -->|No| Plan[Create Plan]
|
||||
Plan --> Document[Document in Chat]
|
||||
|
||||
CheckFiles -->|Yes| Verify[Verify Context]
|
||||
Verify --> Strategy[Develop Strategy]
|
||||
Strategy --> Present[Present Approach]
|
||||
```
|
||||
|
||||
### Act Mode
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Start] --> Context[Check Memory Bank]
|
||||
Context --> Update[Update Documentation]
|
||||
Update --> Rules[Update .clinerules if needed]
|
||||
Rules --> Execute[Execute Task]
|
||||
Execute --> Document[Document Changes]
|
||||
```
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
Memory Bank updates occur when:
|
||||
1. Discovering new project patterns
|
||||
2. After implementing significant changes
|
||||
3. When user requests with **update memory bank** (MUST review ALL files)
|
||||
4. When context needs clarification
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Update Process]
|
||||
|
||||
subgraph Process
|
||||
P1[Review ALL Files]
|
||||
P2[Document Current State]
|
||||
P3[Clarify Next Steps]
|
||||
P4[Update .clinerules]
|
||||
|
||||
P1 --> P2 --> P3 --> P4
|
||||
end
|
||||
|
||||
Start --> Process
|
||||
```
|
||||
|
||||
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.
|
||||
|
||||
## Project Intelligence (.clinerules)
|
||||
|
||||
The .clinerules file is my learning journal for each project. It captures important patterns, preferences, and project intelligence that help me work more effectively. As I work with you and the project, I'll discover and document key insights that aren't obvious from the code alone.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start{Discover New Pattern}
|
||||
|
||||
subgraph Learn [Learning Process]
|
||||
D1[Identify Pattern]
|
||||
D2[Validate with User]
|
||||
D3[Document in .clinerules]
|
||||
end
|
||||
|
||||
subgraph Apply [Usage]
|
||||
A1[Read .clinerules]
|
||||
A2[Apply Learned Patterns]
|
||||
A3[Improve Future Work]
|
||||
end
|
||||
|
||||
Start --> Learn
|
||||
Learn --> Apply
|
||||
```
|
||||
|
||||
### What to Capture
|
||||
- Critical implementation paths
|
||||
- User preferences and workflow
|
||||
- Project-specific patterns
|
||||
- Known challenges
|
||||
- Evolution of project decisions
|
||||
- Tool usage patterns
|
||||
|
||||
The format is flexible - focus on capturing valuable insights that help me work more effectively with you and the project. Think of .clinerules as a living document that grows smarter as we work together.
|
||||
|
||||
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
|
||||
@@ -0,0 +1,47 @@
|
||||
# ميثاق المساهمين
|
||||
|
||||
## تعهدنا
|
||||
|
||||
نحن المساهمون والقائمون على هذا المشروع، نتعهد بتوفير بيئة مفتوحة ومرحبة، ونجعل المشاركة في مشروعنا ومجتمعنا تجربة خالية من التحرش للجميع، بغض النظر عن العمر، أو حجم الجسم، أو الإعاقة، أو العرق، أو الخصائص الجنسية، أو الهوية الجنسية والتعبير عنها، أو مستوى الخبرة، أو التعليم، أو الوضع الاجتماعي والاقتصادي، أو الجنسية، أو المظهر الشخصي، أو الدين، أو الهوية الجنسية والتوجه الجنسي.
|
||||
|
||||
## معاييرنا
|
||||
|
||||
أمثلة على السلوك الذي يساهم في خلق بيئة إيجابية تشمل:
|
||||
|
||||
- استخدام لغة ترحيبية وشاملة
|
||||
- احترام وجهات النظر والخبرات المختلفة
|
||||
- تقبل النقد البناء برحابة صدر
|
||||
- التركيز على ما هو الأفضل للمجتمع
|
||||
- إظهار التعاطف تجاه أعضاء المجتمع الآخرين
|
||||
|
||||
أمثلة على السلوك غير المقبول من قبل المشاركين تشمل:
|
||||
|
||||
- استخدام لغة أو صور جنسية والاهتمام الجنسي غير المرغوب فيه أو التحرش الجنسي
|
||||
- التصيد، والتعليقات المهينة/المسيئة، والهجمات الشخصية أو السياسية
|
||||
- التحرش العلني أو الخاص
|
||||
- نشر معلومات الآخرين الخاصة، مثل العنوان الفعلي أو الإلكتروني، دون إذن صريح
|
||||
- أي سلوك آخر يمكن اعتباره غير لائق في بيئة مهنية
|
||||
|
||||
## مسؤولياتنا
|
||||
|
||||
يتحمل القائمون على المشروع مسؤولية توضيح معايير السلوك المقبول، ومن المتوقع أن يتخذوا إجراءات تصحيحية مناسبة وعادلة استجابة لأي حالات سلوك غير مقبول.
|
||||
|
||||
يحق للقائمين على المشروع إزالة أو تعديل أو رفض التعليقات والالتزامات والتعليمات البرمجية وتعديلات wiki والمشكلات والمساهمات الأخرى التي لا تتماشى مع مدونة قواعد السلوك هذه، أو حظر أي مساهم بشكل مؤقت أو دائم بسبب سلوكيات أخرى يعتبرونها غير لائقة أو مهددة أو مسيئة أو ضارة، كما أنهم يتحملون مسؤولية ذلك.
|
||||
|
||||
## النطاق
|
||||
|
||||
تنطبق مدونة قواعد السلوك هذه داخل مساحات المشروع وفي الأماكن العامة عندما يمثل الفرد المشروع أو مجتمعه. تتضمن أمثلة تمثيل مشروع أو مجتمع استخدام عنوان بريد إلكتروني رسمي للمشروع، أو النشر عبر حساب رسمي على وسائل التواصل الاجتماعي، أو العمل كممثل معين في حدث عبر الإنترنت أو خارجه. يمكن للقائمين على المشروع تحديد وتوضيح تمثيل المشروع بشكل أكبر.
|
||||
|
||||
## التنفيذ
|
||||
|
||||
يمكن الإبلاغ عن حالات السلوك المسيء أو التحرش أو السلوك غير المقبول عن طريق الاتصال بفريق المشروع على hi@cline.bot. ستتم مراجعة جميع الشكاوى والتحقيق فيها وستؤدي إلى استجابة تعتبر ضرورية ومناسبة للظروف. يلتزم فريق المشروع بالحفاظ على السرية فيما يتعلق بالمبلغ عن الحادث. يمكن نشر مزيد من التفاصيل حول سياسات التنفيذ المحددة بشكل منفصل.
|
||||
|
||||
قد يواجه القائمون على المشروع الذين لا يتبعون أو يفرضون مدونة قواعد السلوك بحسن نية تداعيات مؤقتة أو دائمة على النحو الذي يحدده الأعضاء الآخرون في قيادة المشروع.
|
||||
|
||||
## الإسناد
|
||||
|
||||
تم اقتباس مدونة قواعد السلوك هذه من [تعهد المساهم][homepage]، الإصدار 1.4، متاح على https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
للحصول على إجابات للأسئلة الشائعة حول مدونة قواعد السلوك هذه، راجع https://www.contributor-covenant.org/faq
|
||||
@@ -0,0 +1,93 @@
|
||||
# المساهمة في Cline
|
||||
|
||||
نحن سعداء لاهتمامك بالمساهمة في Cline. سواء كنت تصلح خطأً أو تضيف ميزة أو تحسن الوثائق لدينا، فإن كل مساهمة تجعل Cline أذكى! للحفاظ على مجتمعنا نابضًا بالحياة وترحيبيًا، يجب على جميع الأعضاء الالتزام بـ [مدونة قواعد السلوك](CODE_OF_CONDUCT.md) لدينا.
|
||||
|
||||
## الإبلاغ عن الأخطاء أو المشكلات
|
||||
|
||||
تساعد تقارير الأخطاء على جعل Cline أفضل للجميع! قبل إنشاء مشكلة جديدة، يرجى [البحث عن المشكلات الموجودة](https://github.com/cline/cline/issues) لتجنب الازدواجية. عندما تكون جاهزًا للإبلاغ عن خطأ، انتقل إلى [صفحة المشكلات](https://github.com/cline/cline/issues/new/choose) حيث ستجد قالبًا لمساعدتك في ملء المعلومات ذات الصلة.
|
||||
|
||||
<blockquote class='warning-note'>
|
||||
🔐 <b>مهم:</b> إذا اكتشفت ثغرة أمنية، فيرجى استخدام <a href="https://github.com/cline/cline/security/advisories/new">أداة الأمان على Github للإبلاغ عنها بشكل خاص</a>.
|
||||
</blockquote>
|
||||
|
||||
## تحديد ما يجب العمل عليه
|
||||
|
||||
تبحث عن مساهمة أولى جيدة؟ تحقق من المشكلات المميزة بـ ["good first issue"](https://github.com/cline/cline/labels/good%20first%20issue) أو ["help wanted"](https://github.com/cline/cline/labels/help%20wanted). تم تحديد هذه المشكلات خصيصًا للمساهمين الجدد والمجالات التي نرحب فيها بالمساعدة!
|
||||
|
||||
نرحب أيضًا بالمساهمات في [الوثائق](https://github.com/cline/cline/tree/main/docs) لدينا! سواء كان تصحيح أخطاء إملائية، أو تحسين الأدلة الحالية، أو إنشاء محتوى تعليمي جديد - نود بناء مستودع موارد مدفوع من المجتمع يساعد الجميع على الاستفادة القصوى من Cline. يمكنك البدء بالغوص في `/docs` والبحث عن مجالات تحتاج إلى تحسين.
|
||||
|
||||
إذا كنت تخطط للعمل على ميزة أكبر، فيرجى إنشاء [طلب ميزة](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop) أولاً حتى نتمكن من مناقشة ما إذا كان ذلك يتماشى مع رؤية Cline.
|
||||
|
||||
## إعداد التطوير
|
||||
|
||||
1. **إضافات VS Code**
|
||||
|
||||
- عند فتح المشروع، سيطالبك VS Code بتثبيت الإضافات الموصى بها
|
||||
- هذه الإضافات مطلوبة للتطوير - يرجى قبول جميع مطالبات التثبيت
|
||||
- إذا تجاهلت المطالبات، يمكنك تثبيتها يدويًا من لوحة الإضافات
|
||||
|
||||
2. **التطوير المحلي**
|
||||
- قم بتشغيل `npm run install:all` لتثبيت التبعيات
|
||||
- قم بتشغيل `npm run test` لتشغيل الاختبارات محليًا
|
||||
- قبل تقديم طلب السحب، قم بتشغيل `npm run format:fix` لتنسيق التعليمات البرمجية الخاصة بك
|
||||
|
||||
## كتابة وتقديم التعليمات البرمجية
|
||||
|
||||
يمكن لأي شخص المساهمة بالتعليمات البرمجية في Cline، لكننا نطلب منك اتباع هذه الإرشادات لضمان دمج مساهماتك بسلاسة:
|
||||
|
||||
1. **احتفظ بطلبات السحب مركزة**
|
||||
|
||||
- قيد طلبات السحب بميزة واحدة أو إصلاح خطأ
|
||||
- قسم التغييرات الأكبر إلى طلبات سحب أصغر ومتصلة
|
||||
- قسم التغييرات إلى التزامات منطقية يمكن مراجعتها بشكل مستقل
|
||||
|
||||
2. **جودة التعليمات البرمجية**
|
||||
|
||||
- قم بتشغيل `npm run lint` للتحقق من نمط التعليمات البرمجية
|
||||
- قم بتشغيل `npm run format` لتنسيق التعليمات البرمجية تلقائيًا
|
||||
- يجب أن تجتاز جميع طلبات السحب عمليات التحقق المستمر التي تشمل كلاً من التنضيد والتنسيق
|
||||
- تعامل مع أي تحذيرات أو أخطاء ESLint قبل التقديم
|
||||
- اتبع أفضل ممارسات TypeScript والحفاظ على سلامة النوع
|
||||
|
||||
3. **الاختبار**
|
||||
|
||||
- أضف اختبارات للميزات الجديدة
|
||||
- قم بتشغيل `npm test` للتأكد من اجتياز جميع الاختبارات
|
||||
- قم بتحديث الاختبارات الحالية إذا كانت تغييراتك تؤثر عليها
|
||||
- تضمين كل من اختبارات الوحدة واختبارات التكامل حيثما كان ذلك مناسبًا
|
||||
|
||||
4. **إدارة الإصدار مع Changesets**
|
||||
|
||||
- أنشئ changeset لأي تغييرات واجهة المستخدم باستخدام `npm run changeset`
|
||||
- اختر زيادة الإصدار المناسبة:
|
||||
- `major` للتغييرات الكبيرة (1.0.0 → 2.0.0)
|
||||
- `minor` للميزات الجديدة (1.0.0 → 1.1.0)
|
||||
- `patch` لإصلاحات الأخطاء (1.0.0 → 1.0.1)
|
||||
- اكتب رسائل changeset واضحة ووصفية تشرح التأثير
|
||||
- لا تتطلب التغييرات في الوثائق فقط changesets
|
||||
|
||||
5. **إرشادات الالتزام (Commit Guidelines)**
|
||||
|
||||
- اكتب رسائل التزام واضحة وواصفة
|
||||
- استخدم تنسيق الالتزام التقليدي (مثل: "feat:", "fix:", "docs:")
|
||||
- أشر إلى القضايا ذات الصلة في الالتزامات باستخدام #رقم-القضية
|
||||
|
||||
6. **قبل الإرسال**
|
||||
|
||||
- قم بإعادة دمج فرعك مع أحدث إصدار من الفرع الرئيسي
|
||||
- تأكد من أن الفرع الخاص بك يُبنى بنجاح
|
||||
- تحقق من اجتياز جميع الاختبارات
|
||||
- راجع التغييرات الخاصة بك للتأكد من عدم وجود تعليمات تصحيح الأخطاء أو سجلات وحدة التحكم
|
||||
|
||||
7. **وصف طلب السحب (Pull Request Description)**
|
||||
|
||||
- صف بوضوح ما تقوم به التغييرات
|
||||
- قم بتضمين خطوات لاختبار التغييرات
|
||||
- أدرج أي تغييرات غير متوافقة
|
||||
- أضف لقطات شاشة للتغييرات في واجهة المستخدم
|
||||
|
||||
## اتفاقية المساهمة
|
||||
|
||||
من خلال إرسال طلب سحب، فإنك توافق على أن مساهماتك سيتم ترخيصها بنفس ترخيص المشروع ([Apache 2.0](LICENSE)).
|
||||
|
||||
تذكر: المساهمة في Cline لا تقتصر فقط على كتابة الكود - إنها تتعلق بأن تكون جزءًا من مجتمع يُشكل مستقبل التطوير بمساعدة الذكاء الاصطناعي. لنبنِ شيئًا رائعًا معًا! 🚀
|
||||
@@ -0,0 +1,189 @@
|
||||
<div align="center"><sub>
|
||||
العربية | <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 – \#1 على OpenRouter
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tbody>
|
||||
<td align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>تنزيل من متجر VS</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<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/getting-started-new-coders" target="_blank"><strong>البدء</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
التقى Cline، مساعد الذكاء الاصطناعي الذي يمكنه استخدام **سطر الأوامر** و **محرر النصوص** الخاص بك.
|
||||
|
||||
بفضل [قدرات Claude 3.7 Sonnet على التعليمات البرمجية الوكيلة](https://www.anthropic.com/claude/sonnet)، يمكن لـ Cline التعامل مع مهام تطوير البرامج المعقدة خطوة بخطوة. مع الأدوات التي تسمح له بإنشاء وتعديل الملفات، واستكشاف المشاريع الكبيرة، واستخدام المتصفح، وتنفيذ أوامر الطرفية (بعد منحك الإذن)، يمكنه مساعدتك بطرق تتجاوز إكمال الكود أو الدعم الفني. يمكن لـ Cline أيضًا استخدام بروتوكول سياق النموذج (MCP) لإنشاء أدوات جديدة وتوسيع قدراته الخاصة. في حين تعمل النصوص البرمجية الآلية المستقلة تقليديًا في بيئات محاصرة، توفر هذه الإضافة واجهة رسومية لموافقة المستخدم على كل تغيير في الملف وأمر طرفية، مما يوفر طريقة آمنة وسهلة الاستخدام لاستكشاف إمكانات الذكاء الاصطناعي الوكيل.
|
||||
|
||||
1. أدخل مهمتك وأضف الصور لتحويل المحاكاة إلى تطبيقات وظيفية أو إصلاح الأخطاء مع لقطات الشاشة.
|
||||
2. يبدأ Cline بتحليل هيكل الملفات الخاصة بك وشجرة التعريف المصدرية، وإجراء عمليات بحث regex، وقراءة الملفات ذات الصلة للاطلاع على المشاريع الحالية. من خلال إدارة المعلومات التي يتم إضافتها إلى السياق بعناية، يمكن لـ Cline تقديم مساعدة قيمة حتى للمشاريع الكبيرة والمعقدة دون إرهاق نافذة السياق.
|
||||
3. بمجرد حصول Cline على المعلومات التي يحتاجها، يمكنه:
|
||||
- إنشاء وتعديل الملفات + مراقبة أخطاء Linter/Compiler أثناء السير، مما يسمح له بإصلاح المشكلات مثل الواردات المفقودة وأخطاء البناء النحوي بمفرده.
|
||||
- تنفيذ الأوامر مباشرة في الطرفية الخاصة بك ومراقبة إخراجها أثناء العمل، مما يسمح له على سبيل المثال بالاستجابة لمشكلات خادم التطوير بعد تعديل ملف.
|
||||
- بالنسبة لمهام تطوير الويب، يمكن لـ Cline إطلاق الموقع في متصفح بلا رأس، والنقر، وكتابة النص، والتمرير، والتقاط لقطات الشاشة + سجلات وحدة التحكم، مما يسمح له بإصلاح أخطاء وقت التشغيل والأخطاء البصرية.
|
||||
4. عند اكتمال المهمة، سيقدم Cline النتيجة لك مع أمر طرفية مثل `open -a "Google Chrome" index.html`، والذي تقوم بتشغيله بنقرة زر.
|
||||
|
||||
> [!TIP]
|
||||
> استخدم اختصار `CMD/CTRL + Shift + P` لفتح لوحة الأوامر واكتب "Cline: Open In New Tab" لفتح الإضافة كعلامة تبويب في محرر النصوص الخاص بك. يتيح لك هذا استخدام Cline جنبًا إلى جنب مع مستكشف الملفات الخاص بك، ورؤية كيف يغير مساحة العمل الخاصة بك بوضوح أكبر.
|
||||
|
||||
---
|
||||
|
||||
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
|
||||
|
||||
### استخدم أي واجهة برمجة تطبيقات ونموذج
|
||||
|
||||
يدعم Cline مقدمي واجهات برمجة التطبيقات مثل OpenRouter و Anthropic و OpenAI و Google Gemini و AWS Bedrock و Azure و GCP Vertex. يمكنك أيضًا تكوين أي واجهة برمجة تطبيقات متوافقة مع OpenAI، أو استخدام نموذج محلي من خلال LM Studio/Ollama. إذا كنت تستخدم OpenRouter، فستقوم الإضافة بجلب قائمة النماذج الأحدث الخاصة بهم، مما يسمح لك باستخدام أحدث النماذج بمجرد توفرها.
|
||||
|
||||
تتتبع الإضافة أيضًا إجمالي الرموز والاستخدام الخاص بواجهة برمجة التطبيقات لدورة المهمة بأكملها وطلبات فردية، مما يبقيك على اطلاع بالإنفاق في كل خطوة.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
|
||||
|
||||
### تشغيل الأوامر في الطرفية
|
||||
|
||||
بفضل [تحديثات تكامل الشل الجديدة في VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api)، يمكن لـ Cline تنفيذ الأوامر مباشرة في الطرفية الخاصة بك وتلقي الإخراج. يسمح له هذا بأداء مجموعة واسعة من المهام، من تثبيت الحزم وتشغيل سكربتات البناء إلى نشر التطبيقات، وإدارة قواعد البيانات، وتنفيذ الاختبارات، وذلك بالتكيف مع بيئة التطوير الخاصة بك وسلسلة الأدوات للقيام بالعمل على النحو الصحيح.
|
||||
|
||||
بالنسبة للعمليات الطويلة المدى مثل خوادم التطوير، استخدم زر "المتابعة أثناء التشغيل" للسماح لـ Cline بالاستمرار في المهمة بينما يعمل الأمر في الخلفية. أثناء عمل Cline، سيتم إخباره بأي إخراج طرفية جديد على الطريق، مما يسمح له بالاستجابة للمشكلات التي قد تنشأ، مثل أخطاء وقت الإنشاء عند تعديل الملفات.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
|
||||
|
||||
### إنشاء وتعديل الملفات
|
||||
|
||||
يمكن لـ Cline إنشاء وتعديل الملفات مباشرة في محرر النصوص الخاص بك، وعرض الاختلافات. يمكنك تعديل أو إلغاء تغييرات Cline مباشرة في محرر الاختلافات، أو تقديم ملاحظات في الدردشة حتى تكون راضيًا عن النتيجة. يراقب Cline أيضًا أخطاء Linter/Compiler (الواردات المفقودة، أخطاء البناء النحوي، إلخ) حتى يتمكن من إصلاح المشكلات التي تنشأ أثناء السير بمفرده.
|
||||
|
||||
يتم تسجيل جميع التغييرات التي أجراها Cline في جدول زمني للملف، مما يوفر طريقة سهلة لتتبع وإلغاء التعديلات إذا لزم الأمر.
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
|
||||
|
||||
### استخدم المتصفح
|
||||
|
||||
مع قدرة [استخدام الكمبيوتر](https://www.anthropic.com/news/3-5-models-and-computer-use) الجديدة لـ Claude 3.5 Sonnet، يمكن لـ Cline إطلاق متصفح، والنقر على العناصر، وكتابة النص، والتمرير، والتقاط لقطات الشاشة وسجلات وحدة التحكم في كل خطوة. يسمح له هذا بالتصحيح التفاعلي، واختبار نهاية إلى نهاية، وحتى الاستخدام العام للويب! يمنحه هذا الاستقلالية لإصلاح الأخطاء البصرية وأخطاء وقت التشغيل دون الحاجة إلى نسخ ولصق سجلات الأخطاء بنفسك.
|
||||
|
||||
حاول طلب من Cline "اختبار التطبيق"، وشاهده يشغل أمرًا مثل `npm run dev`، ويطلق خادم التطوير المحلي في متصفح، ويجري سلسلة من الاختبارات للتأكد من أن كل شيء يعمل. [شاهد عرضًا توضيحيًا هنا.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
<!-- Transparent pixel to create line break after floating image -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
|
||||
|
||||
### "إضافة أداة التي..."
|
||||
|
||||
شكراً لـ [بروتوكول سياق النموذج](https://github.com/modelcontextprotocol)، يمكن لـ Cline توسيع قدراته من خلال الأدوات المخصصة. بينما يمكنك استخدام [الخوادم التي أنشأها المجتمع](https://github.com/modelcontextprotocol/servers)، يمكن لـ Cline بدلاً من ذلك إنشاء أدوات وتثبيتها مصممة خصيصًا لتناسب سير عملك. ما عليك سوى أن تطلب من Cline "إضافة أداة"، وسيتولى كل شيء، من إنشاء خادم MCP جديد إلى تثبيته في الامتداد. تصبح هذه الأدوات المخصصة بعد ذلك جزءًا من مجموعة أدوات Cline، جاهزة للاستخدام في المهام المستقبلية.
|
||||
|
||||
- **"أضف أداة تجلب تذاكر Jira"**: استرجع تذاكر AC وقم بتشغيل Cline
|
||||
- **"أضف أداة تدير AWS EC2s"**: تحقق من مقاييس الخادم وقم بتوسيع أو تقليص عدد الحالات
|
||||
- **"أضف أداة تجلب أحدث حوادث PagerDuty"**: استرجع التفاصيل واطلب من Cline إصلاح الأخطاء
|
||||
|
||||
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
|
||||
|
||||
### إضافة السياق
|
||||
|
||||
**`@url`**: الصق رابط URL ليقوم الامتداد بجلبه وتحويله إلى Markdown، مفيد عندما تريد تزويد Cline بأحدث الوثائق
|
||||
|
||||
**`@problems`**: أضف أخطاء وتحذيرات بيئة العمل ('لوحة المشكلات') ليتمكن Cline من إصلاحها
|
||||
|
||||
**`@file`**: يضيف محتويات ملف حتى لا تضطر إلى إهدار طلبات API بالموافقة على قراءة الملف (+ البحث في الملفات)
|
||||
|
||||
**`@folder`**: يضيف جميع ملفات المجلد دفعة واحدة لتسريع سير العمل بشكل أكبر
|
||||
|
||||
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
|
||||
|
||||
### نقاط التحقق: المقارنة والاستعادة
|
||||
|
||||
أثناء عمل Cline على مهمة، يأخذ الامتداد لقطة من بيئة العمل في كل خطوة. يمكنك استخدام زر "Compare" لرؤية الفرق بين اللقطة وبيئة العمل الحالية، وزر "Restore" للعودة إلى تلك النقطة.
|
||||
|
||||
على سبيل المثال، عند العمل مع خادم ويب محلي، يمكنك استخدام "استعادة بيئة العمل فقط" لاختبار إصدارات مختلفة من تطبيقك بسرعة، ثم استخدام "استعادة المهمة وبيئة العمل" عندما تجد الإصدار الذي تريد المتابعة منه. يتيح لك ذلك استكشاف أساليب مختلفة بأمان دون فقدان التقدم.
|
||||
|
||||
<!-- بكسل شفاف لإنشاء فاصل سطر بعد الصورة العائمة -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## المساهمة
|
||||
|
||||
للمساهمة في المشروع، ابدأ بـ [دليل المساهمة](CONTRIBUTING.md) لتعلم الأساسيات. يمكنك أيضًا الانضمام إلى [خادم Discord](https://discord.gg/cline) للدردشة مع المساهمين الآخرين في قناة `#contributors`. إذا كنت تبحث عن عمل بدوام كامل، تحقق من الوظائف المتاحة على [صفحة التوظيف](https://cline.bot/join-us)!
|
||||
|
||||
<details>
|
||||
<summary>تعليمات التطوير المحلي</summary>
|
||||
|
||||
1. استنساخ المستودع _(يتطلب [git-lfs](https://git-lfs.com/))_:
|
||||
```bash
|
||||
git clone https://github.com/cline/cline.git
|
||||
```
|
||||
2. افتح المشروع في VSCode:
|
||||
```bash
|
||||
code cline
|
||||
```
|
||||
3. قم بتثبيت التبعيات اللازمة للامتداد وواجهة الويب:
|
||||
```bash
|
||||
npm run install:all
|
||||
```
|
||||
4. قم بالتشغيل بالضغط على `F5` (أو من `Run` -> `Start Debugging`) لفتح نافذة VSCode جديدة مع تحميل الامتداد. (قد تحتاج إلى تثبيت [إضافة esbuild problem matchers](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) إذا واجهت مشكلات في بناء المشروع.)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>إنشاء طلب سحب (Pull Request)</summary>
|
||||
|
||||
1. قبل إنشاء PR، قم بإنشاء إدخال للتغييرات:
|
||||
```bash
|
||||
npm run changeset
|
||||
```
|
||||
سيطلب منك تحديد:
|
||||
- نوع التغيير (رئيسي، ثانوي، إصلاح)
|
||||
- `رئيسي` → تغييرات غير متوافقة (1.0.0 → 2.0.0)
|
||||
- `ثانوي` → ميزات جديدة (1.0.0 → 1.1.0)
|
||||
- `إصلاح` → إصلاحات للأخطاء (1.0.0 → 1.0.1)
|
||||
- وصف التغييرات التي قمت بها
|
||||
|
||||
2. قم بحفظ التغييرات وملف `.changeset` الذي تم إنشاؤه
|
||||
|
||||
3. ادفع فرعك وأنشئ PR على GitHub. سيقوم CI بـ:
|
||||
- تشغيل الاختبارات والفحوصات
|
||||
- سيقوم Changesetbot بإنشاء تعليق يوضح تأثير الإصدار
|
||||
- عند الدمج مع الفرع الرئيسي، سيقوم Changesetbot بإنشاء PR لحزم الإصدار
|
||||
- عند دمج PR لحزم الإصدار، سيتم نشر إصدار جديد
|
||||
|
||||
</details>
|
||||
|
||||
## الرخصة
|
||||
|
||||
[Apache 2.0 © 2025 Cline Bot Inc.](./LICENSE)
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
Lernen Sie Cline kennen, einen KI-Assistenten, der Ihre **CLI** u**N**d **E**ditor nutzen kann.
|
||||
|
||||
Dank der [agentischen Codierungsfähigkeiten von Claude 3.5 Sonnet](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf) kann Cline komplexe Softwareentwicklungsaufgaben Schritt für Schritt bewältigen. Mit Werkzeugen, die ihm das Erstellen und Bearbeiten von Dateien, das Erkunden großer Projekte, die Nutzung des Browsers und das Ausführen von Terminalbefehlen (nach Ihrer Genehmigung) ermöglichen, kann er Ihnen auf eine Weise helfen, die über die Codevervollständigung oder technischen Support hinausgeht. Cline kann sogar das Model Context Protocol (MCP) verwenden, um neue Werkzeuge zu erstellen und seine eigenen Fähigkeiten zu erweitern. Während autonome KI-Skripte traditionell in sandboxed Umgebungen laufen, bietet diese Erweiterung eine Mensch-in-der-Schleife-GUI, um jede Dateiänderung und jeden Terminalbefehl zu genehmigen, was eine sichere und zugängliche Möglichkeit bietet, das Potenzial agentischer KI zu erkunden.
|
||||
Dank der [agentischen Codierungsfähigkeiten von Claude 3.7 Sonnet](https://www.anthropic.com/claude/sonnet) kann Cline komplexe Softwareentwicklungsaufgaben Schritt für Schritt bewältigen. Mit Werkzeugen, die ihm das Erstellen und Bearbeiten von Dateien, das Erkunden großer Projekte, die Nutzung des Browsers und das Ausführen von Terminalbefehlen (nach Ihrer Genehmigung) ermöglichen, kann er Ihnen auf eine Weise helfen, die über die Codevervollständigung oder technischen Support hinausgeht. Cline kann sogar das Model Context Protocol (MCP) verwenden, um neue Werkzeuge zu erstellen und seine eigenen Fähigkeiten zu erweitern. Während autonome KI-Skripte traditionell in sandboxed Umgebungen laufen, bietet diese Erweiterung eine Mensch-in-der-Schleife-GUI, um jede Dateiänderung und jeden Terminalbefehl zu genehmigen, was eine sichere und zugängliche Möglichkeit bietet, das Potenzial agentischer KI zu erkunden.
|
||||
|
||||
1. Geben Sie Ihre Aufgabe ein und fügen Sie Bilder hinzu, um Mockups in funktionale Apps zu konvertieren oder Fehler mit Screenshots zu beheben.
|
||||
2. Cline beginnt mit der Analyse Ihrer Dateistruktur und Quellcode-ASTs, führt Regex-Suchen durch und liest relevante Dateien, um sich in bestehenden Projekten zurechtzufinden. Durch sorgfältiges Management der hinzugefügten Informationen kann Cline wertvolle Unterstützung auch bei großen, komplexen Projekten bieten, ohne das Kontextfenster zu überladen.
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
Conozca a Cline, un asistente de IA que puede usar su **CLI** y **E**ditor.
|
||||
|
||||
Gracias a las [habilidades de codificación agencial de Claude 3.5 Sonnet](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf), Cline puede abordar tareas complejas de desarrollo de software paso a paso. Con herramientas que le permiten crear y editar archivos, explorar grandes proyectos, usar el navegador y ejecutar comandos de terminal (con su aprobación), puede ayudarle de una manera que va más allá de la autocompletación de código o el soporte técnico. Cline incluso puede usar el Model Context Protocol (MCP) para crear nuevas herramientas y expandir sus propias capacidades. Mientras que los scripts de IA autónomos tradicionalmente se ejecutan en entornos aislados, esta extensión ofrece una GUI con un humano en el bucle para aprobar cada cambio de archivo y comando de terminal, proporcionando una forma segura y accesible de explorar el potencial de la IA agencial.
|
||||
Gracias a las [habilidades de codificación agencial de Claude 3.7 Sonnet](https://www.anthropic.com/claude/sonnet), Cline puede abordar tareas complejas de desarrollo de software paso a paso. Con herramientas que le permiten crear y editar archivos, explorar grandes proyectos, usar el navegador y ejecutar comandos de terminal (con su aprobación), puede ayudarle de una manera que va más allá de la autocompletación de código o el soporte técnico. Cline incluso puede usar el Model Context Protocol (MCP) para crear nuevas herramientas y expandir sus propias capacidades. Mientras que los scripts de IA autónomos tradicionalmente se ejecutan en entornos aislados, esta extensión ofrece una GUI con un humano en el bucle para aprobar cada cambio de archivo y comando de terminal, proporcionando una forma segura y accesible de explorar el potencial de la IA agencial.
|
||||
|
||||
1. Ingrese su tarea y agregue imágenes para convertir maquetas en aplicaciones funcionales o solucionar errores con capturas de pantalla.
|
||||
2. Cline comenzará analizando su estructura de archivos y ASTs de código fuente, realizando búsquedas Regex y leyendo archivos relevantes para orientarse en proyectos existentes. Al gestionar cuidadosamente la información agregada, Cline puede proporcionar asistencia valiosa incluso en proyectos grandes y complejos sin sobrecargar la ventana de contexto.
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
Clineは、**CLI**と**エディター**を使用できるAIアシスタントです。
|
||||
|
||||
[Claude 3.5 Sonnetのエージェント的コーディング機能](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf)のおかげで、Clineは複雑なソフトウェア開発タスクをステップバイステップで処理できます。ファイルの作成と編集、大規模プロジェクトの探索、ブラウザの使用、ターミナルコマンドの実行(許可後)などのツールを使用して、コード補完や技術サポートを超えた支援を提供します。Clineは、Model Context Protocol (MCP)を使用して新しいツールを作成し、自身の機能を拡張することもできます。自律的なAIスクリプトは通常サンドボックス環境で実行されますが、この拡張機能はファイル変更やターミナルコマンドを承認するための人間インターフェースを提供し、エージェント的AIの可能性を安全かつアクセスしやすい方法で探求できます。
|
||||
[Claude 3.7 Sonnetのエージェント的コーディング機能](https://www.anthropic.com/claude/sonnet)のおかげで、Clineは複雑なソフトウェア開発タスクをステップバイステップで処理できます。ファイルの作成と編集、大規模プロジェクトの探索、ブラウザの使用、ターミナルコマンドの実行(許可後)などのツールを使用して、コード補完や技術サポートを超えた支援を提供します。Clineは、Model Context Protocol (MCP)を使用して新しいツールを作成し、自身の機能を拡張することもできます。自律的なAIスクリプトは通常サンドボックス環境で実行されますが、この拡張機能はファイル変更やターミナルコマンドを承認するための人間インターフェースを提供し、エージェント的AIの可能性を安全かつアクセスしやすい方法で探求できます。
|
||||
|
||||
1. タスクを入力し、モックアップを機能するアプリに変換したり、スクリーンショットでバグを修正したりします。
|
||||
2. Clineは、ファイル構造とソースコードASTの分析、正規表現検索の実行、関連ファイルの読み取りから始め、既存プロジェクトに精通します。コンテキストに追加される情報を慎重に管理することで、大規模で複雑なプロジェクトでもコンテキストウィンドウを圧倒することなく貴重な支援を提供できます。
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Código de Conduta para Contribuidores
|
||||
|
||||
## Nosso Compromisso
|
||||
|
||||
|
||||
Com o objetivo de promover um ambiente aberto e acolhedor, nós, como contribuidores e mantenedores, nos comprometemos a tornar a participação em nosso projeto e comunidade uma experiência livre de assédio para todos, independentemente de idade, tamanho corporal, deficiência, etnia, características sexuais, identidade e expressão de gênero, nível de experiência, educação, status socioeconômico, nacionalidade, aparência pessoal, raça, religião ou orientação sexual.
|
||||
|
||||
## Nossos Padrões
|
||||
|
||||
Exemplos de comportamentos que contribuem para criar um ambiente positivo incluem:
|
||||
|
||||
- Uso de linguagem acolhedora e inclusiva
|
||||
- Respeito por diferentes pontos de vista e experiências
|
||||
- Aceitar críticas de maneira construtiva
|
||||
- Foco no que é melhor para a comunidade
|
||||
- Ser empático com outros membros da comunidade
|
||||
|
||||
|
||||
Exemplos de comportamentos inaceitáveis por parte dos participantes incluem:
|
||||
|
||||
- Uso de linguagem ou imagens sexualizadas e atenção ou avanços sexuais indesejados
|
||||
- Trollar, insultar, fazer comentários depreciativos, ataques pessoais ou políticos
|
||||
- Assédio público ou privado
|
||||
- Divulgar informações privadas sem autorização, como endereços físicos ou eletrônicos, sem permissão explícita
|
||||
- Outras condutas que poderiam ser consideradas inadequadas em um ambiente profissional
|
||||
|
||||
## Nossas Responsabilidades
|
||||
|
||||
Os mantenedores do projeto são responsáveis por esclarecer os padrões de comportamento aceitáveis e devem tomar ações corretivas apropriadas e justas em resposta a qualquer instância de comportamento inaceitável.
|
||||
|
||||
Os mantenedores têm o direito e a responsabilidade de remover, editar ou rejeitar comentários, commits, códigos, edições no wiki, issues e outras contribuições que não estejam alinhadas com este Código de Conduta. Também podem banir temporária ou permanentemente qualquer colaborador cujo comportamento seja considerado inapropriado, ameaçador, ofensivo ou prejudicial.
|
||||
|
||||
## Escopo
|
||||
|
||||
Este Código de Conduta se aplica tanto aos espaços do projeto quanto aos espaços públicos
|
||||
quando uma pessoa representa o projeto ou sua comunidade. Exemplos de
|
||||
representação de um projeto ou comunidade incluem o uso de um endereço de e-mail oficial do projeto,
|
||||
publicar em uma conta oficial de mídia social ou atuar como representante designado
|
||||
em um evento online ou offline. A representação de um projeto pode
|
||||
ser mais especificamente definido e esclarecido pelos mantenedores do projeto.
|
||||
|
||||
## Aplicação
|
||||
|
||||
Casos de comportamento abusivo, assediador ou inaceitáveis podem ser reportados entrando em contato com a equipe do projeto pelo email hi@cline.bot. Todas as queixas serão revisadas e investigadas confidencialmente. Mais detalhes sobre políticas específicas podem ser publicados separadamente.
|
||||
|
||||
Os mantenedores que não seguirem ou aplicarem este Código de Conduta de boa fé podem enfrentar repercussões temporárias ou permanentes determinadas por outros membros da liderança do projeto.
|
||||
|
||||
## Atribuição
|
||||
|
||||
Este Código de Conduta é adaptado do [Contributor Covenant](https://www.contributor-covenant.org), versão 1.4, disponível em https://www.contributor-covenant.org/version/1/4/code-of-conduct.html.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Contribuir para o Cline
|
||||
|
||||
Estamos felizes por você estar interessado em contribuir com o Cline. Seja corrigindo um erro, adicionando uma funcionalidade ou melhorando nossa documentação, cada contribuição torna o Cline mais inteligente! Para manter nossa comunidade viva e acolhedora, todos os membros devem cumprir nosso Código de Conduta [Código de Conduta](CODE_OF_CONDUCT.md).
|
||||
|
||||
## Relatar erros ou problemas
|
||||
|
||||
Relatar erros ajuda a melhorar o Cline para todos! Antes de criar um novo issue, revise as [issues existentes](https://github.com/cline/cline/issues) para evitar duplicações. Quando estiver pronto para relatar um erro, vá até nossa [página de Issues](https://github.com/cline/cline/issues/new/choose), onde você encontrará um modelo que ajudará a preencher as informações relevantes.
|
||||
|
||||
<blockquote class='warning-note'>
|
||||
🔐 <b>Importante:</b> Se você descobrir uma vulnerabilidade de segurança, utilize a <a href="https://github.com/cline/cline/security/advisories/new">ferramenta de segurança do GitHub</a> para relatá-la de forma privada.
|
||||
</blockquote>
|
||||
|
||||
## Escolher no que trabalhar
|
||||
|
||||
Procurando uma boa primeira contribuição? Consulte os problemas marcados com ["good first issue"](https://github.com/cline/cline/labels/good%20first%20issue) ou ["help wanted"](https://github.com/cline/cline/labels/help%20wanted). Estes foram especialmente selecionados para novos colaboradores e são áreas em que adoraríamos receber ajuda!
|
||||
|
||||
Também damos boas-vindas a contribuições para nossa [documentação](https://github.com/cline/cline/tree/main/docs). Seja corrigindo erros de digitação, melhorando guias existentes ou criando novos conteúdos educativos, queremos construir um repositório de recursos gerido pela comunidade que ajude todos a tirar o máximo proveito do Cline. Você pode começar explorando `/docs` e procurando áreas que precisam de melhorias.
|
||||
|
||||
Se planeja trabalhar em uma funcionalidade maior, crie primeiro uma [solicitação de funcionalidade](https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop) para que possamos discutir se ela se alinha à visão do Cline.
|
||||
|
||||
## Configurar o ambiente de desenvolvimento
|
||||
|
||||
1. **Extensões do VS Code**
|
||||
|
||||
- Ao abrir o projeto, o VS Code solicitará que você instale as extensões recomendadas.
|
||||
- Essas extensões são necessárias para o desenvolvimento – aceite todas as solicitações de instalação.
|
||||
- Caso tenha rejeitado as solicitações, você pode instalá-las manualmente na seção de extensões.
|
||||
|
||||
2. **Desenvolvimento local**
|
||||
- Execute `npm run install:all` para instalar as dependências.
|
||||
- Execute `npm run test` para rodar os testes localmente.
|
||||
- Antes de enviar um PR, execute `npm run format:fix` para formatar seu código.
|
||||
|
||||
## Escrever e enviar código
|
||||
|
||||
Qualquer pessoa pode contribuir com código para o Cline, mas pedimos que siga estas diretrizes para garantir que suas contribuições sejam integradas sem problemas:
|
||||
|
||||
1. **Mantenha os Pull Requests focados**
|
||||
|
||||
- Limite os PRs a uma única funcionalidade ou correção de erro.
|
||||
- Divida alterações maiores em PRs menores e coerentes.
|
||||
- Divida as alterações em commits lógicos que possam ser revisados independentemente.
|
||||
|
||||
2. **Qualidade do código**
|
||||
|
||||
- Execute `npm run lint` para verificar o estilo do código.
|
||||
- Execute `npm run format` para formatar automaticamente o código.
|
||||
- Todos os PRs devem passar nas verificações do CI, que incluem linting e formatação.
|
||||
- Resolva todos os avisos ou erros do ESLint antes de enviar.
|
||||
- Siga as melhores práticas para TypeScript e mantenha a segurança dos tipos.
|
||||
|
||||
3. **Testes**
|
||||
|
||||
- Adicione testes para novas funcionalidades.
|
||||
- Execute `npm test` para garantir que todos os testes passem.
|
||||
- Atualize testes existentes caso suas alterações os afetem.
|
||||
- Inclua tanto testes unitários quanto de integração onde for apropriado.
|
||||
|
||||
4. **Diretrizes de commits**
|
||||
|
||||
- Escreva mensagens de commit claras e descritivas.
|
||||
- Use o formato convencional (por exemplo, "feat:", "fix:", "docs:").
|
||||
- Faça referência aos issues relevantes nos commits usando #número-do-issue.
|
||||
|
||||
5. **Antes de enviar**
|
||||
|
||||
- Faça rebase com sua branch com a última versão da branch principal (main).
|
||||
- Certifique-se de que sua branch seja construída corretamente.
|
||||
- Verifique se todos os testes passam.
|
||||
- Revise suas alterações para remover qualquer código de depuração ou logs desnecessários.
|
||||
|
||||
6. **Descrição do Pull Request**
|
||||
- Descreva claramente o que suas alterações fazem.
|
||||
- Inclua passos para testar as alterações.
|
||||
- Liste quaisquer mudanças importantes.
|
||||
- Adicione capturas de tela para mudanças na interface do usuário.
|
||||
|
||||
## Acordo de contribuição
|
||||
|
||||
Ao enviar um Pull Request, você concorda que suas contribuições serão licenciadas sob a mesma licença do projeto ([Apache 2.0](LICENSE)).
|
||||
|
||||
Lembre-se: Contribuir com o Cline não é apenas escrever código – é fazer parte de uma comunidade que está moldando o futuro do desenvolvimento assistido por IA. Vamos criar algo incrível juntos! 🚀
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# Cline – #1 no OpenRouter
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tbody>
|
||||
<td align="center">
|
||||
<a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank"><strong>Baixar no VS Marketplace</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://discord.gg/cline" target="_blank"><strong>Discord</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://www.reddit.com/r/cline/" target="_blank"><strong>r/cline</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<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>Solicitação de Funcionalidades</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://cline.bot/join-us" target="_blank"><strong>Estamos Contratando!</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Conheça o Cline: um assistente de IA que pode usar seu **CLI** e **Editor**.
|
||||
|
||||
Graças às [habilidades avançadas do Claude 3.7 Sonnet](https://www.anthropic.com/claude/sonnet), o Cline pode lidar com tarefas complexas de desenvolvimento de software passo a passo. Com ferramentas que permitem criar e editar arquivos, explorar grandes projetos, usar o navegador e executar comandos no terminal (com sua aprovação), ele pode ajudar você de maneiras que vão além da inclusão de código ou suporte técnico. O Cline pode é capaz inclusive de usar o Model Context Protocol (MCP) para criar novas ferramentas e expandir seus próprios recursos. Embora os scripts de IA autônomas tradicionalmente sejam executados em ambientes isolados, esta extensão oferece uma GUI com um humano no circuito para aprovar cada alteração de arquivo e comando de terminal, fornecendo uma maneira segura e acessível de explorar todo o potencial da IA.
|
||||
|
||||
1. Insira sua tarefa e adicione imagens para transformar mockups em aplicativos funcionais ou corrigir erros através de capturas de tela.
|
||||
|
||||
2. O Cline começará analisando a estrutura do seu arquivo e os ASTs do código-fonte, fazendo pesquisas com Regex e lendo arquivos relevantes para se orientar em projetos existentes. Ao gerenciar cuidadosamente as informações agregadas, o Cline pode fornecer assistência valiosa mesmo em projetos grandes e complexos, sem sobrecarregar a janela de contexto.
|
||||
3. Assim que ele tiver as informações necessárias, o Cline poderá:
|
||||
- Criar e editar arquivos + monitorar erros de Linter/Compilador, para que você possa corrigir proativamente problemas como importações ausentes e erros de sintaxe.
|
||||
- Executar comandos diretamente no terminal e monitorar o resultado, para que você possa responder a problemas do servidor de desenvolvimento após editar um arquivo.
|
||||
- Para tarefas de desenvolvimento web, o Cline pode iniciar o site em um navegador headless, clicar, digitar, fazer scroll e capturar capturas de tela + registros de console, para que você possa corrigir erros em tempo de execução e erros visuais.
|
||||
|
||||
> [!TIP]
|
||||
> Use o atalho de teclado `CMD/CTRL + Shift + P` para abrir a lista de comandos possiveis e digite "Cline: Abrir em nova aba" para abrir a extensão como uma aba no seu editor. Dessa forma, você pode usar o Cline junto com seu explorador de arquivos e ver mais claramente como seu espaço de trabalho muda.
|
||||
|
||||
---
|
||||
|
||||
<img align="right" width="340" src="https://github.com/user-attachments/assets/3cf21e04-7ce9-4d22-a7b9-ba2c595e88a4">
|
||||
|
||||
### Use qualquer API ou modelo
|
||||
|
||||
O Cline oferece suporte a provedores de API como OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure e GCP Vertex. Você também pode configurar qualquer API compatível com OpenAI ou usar um modelo local via LM Studio/Ollama. Se você usar o OpenRouter, a extensão recuperará sua lista de modelos mais recentes, para que você possa usar os modelos mais novos assim que estiverem disponíveis.
|
||||
|
||||
A extensão também rastreia o uso total de tokens e os custos da API para todo o ciclo de tarefas e solicitações individuais, para que você seja informado sobre as despesas em cada etapa.
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/81be79a8-1fdb-4028-9129-5fe055e01e76">
|
||||
|
||||
### Executar comandos no terminal
|
||||
|
||||
Graças às novas [atualizações de integração do Shell no VSCode v1.93](https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api), o Cline pode executar comandos diretamente no seu terminal e receber o resultado. Isso permite que você execute uma variedade de tarefas, desde instalar pacotes e executar build scripts para fazer deploy de aplicações, gerenciar bancos de dados e executar testes, adaptando-se ao seu ambiente de desenvolvimento e ferramentas para fazer o trabalho corretamente.
|
||||
|
||||
Para processos de longa duração, como servidores de desenvolvimento, use o botão "Continuar durante a execução" para permitir que o Cline continue a tarefa enquanto o comando é executado em segundo plano. Enquanto Cline trabalha, você será notificado sobre novas saídas do terminal, para que possa responder a problemas que possam surgir, como erros de compilação ao editar arquivos.
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="400" src="https://github.com/user-attachments/assets/c5977833-d9b8-491e-90f9-05f9cd38c588">
|
||||
|
||||
### Criar e editar arquivos
|
||||
|
||||
Cline pode criar e editar arquivos diretamente no seu editor, apresentando um diff com as alterações. Você pode editar ou reverter as alterações do Cline diretamente no editor de diff ou fornecer feedback no chat até ficar satisfeito com o resultado. Cline também monitora erros de linter/compilador (importações ausentes, erros de sintaxe, etc.) para que possa corrigir problemas que surgem ao longo do caminho por conta própria.
|
||||
|
||||
Todas as alterações feitas pelo Cline são registradas na Linha do tempo do arquivo, fornecendo uma maneira fácil de rastrear e reverter modificações, caso seja necessário.
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="370" src="https://github.com/user-attachments/assets/bc2e85ba-dfeb-4fe6-9942-7cfc4703cbe5">
|
||||
|
||||
### Uso do navegador
|
||||
|
||||
Com a nova habilidade de [uso de computador](https://www.anthropic.com/news/3-5-models-and-computer-use) do Claude Sonnet 3.5, Cline pode abrir um navegador, clicar em elementos, digitar texto e rolar, capturando a tela e logs de console. Isso permite depurar de maneira interativa, testes end-to-end e até mesmo uso geral da web. Isso lhe dá autonomia para solucionar erros visuais e problemas em tempo de execução sem precisar copiar e colar logs dos erros.
|
||||
|
||||
Tente pedir a Cline para "testar o aplicativo" e observe enquanto o Cline executa um comando como `npm run dev`, inicia seu servidor de desenvolvimento local em um navegador e executa uma série de testes para confirmar se tudo funciona. [Veja uma demonstração aqui.](https://x.com/sdrzn/status/1850880547825823989)
|
||||
|
||||
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/ac0efa14-5c1f-4c26-a42d-9d7c56f5fadd">
|
||||
|
||||
### "adicione uma ferramenta que..."
|
||||
|
||||
Graças ao [Model Context Protocol](https://github.com/modelcontextprotocol), o Cline pode expandir seus recursos por meio de ferramentas personalizadas. Embora você possa usar [servidores criados pela comunidade](https://github.com/modelcontextprotocol/servers), Cline pode criar e instalar ferramentas especificamente para seu fluxo de trabalho. Basta pedir ao Cline para "adicionar uma ferramenta" e ele cuidará de tudo, desde a criação de um novo servidor MCP até a instalação na extensão. Essas ferramentas personalizadas se tornam parte do conjunto de ferramentas da Cline e estão prontas para serem usadas em tarefas futuras.
|
||||
|
||||
- "adicione uma ferramenta que recupere tickets do Jira": Recupere ACs de tickets e coloque Cline para trabalhar
|
||||
- "adicione uma ferramenta que gerencie AWS EC2s": verifique as métricas do servidor e aumente ou diminua as instâncias
|
||||
- "adicione uma ferramenta para recuperar os últimos incidentes do PagerDuty": Recupere detalhes e peça ao Cline para corrigir erros
|
||||
|
||||
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="left" width="360" src="https://github.com/user-attachments/assets/7fdf41e6-281a-4b4b-ac19-020b838b6970">
|
||||
|
||||
### Adicione contexto
|
||||
|
||||
**`@url`:** Insira uma URL para a extensão recuperar e converter para Markdown, que é útil quando você deseja fornecer ao Cline documentos mais recentes
|
||||
|
||||
**`@problems`:** Adicionar erros e avisos do espaço de trabalho (painel 'Problemas') que o Cline deve corrigir
|
||||
|
||||
**`@file`:** Adicione o conteúdo de um arquivo para que você não precise desperdiçar solicitações de API para aprovar a leitura do arquivo (+ para pesquisar arquivos)
|
||||
|
||||
**`@folder`:** Adicione arquivos de uma pasta por vez para acelerar ainda mais seu fluxo de trabalho
|
||||
|
||||
<!-- Pixel transparente para criar uma quebra de linha após a imagem flutuante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
<img align="right" width="350" src="https://github.com/user-attachments/assets/140c8606-d3bf-41b9-9a1f-4dbf0d4c90cb">
|
||||
|
||||
### Checkpoints: Comparar e Restaurar
|
||||
|
||||
Enquanto Cline trabalha em uma tarefa, a extensão cria um instantâneo de seu espaço de trabalho em cada etapa. Você pode usar o botão "Comparar" para ver a diferença entre o instantâneo e seu espaço de trabalho atual, e o botão "Restaurar" para retornar a esse ponto.
|
||||
|
||||
Por exemplo, se estiver trabalhando com um servidor web local, você pode usar 'Restaurar somente o espaço de trabalho' para testar rapidamente diferentes versões do seu aplicativo e, em seguida, 'Restaurar tarefa e espaço de trabalho' quando encontrar a versão na qual deseja continuar trabalhando. Isso permite que você explore diferentes abordagens com segurança sem perder o progresso.
|
||||
|
||||
<!-- Pixel transparente para crear un salto de línea después de la imagen flotante -->
|
||||
|
||||
<img width="2000" height="0" src="https://github.com/user-attachments/assets/ee14e6f7-20b8-4391-9091-8e8e25561929"><br>
|
||||
|
||||
## Contribuições
|
||||
|
||||
Para contribuir com o projeto, comece com nosso [Guia de Contribuição](CONTRIBUTING.md) para aprender o básico. Você também pode entrar no nosso [Discord](https://discord.gg/cline) para bater papo com outros colaboradores no canal `#contributors`. Se você está procurando um emprego de período integral, confira nossas vagas em aberto na nossa [página de carreiras](https://cline.bot/join-us).
|
||||
|
||||
<details>
|
||||
<summary>Instruções para desenvolvimento local</summary>
|
||||
|
||||
1. Clone o repositório _(Necessário [git-lfs](https://git-lfs.com/))_:
|
||||
```bash
|
||||
git clone https://github.com/cline/cline.git
|
||||
```
|
||||
2. Abra o projeto no VSCode:
|
||||
```bash
|
||||
code cline
|
||||
```
|
||||
3. Instale as dependências necessárias para a extensão e webview-gui:
|
||||
```bash
|
||||
npm run install:all
|
||||
```
|
||||
4. Inicie pressionando `F5` (ou `Executar`->`Iniciar Depuração`) para abrir uma nova janela do VSCode com a extensão carregada. (Pode ser necessário instalar a [extensão esbuild problem matchers](https://marketplace.visualstudio.com/items?itemName=connor4312.esbuild-problem-matchers) se você encontrar problemas ao compilar seu projeto.)
|
||||
|
||||
</details>
|
||||
|
||||
## Licença
|
||||
|
||||
[Apache 2.0 © 2024 Cline Bot Inc.](./LICENSE)
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
认识 Cline,一个可以使用你的 **CLI** 和 **编辑器** 的 AI 助手。
|
||||
|
||||
感谢 [Claude 3.5 Sonnet 的代理编码能力](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf),Cline 可以一步步处理复杂的软件开发任务。通过允许他创建和编辑文件、探索大型项目、使用浏览器和执行终端命令(在你授予权限后),他可以提供超越代码完成或技术支持的帮助。Cline 甚至可以使用 Model Context Protocol (MCP) 创建新工具并扩展自己的能力。虽然自主 AI 脚本传统上在沙盒环境中运行,但此扩展提供了一个人机交互的 GUI 来批准每个文件更改和终端命令,提供了一种安全且可访问的方式来探索代理 AI 的潜力。
|
||||
感谢 [Claude 3.7 Sonnet 的代理编码能力](https://www.anthropic.com/claude/sonnet),Cline 可以一步步处理复杂的软件开发任务。通过允许他创建和编辑文件、探索大型项目、使用浏览器和执行终端命令(在你授予权限后),他可以提供超越代码完成或技术支持的帮助。Cline 甚至可以使用 Model Context Protocol (MCP) 创建新工具并扩展自己的能力。虽然自主 AI 脚本传统上在沙盒环境中运行,但此扩展提供了一个人机交互的 GUI 来批准每个文件更改和终端命令,提供了一种安全且可访问的方式来探索代理 AI 的潜力。
|
||||
|
||||
1. 输入你的任务并添加图像,将模型转换为功能应用程序或通过截图修复错误。
|
||||
2. Cline 首先分析你的文件结构和源代码 AST,运行正则表达式搜索,并阅读相关文件以了解现有项目。通过仔细管理添加到上下文中的信息,Cline 即使在大型复杂项目中也能提供有价值的帮助,而不会使上下文窗口过载。
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
認識 Cline,一個可以使用你的 **CLI** 和 **編輯器** 的 AI 助手。
|
||||
|
||||
感謝 [Claude 3.5 Sonnet 的代理編碼能力](https://www-cdn.anthropic.com/fed9cc193a14b84131812372d8d5857f8f304c52/Model_Card_Claude_3_Addendum.pdf),Cline 可以一步步處理複雜的軟件開發任務。通過允許他創建和編輯文件、探索大型項目、使用瀏覽器和執行終端命令(在你授予權限後),他可以提供超越代碼完成或技術支持的幫助。Cline 甚至可以使用 Model Context Protocol (MCP) 創建新工具並擴展自己的能力。雖然自主 AI 腳本傳統上在沙盒環境中運行,但此擴展提供了一個人機交互的 GUI 來批准每個文件更改和終端命令,提供了一種安全且可訪問的方式來探索代理 AI 的潛力。
|
||||
感謝 [Claude 3.7 Sonnet 的代理編碼能力](https://www.anthropic.com/claude/sonnet),Cline 可以一步步處理複雜的軟件開發任務。通過允許他創建和編輯文件、探索大型項目、使用瀏覽器和執行終端命令(在你授予權限後),他可以提供超越代碼完成或技術支持的幫助。Cline 甚至可以使用 Model Context Protocol (MCP) 創建新工具並擴展自己的能力。雖然自主 AI 腳本傳統上在沙盒環境中運行,但此擴展提供了一個人機交互的 GUI 來批准每個文件更改和終端命令,提供了一種安全且可訪問的方式來探索代理 AI 的潛力。
|
||||
|
||||
1. 輸入你的任務並添加圖像,將模型轉換為功能應用程序或通過截圖修復錯誤。
|
||||
2. Cline 首先分析你的文件結構和源代碼 AST,運行正則表達式搜索,並閱讀相關文件以了解現有項目。通過仔細管理添加到上下文中的信息,Cline 即使在大型複雜項目中也能提供有價值的幫助,而不會使上下文窗口過載。
|
||||
|
||||
Generated
+581
-59
@@ -1,19 +1,20 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.2.13",
|
||||
"version": "3.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.2.13",
|
||||
"version": "3.5.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||
"@anthropic-ai/sdk": "^0.26.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.4.1",
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/generative-ai": "^0.18.0",
|
||||
"@mistralai/mistralai": "^1.3.6",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/get-folder-size": "^3.0.4",
|
||||
@@ -36,10 +37,13 @@
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"ollama": "^0.5.13",
|
||||
"open-graph-scraper": "^6.9.0",
|
||||
"openai": "^4.83.0",
|
||||
"os-name": "^6.0.0",
|
||||
"p-wait-for": "^5.0.2",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"posthog-node": "^4.8.1",
|
||||
"puppeteer-chromium-resolver": "^23.0.0",
|
||||
"puppeteer-core": "^23.4.0",
|
||||
"serialize-error": "^11.0.3",
|
||||
@@ -48,7 +52,7 @@
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.12",
|
||||
@@ -63,7 +67,7 @@
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"chai": "^4.3.10",
|
||||
"esbuild": "^0.21.5",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^8.57.0",
|
||||
"husky": "^9.1.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -76,11 +80,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/bedrock-sdk": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/bedrock-sdk/-/bedrock-sdk-0.10.2.tgz",
|
||||
"integrity": "sha512-sGmTzKJQHVwfXexe+yfzPU3rJmUMCygC+GNPkmMsPX/Jr+WKtJ0M71nGyHONr6vcHwUpUWA6o0MRH/oHaE54KA==",
|
||||
"version": "0.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/bedrock-sdk/-/bedrock-sdk-0.12.4.tgz",
|
||||
"integrity": "sha512-kraOgWWyVO/Wef3wYbpws77pCZubg/hCzXQ7RGrLRJsRpbTIT+ms+MigGu/0b1qn3o5WuIrumlT3Qtu3esHpzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0",
|
||||
"@anthropic-ai/sdk": ">=0.36 <1",
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "^3.423.0",
|
||||
"@aws-sdk/credential-providers": "^3.341.0",
|
||||
@@ -94,9 +99,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.26.0.tgz",
|
||||
"integrity": "sha512-vNbZ2rnnMfk8Bf4OdeVy6GA4EXao8tGC0tLEoSAl1NZrip9oOxnEGUkXl3FsPQgeBM5hmpGE1tSLuu9HEVJiHg==",
|
||||
"version": "0.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.37.0.tgz",
|
||||
"integrity": "sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
@@ -108,23 +114,48 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk/node_modules/@types/node": {
|
||||
"version": "18.19.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz",
|
||||
"integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==",
|
||||
"version": "18.19.78",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.78.tgz",
|
||||
"integrity": "sha512-m1ilZCTwKLkk9rruBJXFeYN0Bc5SbjirwYX/Td3MqPfioYbgun3IvK/m8dQxMCnrPGZPg1kvXjp3SIekCN/ynw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/vertex-sdk": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.4.1.tgz",
|
||||
"integrity": "sha512-RT/2CWzqyAcJDZWxnNc1mXa7XiiHDaQ9aknfW4mIDw6zE+Zj/R2vCKpTb0dIwrmHYNOyKQNaD7Z1ynDt9oXFWA==",
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/vertex-sdk/-/vertex-sdk-0.6.4.tgz",
|
||||
"integrity": "sha512-rMBlO2jF53TfMRmsQMm1bPO2JRUh4jYddjq/OJLj8DSAkfbCrNWhc0yhDed6oLYJg5s+VpDbvlPzMggqHhTfMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": ">=0.14 <1",
|
||||
"@anthropic-ai/sdk": ">=0.35 <1",
|
||||
"google-auth-library": "^9.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/vertex-sdk/node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.38.0.tgz",
|
||||
"integrity": "sha512-ZUYjadEEeb1wwKd9MEM+plSfl7zQjYixhjHRtyPsjO7MtzRmbZvBb1n1ofo2kHwb0aUiIdb3aAEbAwS9Bcbm/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/vertex-sdk/node_modules/@types/node": {
|
||||
"version": "18.19.76",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz",
|
||||
"integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/crc32": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
|
||||
@@ -2533,10 +2564,78 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2547,7 +2646,347 @@
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
|
||||
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
|
||||
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
|
||||
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
|
||||
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
@@ -3350,6 +3789,18 @@
|
||||
"resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz",
|
||||
"integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ=="
|
||||
},
|
||||
"node_modules/@google-cloud/vertexai": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-1.9.3.tgz",
|
||||
"integrity": "sha512-35o5tIEMLW3JeFJOaaMNR2e5sq+6rpnhrF97PuAxeOm0GlqVTESKhkGj7a5B5mmJSSSU3hUfIhcQCRRsw4Ipzg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"google-auth-library": "^9.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google/generative-ai": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.18.0.tgz",
|
||||
@@ -3690,13 +4141,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mistralai/mistralai": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.4.0.tgz",
|
||||
"integrity": "sha512-xA3DAtIDh4Qgr1EoSuiGVE+2ABNrxpcTeC0kSXYbkDNUGdthalLAH7DgbG0fkKZ7TN8xdWXQq2WiIghp/O96Eg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.0.tgz",
|
||||
"integrity": "sha512-AIn8pwAwA/fDvEUvmkt+40zH1ZmfaG3Q7oUWl17GUEC1tU7ZPwYz8Cv9P59lyS1SisHdDSu81oknO7f1ywkz8Q==",
|
||||
"dependencies": {
|
||||
"zod-to-json-schema": "^3.24.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": ">= 3"
|
||||
}
|
||||
},
|
||||
"node_modules/@mistralai/mistralai/node_modules/zod-to-json-schema": {
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz",
|
||||
"integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@mixmark-io/domino": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
||||
@@ -6650,6 +7113,15 @@
|
||||
"devtools-protocol": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi/node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
|
||||
@@ -7472,9 +7944,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -7482,32 +7954,34 @@
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
"@esbuild/aix-ppc64": "0.25.0",
|
||||
"@esbuild/android-arm": "0.25.0",
|
||||
"@esbuild/android-arm64": "0.25.0",
|
||||
"@esbuild/android-x64": "0.25.0",
|
||||
"@esbuild/darwin-arm64": "0.25.0",
|
||||
"@esbuild/darwin-x64": "0.25.0",
|
||||
"@esbuild/freebsd-arm64": "0.25.0",
|
||||
"@esbuild/freebsd-x64": "0.25.0",
|
||||
"@esbuild/linux-arm": "0.25.0",
|
||||
"@esbuild/linux-arm64": "0.25.0",
|
||||
"@esbuild/linux-ia32": "0.25.0",
|
||||
"@esbuild/linux-loong64": "0.25.0",
|
||||
"@esbuild/linux-mips64el": "0.25.0",
|
||||
"@esbuild/linux-ppc64": "0.25.0",
|
||||
"@esbuild/linux-riscv64": "0.25.0",
|
||||
"@esbuild/linux-s390x": "0.25.0",
|
||||
"@esbuild/linux-x64": "0.25.0",
|
||||
"@esbuild/netbsd-arm64": "0.25.0",
|
||||
"@esbuild/netbsd-x64": "0.25.0",
|
||||
"@esbuild/openbsd-arm64": "0.25.0",
|
||||
"@esbuild/openbsd-x64": "0.25.0",
|
||||
"@esbuild/sunos-x64": "0.25.0",
|
||||
"@esbuild/win32-arm64": "0.25.0",
|
||||
"@esbuild/win32-ia32": "0.25.0",
|
||||
"@esbuild/win32-x64": "0.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@@ -10591,6 +11065,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ollama": {
|
||||
"version": "0.5.13",
|
||||
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.13.tgz",
|
||||
"integrity": "sha512-qK3eE2GjMYjCiTknEJfAHjbUzUqgVtf9qtzjxWrkwBZgBG7kOB6Z4+Ov4fbvDjmKKHv+rpuTsWFg4jZvVjNBtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-fetch": "^3.6.20"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -10616,6 +11099,27 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/open-graph-scraper": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/open-graph-scraper/-/open-graph-scraper-6.9.0.tgz",
|
||||
"integrity": "sha512-1KoV5v6GT0/MqlryrVGQROhEAD4u8wC3VjYOxsnhj3mWeGJ6N6nF/rbrcZREFr+kiYm9I5LMrzdK9t9hBMbL2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chardet": "^2.0.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"undici": "^6.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/open-graph-scraper/node_modules/chardet": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz",
|
||||
"integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.83.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.83.0.tgz",
|
||||
@@ -11175,6 +11679,18 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-node": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.8.1.tgz",
|
||||
"integrity": "sha512-ApMEC1+DbctP/88+VhaCl8SRKpIoReibMf7Mb3rxw3yMthr1rKaM4opbHdZJ0buLhwS5zX8B2ckqLjpwpSjRPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -12908,6 +13424,12 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-fetch": {
|
||||
"version": "3.6.20",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
|
||||
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
@@ -13388,9 +13910,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"version": "3.24.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
+54
-10
@@ -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.3.0",
|
||||
"version": "3.5.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"galleryBanner": {
|
||||
"color": "#617A91",
|
||||
@@ -73,7 +73,7 @@
|
||||
{
|
||||
"command": "cline.mcpButtonClicked",
|
||||
"title": "MCP Servers",
|
||||
"icon": "$(server)"
|
||||
"icon": "$(extensions)"
|
||||
},
|
||||
{
|
||||
"command": "cline.historyButtonClicked",
|
||||
@@ -162,6 +162,11 @@
|
||||
"default": true,
|
||||
"description": "Enables extension to save checkpoints of workspace throughout the task."
|
||||
},
|
||||
"cline.disableBrowserTool": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Disables extension from spawning browser session."
|
||||
},
|
||||
"cline.modelSettings.o3Mini.reasoningEffort": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -171,6 +176,41 @@
|
||||
],
|
||||
"default": "medium",
|
||||
"description": "Controls the reasoning effort when using the o3-mini model. Higher values may result in more thorough but slower responses."
|
||||
},
|
||||
"cline.chromeExecutablePath": {
|
||||
"type": "string",
|
||||
"default": null,
|
||||
"description": "Path to Chrome executable for browser use functionality. If not set, the extension will attempt to find or download it automatically."
|
||||
},
|
||||
"cline.preferredLanguage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"English",
|
||||
"Arabic - العربية",
|
||||
"Portuguese - Português (Brasil)",
|
||||
"Czech - Čeština",
|
||||
"French - Français",
|
||||
"German - Deutsch",
|
||||
"Hindi - हिन्दी",
|
||||
"Hungarian - Magyar",
|
||||
"Italian - Italiano",
|
||||
"Japanese - 日本語",
|
||||
"Korean - 한국어",
|
||||
"Polish - Polski",
|
||||
"Portuguese - Português (Portugal)",
|
||||
"Russian - Русский",
|
||||
"Simplified Chinese - 简体中文",
|
||||
"Spanish - Español",
|
||||
"Traditional Chinese - 繁體中文",
|
||||
"Turkish - Türkçe"
|
||||
],
|
||||
"default": "English",
|
||||
"description": "The language that Cline should use for communication."
|
||||
},
|
||||
"cline.mcpMarketplace.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Controls whether the MCP Marketplace is enabled."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,12 +226,12 @@
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
"check-types": "tsc --noEmit",
|
||||
"lint": "eslint src --ext ts",
|
||||
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts",
|
||||
"format": "prettier . --check",
|
||||
"format:fix": "prettier . --write",
|
||||
"test": "vscode-test",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"start:webview": "cd webview-ui && npm run start",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
"test:webview": "cd webview-ui && npm run test",
|
||||
"publish:marketplace": "vsce publish && ovsx publish",
|
||||
@@ -213,7 +253,7 @@
|
||||
"@vscode/test-cli": "^0.0.9",
|
||||
"@vscode/test-electron": "^2.4.0",
|
||||
"chai": "^4.3.10",
|
||||
"esbuild": "^0.21.5",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^8.57.0",
|
||||
"husky": "^9.1.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -222,11 +262,12 @@
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||
"@anthropic-ai/sdk": "^0.26.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.4.1",
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.6.4",
|
||||
"@google-cloud/vertexai": "^1.9.3",
|
||||
"@google/generative-ai": "^0.18.0",
|
||||
"@mistralai/mistralai": "^1.3.6",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.0.1",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/get-folder-size": "^3.0.4",
|
||||
@@ -249,10 +290,13 @@
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"ollama": "^0.5.13",
|
||||
"open-graph-scraper": "^6.9.0",
|
||||
"openai": "^4.83.0",
|
||||
"os-name": "^6.0.0",
|
||||
"p-wait-for": "^5.0.2",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"posthog-node": "^4.8.1",
|
||||
"puppeteer-chromium-resolver": "^23.0.0",
|
||||
"puppeteer-core": "^23.4.0",
|
||||
"serialize-error": "^11.0.3",
|
||||
@@ -261,6 +305,6 @@
|
||||
"tree-sitter-wasms": "^0.1.11",
|
||||
"turndown": "^7.2.0",
|
||||
"web-tree-sitter": "^0.22.6",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import { QwenHandler } from "./providers/qwen"
|
||||
import { MistralHandler } from "./providers/mistral"
|
||||
import { VsCodeLmHandler } from "./providers/vscode-lm"
|
||||
import { LiteLlmHandler } from "./providers/litellm"
|
||||
import { AskSageHandler } from "./providers/asksage"
|
||||
import { XAIHandler } from "./providers/xai"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -62,6 +64,10 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new VsCodeLmHandler(options)
|
||||
case "litellm":
|
||||
return new LiteLlmHandler(options)
|
||||
case "asksage":
|
||||
return new AskSageHandler(options)
|
||||
case "xai":
|
||||
return new XAIHandler(options)
|
||||
default:
|
||||
return new AnthropicHandler(options)
|
||||
}
|
||||
|
||||
@@ -20,10 +20,15 @@ export class AnthropicHandler implements ApiHandler {
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
let stream: AnthropicStream<Anthropic.Beta.PromptCaching.Messages.RawPromptCachingBetaMessageStreamEvent>
|
||||
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
|
||||
const modelId = model.id
|
||||
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
|
||||
switch (modelId) {
|
||||
// 'latest' alias does not support cache_control
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
case "claude-3-opus-20240229":
|
||||
@@ -37,11 +42,14 @@ export class AnthropicHandler implements ApiHandler {
|
||||
)
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
stream = await this.client.beta.promptCaching.messages.create(
|
||||
stream = await this.client.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
// "Thinking isn’t compatible with temperature, top_p, or top_k modifications as well as forced tool use."
|
||||
// (https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking)
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
@@ -88,6 +96,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
// https://github.com/anthropics/anthropic-sdk-typescript?tab=readme-ov-file#default-headers
|
||||
// https://github.com/anthropics/anthropic-sdk-typescript/commit/c920b77fc67bd839bfeb6716ceab9d7c9bbe7393
|
||||
switch (modelId) {
|
||||
case "claude-3-7-sonnet-20250219":
|
||||
case "claude-3-5-sonnet-20241022":
|
||||
case "claude-3-5-haiku-20241022":
|
||||
case "claude-3-opus-20240229":
|
||||
@@ -146,6 +155,20 @@ export class AnthropicHandler implements ApiHandler {
|
||||
break
|
||||
case "content_block_start":
|
||||
switch (chunk.content_block.type) {
|
||||
case "thinking":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: chunk.content_block.thinking || "",
|
||||
}
|
||||
break
|
||||
case "redacted_thinking":
|
||||
// Handle redacted thinking blocks - we still mark it as reasoning
|
||||
// but note that the content is encrypted
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: "[Redacted thinking block]",
|
||||
}
|
||||
break
|
||||
case "text":
|
||||
// we may receive multiple text blocks, in which case just insert a line break between them
|
||||
if (chunk.index > 0) {
|
||||
@@ -163,12 +186,22 @@ export class AnthropicHandler implements ApiHandler {
|
||||
break
|
||||
case "content_block_delta":
|
||||
switch (chunk.delta.type) {
|
||||
case "thinking_delta":
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: chunk.delta.thinking,
|
||||
}
|
||||
break
|
||||
case "text_delta":
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.delta.text,
|
||||
}
|
||||
break
|
||||
case "signature_delta":
|
||||
// We don't need to do anything with the signature in the client
|
||||
// It's used when sending the thinking block back to the API
|
||||
break
|
||||
}
|
||||
break
|
||||
case "content_block_stop":
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiHandler } from ".."
|
||||
import {
|
||||
ApiHandlerOptions,
|
||||
ModelInfo,
|
||||
AskSageModelId,
|
||||
askSageModels,
|
||||
askSageDefaultModelId,
|
||||
askSageDefaultURL,
|
||||
} from "../../shared/api"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
type AskSageRequest = {
|
||||
system_prompt: string
|
||||
message: {
|
||||
user: "gpt" | "me"
|
||||
message: string
|
||||
}[]
|
||||
model: string
|
||||
dataset: "none"
|
||||
}
|
||||
|
||||
type AskSageResponse = {
|
||||
uuid: string
|
||||
status: number
|
||||
// Response status
|
||||
response: string
|
||||
// Generated response message
|
||||
message: string
|
||||
}
|
||||
|
||||
export class AskSageHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private apiUrl: string
|
||||
private apiKey: string
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
console.log("init api url", options.asksageApiUrl, askSageDefaultURL)
|
||||
this.options = options
|
||||
this.apiKey = options.asksageApiKey || ""
|
||||
this.apiUrl = options.asksageApiUrl || askSageDefaultURL
|
||||
|
||||
if (!this.apiKey) {
|
||||
throw new Error("AskSage API key is required")
|
||||
}
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
try {
|
||||
const model = this.getModel()
|
||||
|
||||
// Transform messages into AskSageRequest format
|
||||
const formattedMessages = messages.map((msg) => {
|
||||
const content = Array.isArray(msg.content)
|
||||
? msg.content.map((block) => ("text" in block ? block.text : "")).join("")
|
||||
: msg.content
|
||||
|
||||
return {
|
||||
user: msg.role === "assistant" ? ("gpt" as const) : ("me" as const),
|
||||
message: content,
|
||||
}
|
||||
})
|
||||
|
||||
const request: AskSageRequest = {
|
||||
system_prompt: systemPrompt,
|
||||
message: formattedMessages,
|
||||
model: model.id,
|
||||
dataset: "none",
|
||||
}
|
||||
|
||||
// Make request to AskSage API
|
||||
const response = await fetch(`${this.apiUrl}/query`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-access-tokens": this.apiKey,
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text()
|
||||
throw new Error(`AskSage API error: ${error}`)
|
||||
}
|
||||
|
||||
const result = (await response.json()) as AskSageResponse
|
||||
|
||||
if (!result.message) {
|
||||
throw new Error("No content in AskSage response")
|
||||
}
|
||||
|
||||
// Return entire response as a single chunk since streaming is not supported
|
||||
yield {
|
||||
type: "text",
|
||||
text: result.message,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`AskSage request failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in askSageModels) {
|
||||
const id = modelId as AskSageModelId
|
||||
return { id, info: askSageModels[id] }
|
||||
}
|
||||
return {
|
||||
id: askSageDefaultModelId,
|
||||
info: askSageModels[askSageDefaultModelId],
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
-51
@@ -1,73 +1,83 @@
|
||||
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../../shared/api"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { fromIni } from "@aws-sdk/credential-providers"
|
||||
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
||||
export class AwsBedrockHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: AnthropicBedrock
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
|
||||
const clientConfig: any = {
|
||||
awsRegion: this.options.awsRegion || "us-east-1",
|
||||
}
|
||||
|
||||
if (this.options.awsUseProfile) {
|
||||
// Use profile-based credentials if enabled
|
||||
if (this.options.awsProfile) {
|
||||
clientConfig.credentials = fromIni({
|
||||
profile: this.options.awsProfile,
|
||||
})
|
||||
} else {
|
||||
// Use default profile if no specific profile is set
|
||||
clientConfig.credentials = fromIni()
|
||||
}
|
||||
} else if (this.options.awsAccessKey && this.options.awsSecretKey) {
|
||||
// Use direct credentials if provided
|
||||
clientConfig.awsAccessKey = this.options.awsAccessKey
|
||||
clientConfig.awsSecretKey = this.options.awsSecretKey
|
||||
if (this.options.awsSessionToken) {
|
||||
clientConfig.awsSessionToken = this.options.awsSessionToken
|
||||
}
|
||||
}
|
||||
|
||||
this.client = new AnthropicBedrock(clientConfig)
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
// cross region inference requires prefixing the model id with the region
|
||||
let modelId: string
|
||||
if (this.options.awsUseCrossRegionInference) {
|
||||
let regionPrefix = (this.options.awsRegion || "").slice(0, 3)
|
||||
switch (regionPrefix) {
|
||||
case "us-":
|
||||
modelId = `us.${this.getModel().id}`
|
||||
break
|
||||
case "eu-":
|
||||
modelId = `eu.${this.getModel().id}`
|
||||
break
|
||||
default:
|
||||
// cross region inference is not supported in this region, falling back to default model
|
||||
modelId = this.getModel().id
|
||||
break
|
||||
}
|
||||
} else {
|
||||
modelId = this.getModel().id
|
||||
}
|
||||
let modelId = await this.getModelId()
|
||||
|
||||
const stream = await this.client.messages.create({
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
|
||||
// Get model info and message indices for caching
|
||||
const model = this.getModel()
|
||||
const userMsgIndices = messages.reduce((acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[])
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
// Create anthropic client, using sessions created or renewed after this handler's
|
||||
// initialization, and allowing for session renewal if necessary as well
|
||||
const client = await this.getClient()
|
||||
|
||||
const stream = await client.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: this.getModel().info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
...(this.options.awsBedrockUsePromptCache === true && {
|
||||
cache_control: { type: "ephemeral" },
|
||||
}),
|
||||
},
|
||||
],
|
||||
messages: messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
...(this.options.awsBedrockUsePromptCache === true && {
|
||||
cache_control: { type: "ephemeral" },
|
||||
}),
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
...(this.options.awsBedrockUsePromptCache === true && {
|
||||
cache_control: { type: "ephemeral" },
|
||||
}),
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}
|
||||
return message
|
||||
}),
|
||||
stream: true,
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
case "message_start":
|
||||
@@ -76,6 +86,8 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
type: "usage",
|
||||
inputTokens: usage.input_tokens || 0,
|
||||
outputTokens: usage.output_tokens || 0,
|
||||
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
|
||||
cacheReadTokens: usage.cache_read_input_tokens || undefined,
|
||||
}
|
||||
break
|
||||
case "message_delta":
|
||||
@@ -85,7 +97,6 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
outputTokens: chunk.usage.output_tokens || 0,
|
||||
}
|
||||
break
|
||||
|
||||
case "content_block_start":
|
||||
switch (chunk.content_block.type) {
|
||||
case "text":
|
||||
@@ -127,4 +138,68 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
info: bedrockModels[bedrockDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
private async getClient(): Promise<AnthropicBedrock> {
|
||||
// Create AWS credentials by executing a an AWS provider chain exactly as the
|
||||
// Anthropic SDK does it, by wrapping the default chain into a temporary process
|
||||
// environment.
|
||||
const providerChain = fromNodeProviderChain()
|
||||
const credentials = await AwsBedrockHandler.withTempEnv(
|
||||
() => {
|
||||
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
|
||||
AwsBedrockHandler.setEnv("AWS_ACCESS_KEY_ID", this.options.awsAccessKey)
|
||||
AwsBedrockHandler.setEnv("AWS_SECRET_ACCESS_KEY", this.options.awsSecretKey)
|
||||
AwsBedrockHandler.setEnv("AWS_SESSION_TOKEN", this.options.awsSessionToken)
|
||||
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
|
||||
},
|
||||
() => providerChain(),
|
||||
)
|
||||
|
||||
// Return an AnthropicBedrock client with the resolved/assumed credentials.
|
||||
//
|
||||
// When AnthropicBedrock creates its AWS client, the chain will execute very
|
||||
// fast as the access/secret keys will already be already provided, and have
|
||||
// a higher precedence than the profiles.
|
||||
return new AnthropicBedrock({
|
||||
awsAccessKey: credentials.accessKeyId,
|
||||
awsSecretKey: credentials.secretAccessKey,
|
||||
awsSessionToken: credentials.sessionToken,
|
||||
awsRegion: this.options.awsRegion || "us-east-1",
|
||||
})
|
||||
}
|
||||
|
||||
async getModelId(): Promise<string> {
|
||||
if (this.options.awsUseCrossRegionInference) {
|
||||
let regionPrefix = (this.options.awsRegion || "").slice(0, 3)
|
||||
switch (regionPrefix) {
|
||||
case "us-":
|
||||
return `us.${this.getModel().id}`
|
||||
case "eu-":
|
||||
return `eu.${this.getModel().id}`
|
||||
case "ap-":
|
||||
return `apac.${this.getModel().id}`
|
||||
default:
|
||||
// cross region inference is not supported in this region, falling back to default model
|
||||
return this.getModel().id
|
||||
}
|
||||
}
|
||||
return this.getModel().id
|
||||
}
|
||||
|
||||
private static async withTempEnv<R>(updateEnv: () => void, fn: () => Promise<R>): Promise<R> {
|
||||
const previousEnv = { ...process.env }
|
||||
|
||||
try {
|
||||
updateEnv()
|
||||
return await fn()
|
||||
} finally {
|
||||
process.env = previousEnv
|
||||
}
|
||||
}
|
||||
|
||||
private static setEnv(key: string, value: string | undefined) {
|
||||
if (key !== "" && value !== undefined) {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, DeepSeekModelId, ModelInfo, deepSeekDefaultModelId, deepSeekModels } from "../../shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../utils/cost"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
@@ -19,6 +20,37 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
})
|
||||
}
|
||||
|
||||
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
|
||||
// Deepseek reports total input AND cache reads/writes,
|
||||
// see context caching: https://api-docs.deepseek.com/guides/kv_cache)
|
||||
// where the input tokens is the sum of the cache hits/misses, just like OpenAI.
|
||||
// This affects:
|
||||
// 1) context management truncation algorithm, and
|
||||
// 2) cost calculation
|
||||
|
||||
// Deepseek usage includes extra fields.
|
||||
// Safely cast the prompt token details section to the appropriate structure.
|
||||
interface DeepSeekUsage extends OpenAI.CompletionUsage {
|
||||
prompt_cache_hit_tokens?: number
|
||||
prompt_cache_miss_tokens?: number
|
||||
}
|
||||
const deepUsage = usage as DeepSeekUsage
|
||||
|
||||
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)
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
totalCost: totalCost,
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
@@ -61,15 +93,7 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0, // (deepseek reports total input AND cache reads/writes, see context caching: https://api-docs.deepseek.com/guides/kv_cache) where the input tokens is the sum of the cache hits/misses, while anthropic reports them as separate tokens. This is important to know for 1) context management truncation algorithm, and 2) cost calculation (NOTE: we report both input and cache stats but for now set input price to 0 since all the cost calculation will be done using cache hits/misses)
|
||||
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,
|
||||
}
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
|
||||
apiKey: "not-needed",
|
||||
apiKey: this.options.liteLlmApiKey || "noop",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ export class MistralHandler implements ApiHandler {
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new Mistral({
|
||||
serverURL: "https://api.mistral.ai",
|
||||
apiKey: this.options.mistralApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
+12
-17
@@ -1,40 +1,35 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { Message, Ollama } from "ollama"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class OllamaHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: Ollama
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: (this.options.ollamaBaseUrl || "http://localhost:11434") + "/v1",
|
||||
apiKey: "ollama",
|
||||
})
|
||||
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await this.client.chat({
|
||||
model: this.getModel().id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
messages: ollamaMessages,
|
||||
stream: true,
|
||||
options: {
|
||||
num_ctx: Number(this.options.ollamaApiOptionsCtxNum) || 32768,
|
||||
},
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
if (typeof chunk.message.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
text: chunk.message.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
openAiNativeModels,
|
||||
} from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { calculateApiCostOpenAI } from "../../utils/cost"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
|
||||
|
||||
@@ -24,31 +25,47 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
})
|
||||
}
|
||||
|
||||
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
|
||||
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)
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
totalCost: totalCost,
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
switch (this.getModel().id) {
|
||||
const model = this.getModel()
|
||||
|
||||
switch (model.id) {
|
||||
case "o1":
|
||||
case "o1-preview":
|
||||
case "o1-mini": {
|
||||
// o1 doesnt support streaming, non-1 temp, or system prompt
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
model: model.id,
|
||||
messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
})
|
||||
yield {
|
||||
type: "text",
|
||||
text: response.choices[0]?.message.content || "",
|
||||
}
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: response.usage?.prompt_tokens || 0,
|
||||
outputTokens: response.usage?.completion_tokens || 0,
|
||||
}
|
||||
|
||||
yield* this.yieldUsage(model.info, response.usage)
|
||||
|
||||
break
|
||||
}
|
||||
case "o3-mini": {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
model: model.id,
|
||||
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
@@ -63,18 +80,15 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
}
|
||||
// Only last chunk contains usage
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
model: model.id,
|
||||
// max_completion_tokens: this.getModel().info.maxTokens,
|
||||
temperature: 0,
|
||||
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
@@ -90,14 +104,9 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
// contains a null value except for the last chunk which contains the token usage statistics for the entire request
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
}
|
||||
// Only last chunk contains usage
|
||||
yield* this.yieldUsage(model.info, chunk.usage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ApiHandler } from "../index"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
|
||||
|
||||
export class OpenAiHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -32,20 +33,30 @@ export class OpenAiHandler implements ApiHandler {
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.options.openAiModelId ?? ""
|
||||
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
|
||||
const isO3Mini = modelId.includes("o3-mini")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
let temperature: number | undefined = 0
|
||||
let reasoningEffort: ChatCompletionReasoningEffort | undefined = undefined
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
if (isO3Mini) {
|
||||
openAiMessages = [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
|
||||
temperature = undefined // does not support temperature
|
||||
reasoningEffort = (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium"
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
temperature,
|
||||
reasoning_effort: reasoningEffort,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
@@ -78,7 +89,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.openAiModelId ?? "",
|
||||
info: openAiModelInfoSaneDefaults,
|
||||
info: this.options.openAiModelInfo ?? openAiModelInfoSaneDefaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefau
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
|
||||
export class OpenRouterHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -38,6 +39,11 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// prompt caching: https://openrouter.ai/docs/prompt-caching
|
||||
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
case "anthropic/claude-3.7-sonnet:thinking":
|
||||
case "anthropic/claude-3-7-sonnet":
|
||||
case "anthropic/claude-3-7-sonnet:beta":
|
||||
case "anthropic/claude-3.5-sonnet":
|
||||
case "anthropic/claude-3.5-sonnet:beta":
|
||||
case "anthropic/claude-3.5-sonnet-20240620":
|
||||
@@ -89,6 +95,11 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// (models usually default to max tokens allowed)
|
||||
let maxTokens: number | undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
case "anthropic/claude-3.7-sonnet:thinking":
|
||||
case "anthropic/claude-3-7-sonnet":
|
||||
case "anthropic/claude-3-7-sonnet:beta":
|
||||
case "anthropic/claude-3.5-sonnet":
|
||||
case "anthropic/claude-3.5-sonnet:beta":
|
||||
case "anthropic/claude-3.5-sonnet-20240620":
|
||||
@@ -101,16 +112,29 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
break
|
||||
}
|
||||
|
||||
let temperature = 0
|
||||
let temperature: number | undefined = 0
|
||||
let topP: number | undefined = undefined
|
||||
// Handle models based on deepseek-r1
|
||||
if (this.getModel().id.startsWith("deepseek/deepseek-r1") || this.getModel().id === "perplexity/sonar-reasoning") {
|
||||
// Recommended temperature for DeepSeek reasoning models
|
||||
temperature = 0.6
|
||||
// DeepSeek highly recommends using user instead of system role
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
// Some provider support topP and 0.95 is value that Deepseek used in their benchmarks
|
||||
// Recommended values from DeepSeek
|
||||
temperature = 0.7
|
||||
topP = 0.95
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
let reasoning: { max_tokens: number } | undefined = undefined
|
||||
switch (model.id) {
|
||||
case "anthropic/claude-3.7-sonnet":
|
||||
case "anthropic/claude-3.7-sonnet:beta":
|
||||
case "anthropic/claude-3.7-sonnet:thinking":
|
||||
case "anthropic/claude-3-7-sonnet":
|
||||
case "anthropic/claude-3-7-sonnet:beta":
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = budget_tokens !== 0 ? true : false
|
||||
if (reasoningOn) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: budget_tokens }
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Removes messages in the middle when close to context window limit. Should not be applied to models that support prompt caching since it would continuously break the cache.
|
||||
@@ -131,6 +155,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
|
||||
include_reasoning: true,
|
||||
...(model.id === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
})
|
||||
|
||||
let genId: string | undefined
|
||||
@@ -138,9 +163,11 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
for await (const chunk of stream) {
|
||||
// openrouter returns an error object instead of the openai sdk throwing an error
|
||||
if ("error" in chunk) {
|
||||
const error = chunk.error as { message?: string; code?: number }
|
||||
const error = chunk.error as OpenRouterErrorResponse["error"]
|
||||
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
|
||||
throw new Error(`OpenRouter API Error ${error?.code}: ${error?.message}`)
|
||||
// Include metadata in the error message if available
|
||||
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
|
||||
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
|
||||
}
|
||||
|
||||
if (!genId && chunk.id) {
|
||||
@@ -194,8 +221,31 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// }
|
||||
}
|
||||
|
||||
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
|
||||
if (genId) {
|
||||
await delay(500) // FIXME: necessary delay to ensure generation endpoint is ready
|
||||
try {
|
||||
const generationIterator = this.fetchGenerationDetails(genId)
|
||||
const generation = (await generationIterator.next()).value
|
||||
// console.log("OpenRouter generation details:", generation)
|
||||
yield {
|
||||
type: "usage",
|
||||
// cacheWriteTokens: 0,
|
||||
// cacheReadTokens: 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching OpenRouter generation details:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry({ maxRetries: 4, baseDelay: 250, maxDelay: 1000, retryAllErrors: true })
|
||||
async *fetchGenerationDetails(genId: string) {
|
||||
// console.log("Fetching generation details for:", genId)
|
||||
try {
|
||||
const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, {
|
||||
headers: {
|
||||
@@ -203,21 +253,11 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
},
|
||||
timeout: 5_000, // this request hangs sometimes
|
||||
})
|
||||
|
||||
const generation = response.data?.data
|
||||
console.log("OpenRouter generation details:", response.data)
|
||||
yield {
|
||||
type: "usage",
|
||||
// cacheWriteTokens: 0,
|
||||
// cacheReadTokens: 0,
|
||||
// openrouter generation endpoint fails often
|
||||
inputTokens: generation?.native_tokens_prompt || 0,
|
||||
outputTokens: generation?.native_tokens_completion || 0,
|
||||
totalCost: generation?.total_cost || 0,
|
||||
}
|
||||
yield response.data?.data
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching OpenRouter generation details:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, QwenModelId, ModelInfo, qwenDefaultModelId, qwenModels } from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
|
||||
export class QwenHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -34,17 +35,21 @@ export class QwenHandler implements ApiHandler {
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-r1")
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...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 },
|
||||
...(model.id === "deepseek-r1" ? {} : { temperature: 0 }),
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
|
||||
export class RequestyHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -16,30 +15,32 @@ export class RequestyHandler implements ApiHandler {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://router.requesty.ai/v1",
|
||||
apiKey: this.options.requestyApiKey,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.options.requestyModelId ?? ""
|
||||
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
if (isDeepseekReasoner) {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...(modelId === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
@@ -56,11 +57,31 @@ export class RequestyHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Requesty usage includes an extra field for Anthropic use cases.
|
||||
// Safely cast the prompt token details section to the appropriate structure.
|
||||
interface RequestyUsage extends OpenAI.CompletionUsage {
|
||||
prompt_tokens_details?: {
|
||||
caching_tokens?: number
|
||||
cached_tokens?: number
|
||||
}
|
||||
total_cost?: number
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
const usage = chunk.usage as RequestyUsage
|
||||
const inputTokens = usage.prompt_tokens || 0
|
||||
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 = 0 // TODO: Replace with calculateApiCostOpenAI(model.info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
totalCost: totalCost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// For the following openrouter error type sources, see the docs here:
|
||||
// https://openrouter.ai/docs/api-reference/errors
|
||||
|
||||
export type OpenRouterErrorResponse = {
|
||||
error: {
|
||||
message: string
|
||||
code: number
|
||||
metadata?: OpenRouterProviderErrorMetadata | OpenRouterModerationErrorMetadata | Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export type OpenRouterProviderErrorMetadata = {
|
||||
provider_name: string // The name of the provider that encountered the error
|
||||
raw: unknown // The raw error from the provider
|
||||
}
|
||||
|
||||
export type OpenRouterModerationErrorMetadata = {
|
||||
reasons: string[] // Why your input was flagged
|
||||
flagged_input: string // The text segment that was flagged, limited to 100 characters. If the flagged input is longer than 100 characters, it will be truncated in the middle and replaced with ...
|
||||
provider_name: string // The name of the provider that requested moderation
|
||||
model_slug: string
|
||||
}
|
||||
+216
-45
@@ -4,75 +4,246 @@ import { withRetry } from "../retry"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../../shared/api"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { VertexAI } from "@google-cloud/vertexai"
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
|
||||
export class VertexHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: AnthropicVertex
|
||||
private clientAnthropic: AnthropicVertex
|
||||
private clientVertex: VertexAI
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new AnthropicVertex({
|
||||
this.clientAnthropic = new AnthropicVertex({
|
||||
projectId: this.options.vertexProjectId,
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
|
||||
region: this.options.vertexRegion,
|
||||
})
|
||||
this.clientVertex = new VertexAI({
|
||||
project: this.options.vertexProjectId,
|
||||
location: this.options.vertexRegion,
|
||||
})
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const stream = await this.client.messages.create({
|
||||
model: this.getModel().id,
|
||||
max_tokens: this.getModel().info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
system: systemPrompt,
|
||||
messages,
|
||||
stream: true,
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
case "message_start":
|
||||
const usage = chunk.message.usage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.input_tokens || 0,
|
||||
outputTokens: usage.output_tokens || 0,
|
||||
}
|
||||
break
|
||||
case "message_delta":
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: chunk.usage.output_tokens || 0,
|
||||
}
|
||||
break
|
||||
const model = this.getModel()
|
||||
const modelId = model.id
|
||||
|
||||
case "content_block_start":
|
||||
switch (chunk.content_block.type) {
|
||||
case "text":
|
||||
if (chunk.index > 0) {
|
||||
if (modelId.includes("claude")) {
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = modelId.includes("3-7") && budget_tokens !== 0 ? true : false
|
||||
|
||||
let stream
|
||||
switch (modelId) {
|
||||
case "claude-3-7-sonnet@20250219":
|
||||
case "claude-3-5-sonnet-v2@20241022":
|
||||
case "claude-3-5-sonnet@20240620":
|
||||
case "claude-3-5-haiku@20241022":
|
||||
case "claude-3-opus@20240229":
|
||||
case "claude-3-haiku@20240307": {
|
||||
// Find indices of user messages for cache control
|
||||
const userMsgIndices = messages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
stream = await this.clientAnthropic.beta.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
temperature: reasoningOn ? undefined : 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
messages: messages.map((message, index) => {
|
||||
if (index === lastUserMsgIndex || index === secondLastMsgUserIndex) {
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
]
|
||||
: message.content.map((content, contentIndex) =>
|
||||
contentIndex === message.content.length - 1
|
||||
? {
|
||||
...content,
|
||||
cache_control: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
}
|
||||
: content,
|
||||
),
|
||||
}
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
}
|
||||
}),
|
||||
stream: true,
|
||||
},
|
||||
{
|
||||
headers: {},
|
||||
},
|
||||
)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
stream = await this.clientAnthropic.beta.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
system: [
|
||||
{
|
||||
text: systemPrompt,
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
messages: messages.map((message) => ({
|
||||
...message,
|
||||
content:
|
||||
typeof message.content === "string"
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: message.content,
|
||||
},
|
||||
]
|
||||
: message.content,
|
||||
})),
|
||||
stream: true,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
case "message_start":
|
||||
const usage = chunk.message.usage
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.input_tokens || 0,
|
||||
outputTokens: usage.output_tokens || 0,
|
||||
cacheWriteTokens: usage.cache_creation_input_tokens || undefined,
|
||||
cacheReadTokens: usage.cache_read_input_tokens || undefined,
|
||||
}
|
||||
break
|
||||
case "message_delta":
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: 0,
|
||||
outputTokens: chunk.usage.output_tokens || 0,
|
||||
}
|
||||
break
|
||||
case "message_stop":
|
||||
break
|
||||
case "content_block_start":
|
||||
switch (chunk.content_block.type) {
|
||||
case "text":
|
||||
if (chunk.index > 0) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: "\n",
|
||||
}
|
||||
}
|
||||
yield {
|
||||
type: "text",
|
||||
text: "\n",
|
||||
text: chunk.content_block.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case "content_block_delta":
|
||||
switch (chunk.delta.type) {
|
||||
case "text_delta":
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.delta.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
case "content_block_stop":
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// gemini
|
||||
const generativeModel = this.clientVertex.getGenerativeModel({
|
||||
model: this.getModel().id,
|
||||
systemInstruction: {
|
||||
role: "system",
|
||||
parts: [{ text: systemPrompt }],
|
||||
},
|
||||
})
|
||||
const request = {
|
||||
contents: [
|
||||
{
|
||||
role: "user",
|
||||
parts: messages.map((m) => {
|
||||
if (typeof m.content === "string") {
|
||||
return { text: m.content }
|
||||
} else if (Array.isArray(m.content)) {
|
||||
return {
|
||||
text: m.content
|
||||
.map((block) => {
|
||||
if (typeof block === "string") {
|
||||
return block
|
||||
} else if (block.type === "text") {
|
||||
return block.text
|
||||
} else {
|
||||
console.log("Unsupported block type", block)
|
||||
return ""
|
||||
}
|
||||
})
|
||||
.join(" "),
|
||||
}
|
||||
} else {
|
||||
return { text: "" }
|
||||
}
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
const streamingResult = await generativeModel.generateContentStream(request)
|
||||
for await (const chunk of streamingResult.stream) {
|
||||
// If usage data is available, yield it similarly:
|
||||
// yield { type: "usage", inputTokens: 0, outputTokens: 0 }
|
||||
// Otherwise, just yield text:
|
||||
const candidates = chunk.candidates || []
|
||||
for (const candidate of candidates) {
|
||||
for (const part of candidate.content?.parts || []) {
|
||||
if (part.text) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.content_block.text,
|
||||
text: part.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
case "content_block_delta":
|
||||
switch (chunk.delta.type) {
|
||||
case "text_delta":
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.delta.text,
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import * as vscode from "vscode"
|
||||
import { ApiHandler, SingleCompletionHandler } from "../"
|
||||
import { calculateApiCost } from "../../utils/cost"
|
||||
import { calculateApiCostAnthropic } from "../../utils/cost"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
|
||||
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
|
||||
@@ -525,7 +525,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
type: "usage",
|
||||
inputTokens: totalInputTokens,
|
||||
outputTokens: totalOutputTokens,
|
||||
totalCost: calculateApiCost(this.getModel().info, totalInputTokens, totalOutputTokens),
|
||||
totalCost: calculateApiCostAnthropic(this.getModel().info, totalInputTokens, totalOutputTokens),
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
this.ensureCleanState()
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels } from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class XAIHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.x.ai/v1",
|
||||
apiKey: this.options.xaiApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
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 },
|
||||
})
|
||||
|
||||
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: 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: XAIModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in xaiModels) {
|
||||
const id = modelId as XAIModelId
|
||||
return { id, info: xaiModels[id] }
|
||||
}
|
||||
return {
|
||||
id: xaiDefaultModelId,
|
||||
info: xaiModels[xaiDefaultModelId],
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -2,16 +2,18 @@ interface RetryOptions {
|
||||
maxRetries?: number
|
||||
baseDelay?: number
|
||||
maxDelay?: number
|
||||
retryAllErrors?: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Required<RetryOptions> = {
|
||||
maxRetries: 3,
|
||||
baseDelay: 1_000,
|
||||
maxDelay: 10_000,
|
||||
retryAllErrors: false,
|
||||
}
|
||||
|
||||
export function withRetry(options: RetryOptions = {}) {
|
||||
const { maxRetries, baseDelay, maxDelay } = { ...DEFAULT_OPTIONS, ...options }
|
||||
const { maxRetries, baseDelay, maxDelay, retryAllErrors } = { ...DEFAULT_OPTIONS, ...options }
|
||||
|
||||
return function (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
const originalMethod = descriptor.value
|
||||
@@ -25,7 +27,7 @@ export function withRetry(options: RetryOptions = {}) {
|
||||
const isRateLimit = error?.status === 429
|
||||
const isLastAttempt = attempt === maxRetries - 1
|
||||
|
||||
if (!isRateLimit || isLastAttempt) {
|
||||
if ((!isRateLimit && !retryAllErrors) || isLastAttempt) {
|
||||
throw error
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import {
|
||||
Content,
|
||||
EnhancedGenerateContentResponse,
|
||||
FunctionCallPart,
|
||||
FunctionDeclaration,
|
||||
FunctionResponsePart,
|
||||
InlineDataPart,
|
||||
Part,
|
||||
SchemaType,
|
||||
TextPart,
|
||||
} from "@google/generative-ai"
|
||||
import { Content, EnhancedGenerateContentResponse, InlineDataPart, Part, TextPart } from "@google/generative-ai"
|
||||
|
||||
export function convertAnthropicContentToGemini(
|
||||
content:
|
||||
| string
|
||||
| Array<
|
||||
| Anthropic.Messages.TextBlockParam
|
||||
| Anthropic.Messages.ImageBlockParam
|
||||
| Anthropic.Messages.ToolUseBlockParam
|
||||
| Anthropic.Messages.ToolResultBlockParam
|
||||
>,
|
||||
): Part[] {
|
||||
export function convertAnthropicContentToGemini(content: string | Anthropic.ContentBlockParam[]): Part[] {
|
||||
if (typeof content === "string") {
|
||||
return [{ text: content } as TextPart]
|
||||
}
|
||||
@@ -38,55 +19,6 @@ export function convertAnthropicContentToGemini(
|
||||
mimeType: block.source.media_type,
|
||||
},
|
||||
} as InlineDataPart
|
||||
case "tool_use":
|
||||
return {
|
||||
functionCall: {
|
||||
name: block.name,
|
||||
args: block.input,
|
||||
},
|
||||
} as FunctionCallPart
|
||||
case "tool_result":
|
||||
const name = block.tool_use_id.split("-")[0]
|
||||
if (!block.content) {
|
||||
return []
|
||||
}
|
||||
if (typeof block.content === "string") {
|
||||
return {
|
||||
functionResponse: {
|
||||
name,
|
||||
response: {
|
||||
name,
|
||||
content: block.content,
|
||||
},
|
||||
},
|
||||
} as FunctionResponsePart
|
||||
} else {
|
||||
// The only case when tool_result could be array is when the tool failed and we're providing ie user feedback potentially with images
|
||||
const textParts = block.content.filter((part) => part.type === "text")
|
||||
const imageParts = block.content.filter((part) => part.type === "image")
|
||||
const text = textParts.length > 0 ? textParts.map((part) => part.text).join("\n\n") : ""
|
||||
const imageText = imageParts.length > 0 ? "\n\n(See next part for image)" : ""
|
||||
return [
|
||||
{
|
||||
functionResponse: {
|
||||
name,
|
||||
response: {
|
||||
name,
|
||||
content: text + imageText,
|
||||
},
|
||||
},
|
||||
} as FunctionResponsePart,
|
||||
...imageParts.map(
|
||||
(part) =>
|
||||
({
|
||||
inlineData: {
|
||||
data: part.source.data,
|
||||
mimeType: part.source.media_type,
|
||||
},
|
||||
}) as InlineDataPart,
|
||||
),
|
||||
]
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported content block type: ${(block as any).type}`)
|
||||
}
|
||||
@@ -100,26 +32,6 @@ export function convertAnthropicMessageToGemini(message: Anthropic.Messages.Mess
|
||||
}
|
||||
}
|
||||
|
||||
export function convertAnthropicToolToGemini(tool: Anthropic.Messages.Tool): FunctionDeclaration {
|
||||
return {
|
||||
name: tool.name,
|
||||
description: tool.description || "",
|
||||
parameters: {
|
||||
type: SchemaType.OBJECT,
|
||||
properties: Object.fromEntries(
|
||||
Object.entries(tool.input_schema.properties || {}).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
type: (value as any).type.toUpperCase(),
|
||||
description: (value as any).description || "",
|
||||
},
|
||||
]),
|
||||
),
|
||||
required: (tool.input_schema.required as string[]) || [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
It looks like gemini likes to double escape certain characters when writing file contents: https://discuss.ai.google.dev/t/function-call-string-property-is-double-escaped/37867
|
||||
*/
|
||||
@@ -133,23 +45,7 @@ export function convertGeminiResponseToAnthropic(response: EnhancedGenerateConte
|
||||
// Add the main text response
|
||||
const text = response.text()
|
||||
if (text) {
|
||||
content.push({ type: "text", text })
|
||||
}
|
||||
|
||||
// Add function calls as tool_use blocks
|
||||
const functionCalls = response.functionCalls()
|
||||
if (functionCalls) {
|
||||
functionCalls.forEach((call, index) => {
|
||||
if ("content" in call.args && typeof call.args.content === "string") {
|
||||
call.args.content = unescapeGeminiContent(call.args.content)
|
||||
}
|
||||
content.push({
|
||||
type: "tool_use",
|
||||
id: `${call.name}-${index}-${Date.now()}`,
|
||||
name: call.name,
|
||||
input: call.args,
|
||||
})
|
||||
})
|
||||
content.push({ type: "text", text, citations: null })
|
||||
}
|
||||
|
||||
// Determine stop reason
|
||||
@@ -183,6 +79,8 @@ export function convertGeminiResponseToAnthropic(response: EnhancedGenerateConte
|
||||
usage: {
|
||||
input_tokens: response.usageMetadata?.promptTokenCount ?? 0,
|
||||
output_tokens: response.usageMetadata?.candidatesTokenCount ?? 0,
|
||||
cache_creation_input_tokens: null,
|
||||
cache_read_input_tokens: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +376,7 @@ export function convertO1ResponseToAnthropicMessage(
|
||||
{
|
||||
type: "text",
|
||||
text: normalText,
|
||||
citations: null,
|
||||
},
|
||||
],
|
||||
model: completion.model,
|
||||
@@ -396,6 +397,8 @@ export function convertO1ResponseToAnthropicMessage(
|
||||
usage: {
|
||||
input_tokens: completion.usage?.prompt_tokens || 0,
|
||||
output_tokens: completion.usage?.completion_tokens || 0,
|
||||
cache_creation_input_tokens: null,
|
||||
cache_read_input_tokens: null,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Message } from "ollama"
|
||||
|
||||
export function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessageParam[]): Message[] {
|
||||
const ollamaMessages: Message[] = []
|
||||
|
||||
for (const anthropicMessage of anthropicMessages) {
|
||||
if (typeof anthropicMessage.content === "string") {
|
||||
ollamaMessages.push({
|
||||
role: anthropicMessage.role,
|
||||
content: anthropicMessage.content,
|
||||
})
|
||||
} else {
|
||||
if (anthropicMessage.role === "user") {
|
||||
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
|
||||
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
|
||||
toolMessages: Anthropic.ToolResultBlockParam[]
|
||||
}>(
|
||||
(acc, part) => {
|
||||
if (part.type === "tool_result") {
|
||||
acc.toolMessages.push(part)
|
||||
} else if (part.type === "text" || part.type === "image") {
|
||||
acc.nonToolMessages.push(part)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{ nonToolMessages: [], toolMessages: [] },
|
||||
)
|
||||
|
||||
// Process tool result messages FIRST since they must follow the tool use messages
|
||||
let toolResultImages: string[] = []
|
||||
toolMessages.forEach((toolMessage) => {
|
||||
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the Ollama SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
|
||||
let content: string
|
||||
|
||||
if (typeof toolMessage.content === "string") {
|
||||
content = toolMessage.content
|
||||
} else {
|
||||
content =
|
||||
toolMessage.content
|
||||
?.map((part) => {
|
||||
if (part.type === "image") {
|
||||
toolResultImages.push(`data:${part.source.media_type};base64,${part.source.data}`)
|
||||
return "(see following user message for image)"
|
||||
}
|
||||
return part.text
|
||||
})
|
||||
.join("\n") ?? ""
|
||||
}
|
||||
ollamaMessages.push({
|
||||
role: "user",
|
||||
images: toolResultImages.length > 0 ? toolResultImages : undefined,
|
||||
content: content,
|
||||
})
|
||||
})
|
||||
|
||||
// Process non-tool messages
|
||||
if (nonToolMessages.length > 0) {
|
||||
ollamaMessages.push({
|
||||
role: "user",
|
||||
content: nonToolMessages
|
||||
.map((part) => {
|
||||
if (part.type === "image") {
|
||||
return `data:${part.source.media_type};base64,${part.source.data}`
|
||||
}
|
||||
return part.text
|
||||
})
|
||||
.join("\n"),
|
||||
})
|
||||
}
|
||||
} else if (anthropicMessage.role === "assistant") {
|
||||
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
|
||||
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
|
||||
toolMessages: Anthropic.ToolUseBlockParam[]
|
||||
}>(
|
||||
(acc, part) => {
|
||||
if (part.type === "tool_use") {
|
||||
acc.toolMessages.push(part)
|
||||
} else if (part.type === "text" || part.type === "image") {
|
||||
acc.nonToolMessages.push(part)
|
||||
} // assistant cannot send tool_result messages
|
||||
return acc
|
||||
},
|
||||
{ nonToolMessages: [], toolMessages: [] },
|
||||
)
|
||||
|
||||
// Process non-tool messages
|
||||
let content: string = ""
|
||||
if (nonToolMessages.length > 0) {
|
||||
content = nonToolMessages
|
||||
.map((part) => {
|
||||
if (part.type === "image") {
|
||||
return "" // impossible as the assistant cannot send images
|
||||
}
|
||||
return part.text
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
ollamaMessages.push({
|
||||
role: "assistant",
|
||||
content,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ollamaMessages
|
||||
}
|
||||
@@ -161,6 +161,7 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
|
||||
{
|
||||
type: "text",
|
||||
text: openAiMessage.content || "",
|
||||
citations: null,
|
||||
},
|
||||
],
|
||||
model: completion.model,
|
||||
@@ -181,6 +182,8 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
|
||||
usage: {
|
||||
input_tokens: completion.usage?.prompt_tokens || 0,
|
||||
output_tokens: completion.usage?.completion_tokens || 0,
|
||||
cache_creation_input_tokens: null,
|
||||
cache_read_input_tokens: null,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
|
||||
type ContentPartText = OpenAI.Chat.ChatCompletionContentPartText
|
||||
type ContentPartImage = OpenAI.Chat.ChatCompletionContentPartImage
|
||||
type UserMessage = OpenAI.Chat.ChatCompletionUserMessageParam
|
||||
type AssistantMessage = OpenAI.Chat.ChatCompletionAssistantMessageParam
|
||||
type Message = OpenAI.Chat.ChatCompletionMessageParam
|
||||
type AnthropicMessage = Anthropic.Messages.MessageParam
|
||||
|
||||
/**
|
||||
* Converts Anthropic messages to OpenAI format while merging consecutive messages with the same role.
|
||||
* Converts Anthropic messages to OpenAI format and merges consecutive messages with the same role.
|
||||
* This is required for DeepSeek Reasoner which does not support successive messages with the same role.
|
||||
* DeepSeek highly recommends using 'user' role instead of 'system' role for optimal performance.
|
||||
*
|
||||
* @param messages Array of Anthropic messages
|
||||
* @returns Array of OpenAI messages where consecutive messages with the same role are combined
|
||||
* @returns Array of OpenAI messages where consecutive messages with the same role are merged together
|
||||
*/
|
||||
export function convertToR1Format(messages: AnthropicMessage[]): Message[] {
|
||||
return messages.reduce<Message[]>((merged, message) => {
|
||||
export function convertToR1Format(messages: Anthropic.Messages.MessageParam[]): OpenAI.Chat.ChatCompletionMessageParam[] {
|
||||
return messages.reduce<OpenAI.Chat.ChatCompletionMessageParam[]>((merged, message) => {
|
||||
const lastMessage = merged[merged.length - 1]
|
||||
let messageContent: string | (ContentPartText | ContentPartImage)[] = ""
|
||||
let messageContent: string | (OpenAI.Chat.ChatCompletionContentPartText | OpenAI.Chat.ChatCompletionContentPartImage)[] =
|
||||
""
|
||||
let hasImages = false
|
||||
|
||||
// Convert content to appropriate format
|
||||
if (Array.isArray(message.content)) {
|
||||
const textParts: string[] = []
|
||||
const imageParts: ContentPartImage[] = []
|
||||
const imageParts: OpenAI.Chat.ChatCompletionContentPartImage[] = []
|
||||
|
||||
message.content.forEach((part) => {
|
||||
if (part.type === "text") {
|
||||
@@ -40,7 +34,7 @@ export function convertToR1Format(messages: AnthropicMessage[]): Message[] {
|
||||
})
|
||||
|
||||
if (hasImages) {
|
||||
const parts: (ContentPartText | ContentPartImage)[] = []
|
||||
const parts: (OpenAI.Chat.ChatCompletionContentPartText | OpenAI.Chat.ChatCompletionContentPartImage)[] = []
|
||||
if (textParts.length > 0) {
|
||||
parts.push({ type: "text", text: textParts.join("\n") })
|
||||
}
|
||||
@@ -53,13 +47,11 @@ export function convertToR1Format(messages: AnthropicMessage[]): Message[] {
|
||||
messageContent = message.content
|
||||
}
|
||||
|
||||
// If last message has same role, merge the content
|
||||
// If the last message has the same role, merge the content
|
||||
if (lastMessage?.role === message.role) {
|
||||
if (typeof lastMessage.content === "string" && typeof messageContent === "string") {
|
||||
lastMessage.content += `\n${messageContent}`
|
||||
}
|
||||
// If either has image content, convert both to array format
|
||||
else {
|
||||
} else {
|
||||
const lastContent = Array.isArray(lastMessage.content)
|
||||
? lastMessage.content
|
||||
: [{ type: "text" as const, text: lastMessage.content || "" }]
|
||||
@@ -69,30 +61,32 @@ export function convertToR1Format(messages: AnthropicMessage[]): Message[] {
|
||||
: [{ type: "text" as const, text: messageContent }]
|
||||
|
||||
if (message.role === "assistant") {
|
||||
const mergedContent = [...lastContent, ...newContent] as AssistantMessage["content"]
|
||||
const mergedContent = [
|
||||
...lastContent,
|
||||
...newContent,
|
||||
] as OpenAI.Chat.ChatCompletionAssistantMessageParam["content"]
|
||||
lastMessage.content = mergedContent
|
||||
} else {
|
||||
const mergedContent = [...lastContent, ...newContent] as UserMessage["content"]
|
||||
const mergedContent = [...lastContent, ...newContent] as OpenAI.Chat.ChatCompletionUserMessageParam["content"]
|
||||
lastMessage.content = mergedContent
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Add as new message with the correct type based on role
|
||||
// Adds new message with the correct type based on role
|
||||
if (message.role === "assistant") {
|
||||
const newMessage: AssistantMessage = {
|
||||
const newMessage: OpenAI.Chat.ChatCompletionAssistantMessageParam = {
|
||||
role: "assistant",
|
||||
content: messageContent as AssistantMessage["content"],
|
||||
content: messageContent as OpenAI.Chat.ChatCompletionAssistantMessageParam["content"],
|
||||
}
|
||||
merged.push(newMessage)
|
||||
} else {
|
||||
const newMessage: UserMessage = {
|
||||
const newMessage: OpenAI.Chat.ChatCompletionUserMessageParam = {
|
||||
role: "user",
|
||||
content: messageContent as UserMessage["content"],
|
||||
content: messageContent as OpenAI.Chat.ChatCompletionUserMessageParam["content"],
|
||||
}
|
||||
merged.push(newMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return merged
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -175,6 +175,7 @@ export async function convertToAnthropicMessage(
|
||||
return {
|
||||
type: "text",
|
||||
text: part.value,
|
||||
citations: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +196,8 @@ export async function convertToAnthropicMessage(
|
||||
usage: {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_creation_input_tokens: null,
|
||||
cache_read_input_tokens: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+251
-189
@@ -14,7 +14,7 @@ import { OpenRouterHandler } from "../api/providers/openrouter"
|
||||
import { ApiStream } from "../api/transform/stream"
|
||||
import CheckpointTracker from "../integrations/checkpoints/CheckpointTracker"
|
||||
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
|
||||
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
|
||||
import { formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
|
||||
import { extractTextFromFile } from "../integrations/misc/extract-text"
|
||||
import { showSystemNotification } from "../integrations/notifications"
|
||||
import { TerminalManager } from "../integrations/terminal/TerminalManager"
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
import { getApiMetrics } from "../shared/getApiMetrics"
|
||||
import { HistoryItem } from "../shared/HistoryItem"
|
||||
import { ClineAskResponse, ClineCheckpointRestore } from "../shared/WebviewMessage"
|
||||
import { calculateApiCost } from "../utils/cost"
|
||||
import { calculateApiCostAnthropic } from "../utils/cost"
|
||||
import { fileExistsAtPath } from "../utils/fs"
|
||||
import { arePathsEqual, getReadablePath } from "../utils/path"
|
||||
import { fixModelHtmlEscaping, removeInvalidChars } from "../utils/string"
|
||||
@@ -59,13 +59,13 @@ import { formatResponse } from "./prompts/responses"
|
||||
import { addUserInstructions, SYSTEM_PROMPT } from "./prompts/system"
|
||||
import { getNextTruncationRange, getTruncatedMessages } from "./sliding-window"
|
||||
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
|
||||
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay, LanguageKey } from "../shared/Languages"
|
||||
import { telemetryService } from "../services/telemetry/TelemetryService"
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
|
||||
|
||||
type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
type UserContent = Array<
|
||||
Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
|
||||
>
|
||||
type UserContent = Array<Anthropic.ContentBlockParam>
|
||||
|
||||
export class Cline {
|
||||
readonly taskId: string
|
||||
@@ -353,6 +353,20 @@ export class Cline {
|
||||
break
|
||||
}
|
||||
|
||||
if (restoreType !== "task") {
|
||||
// Set isCheckpointCheckedOut flag on the message
|
||||
// Find all checkpoint messages before this one
|
||||
const checkpointMessages = this.clineMessages.filter((m) => m.say === "checkpoint_created")
|
||||
const currentMessageIndex = checkpointMessages.findIndex((m) => m.ts === messageTs)
|
||||
|
||||
// Set isCheckpointCheckedOut to false for all checkpoint messages
|
||||
checkpointMessages.forEach((m, i) => {
|
||||
m.isCheckpointCheckedOut = i === currentMessageIndex
|
||||
})
|
||||
}
|
||||
|
||||
await this.saveClineMessages()
|
||||
|
||||
await this.providerRef.deref()?.postMessageToWebview({ type: "relinquishControl" })
|
||||
|
||||
this.providerRef.deref()?.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
|
||||
@@ -846,42 +860,7 @@ export class Cline {
|
||||
|
||||
// need to make sure that the api conversation history can be resumed by the api, even if it goes out of sync with cline messages
|
||||
|
||||
let existingApiConversationHistory: Anthropic.Messages.MessageParam[] = await this.getSavedApiConversationHistory()
|
||||
|
||||
// v2.0 xml tags refactor caveat: since we don't use tools anymore, we need to replace all tool use blocks with a text block since the API disallows conversations with tool uses and no tool schema
|
||||
const conversationWithoutToolBlocks = existingApiConversationHistory.map((message) => {
|
||||
if (Array.isArray(message.content)) {
|
||||
const newContent = message.content.map((block) => {
|
||||
if (block.type === "tool_use") {
|
||||
// it's important we convert to the new tool schema format so the model doesn't get confused about how to invoke tools
|
||||
const inputAsXml = Object.entries(block.input as Record<string, string>)
|
||||
.map(([key, value]) => `<${key}>\n${value}\n</${key}>`)
|
||||
.join("\n")
|
||||
return {
|
||||
type: "text",
|
||||
text: `<${block.name}>\n${inputAsXml}\n</${block.name}>`,
|
||||
} as Anthropic.Messages.TextBlockParam
|
||||
} else if (block.type === "tool_result") {
|
||||
// Convert block.content to text block array, removing images
|
||||
const contentAsTextBlocks = Array.isArray(block.content)
|
||||
? block.content.filter((item) => item.type === "text")
|
||||
: [{ type: "text", text: block.content }]
|
||||
const textContent = contentAsTextBlocks.map((item) => item.text).join("\n\n")
|
||||
const toolName = findToolName(block.tool_use_id, existingApiConversationHistory)
|
||||
return {
|
||||
type: "text",
|
||||
text: `[${toolName} Result]\n\n${textContent}`,
|
||||
} as Anthropic.Messages.TextBlockParam
|
||||
}
|
||||
return block
|
||||
})
|
||||
return { ...message, content: newContent }
|
||||
}
|
||||
return message
|
||||
})
|
||||
existingApiConversationHistory = conversationWithoutToolBlocks
|
||||
|
||||
// FIXME: remove tool use blocks altogether
|
||||
const existingApiConversationHistory: Anthropic.Messages.MessageParam[] = await this.getSavedApiConversationHistory()
|
||||
|
||||
// if the last message is an assistant message, we need to check if there's tool use since every tool use has to have a tool response
|
||||
// if there's no tool use and only a text block, then we can just add a user message
|
||||
@@ -1062,41 +1041,75 @@ export class Cline {
|
||||
|
||||
// Checkpoints
|
||||
|
||||
async saveCheckpoint() {
|
||||
const commitHash = await this.checkpointTracker?.commit() // silently fails for now
|
||||
if (commitHash) {
|
||||
// Start from the end and work backwards until we find a tool use or another message with a hash
|
||||
for (let i = this.clineMessages.length - 1; i >= 0; i--) {
|
||||
const message = this.clineMessages[i]
|
||||
if (message.lastCheckpointHash) {
|
||||
// Found a message with a hash, so we can stop
|
||||
break
|
||||
}
|
||||
// Update this message with a hash
|
||||
message.lastCheckpointHash = commitHash
|
||||
|
||||
// We only care about adding the hash to the last tool use (we don't want to add this hash to every prior message ie for tasks pre-checkpoint)
|
||||
const isToolUse =
|
||||
message.say === "tool" ||
|
||||
message.ask === "tool" ||
|
||||
message.say === "command" ||
|
||||
message.ask === "command" ||
|
||||
message.say === "completion_result" ||
|
||||
message.ask === "completion_result" ||
|
||||
message.ask === "followup" ||
|
||||
message.say === "use_mcp_server" ||
|
||||
message.ask === "use_mcp_server" ||
|
||||
message.say === "browser_action" ||
|
||||
message.say === "browser_action_launch" ||
|
||||
message.ask === "browser_action_launch"
|
||||
|
||||
if (isToolUse) {
|
||||
break
|
||||
}
|
||||
async saveCheckpoint(isAttemptCompletionMessage: boolean = false) {
|
||||
// Set isCheckpointCheckedOut to false for all checkpoint_created messages
|
||||
this.clineMessages.forEach((message) => {
|
||||
if (message.say === "checkpoint_created") {
|
||||
message.isCheckpointCheckedOut = false
|
||||
}
|
||||
})
|
||||
|
||||
if (!isAttemptCompletionMessage) {
|
||||
// For non-attempt completion we just say checkpoints
|
||||
await this.say("checkpoint_created")
|
||||
this.checkpointTracker?.commit().then(async (commitHash) => {
|
||||
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
|
||||
if (lastCheckpointMessage) {
|
||||
lastCheckpointMessage.lastCheckpointHash = commitHash
|
||||
await this.saveClineMessages()
|
||||
}
|
||||
}) // silently fails for now
|
||||
|
||||
//
|
||||
} else {
|
||||
// attempt completion requires checkpoint to be sync so that we can present button after attempt_completion
|
||||
const commitHash = await this.checkpointTracker?.commit()
|
||||
// For attempt_completion, find the last completion_result message and set its checkpoint hash. This will be used to present the 'see new changes' button
|
||||
const lastCompletionResultMessage = findLast(
|
||||
this.clineMessages,
|
||||
(m) => m.say === "completion_result" || m.ask === "completion_result",
|
||||
)
|
||||
if (lastCompletionResultMessage) {
|
||||
lastCompletionResultMessage.lastCheckpointHash = commitHash
|
||||
await this.saveClineMessages()
|
||||
}
|
||||
// Save the updated messages
|
||||
await this.saveClineMessages()
|
||||
}
|
||||
|
||||
// if (commitHash) {
|
||||
|
||||
// Previously we checkpointed every message, but this is excessive and unnecessary.
|
||||
// // Start from the end and work backwards until we find a tool use or another message with a hash
|
||||
// for (let i = this.clineMessages.length - 1; i >= 0; i--) {
|
||||
// const message = this.clineMessages[i]
|
||||
// if (message.lastCheckpointHash) {
|
||||
// // Found a message with a hash, so we can stop
|
||||
// break
|
||||
// }
|
||||
// // Update this message with a hash
|
||||
// message.lastCheckpointHash = commitHash
|
||||
|
||||
// // We only care about adding the hash to the last tool use (we don't want to add this hash to every prior message ie for tasks pre-checkpoint)
|
||||
// const isToolUse =
|
||||
// message.say === "tool" ||
|
||||
// message.ask === "tool" ||
|
||||
// message.say === "command" ||
|
||||
// message.ask === "command" ||
|
||||
// message.say === "completion_result" ||
|
||||
// message.ask === "completion_result" ||
|
||||
// message.ask === "followup" ||
|
||||
// message.say === "use_mcp_server" ||
|
||||
// message.ask === "use_mcp_server" ||
|
||||
// message.say === "browser_action" ||
|
||||
// message.say === "browser_action_launch" ||
|
||||
// message.ask === "browser_action_launch"
|
||||
|
||||
// if (isToolUse) {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// // Save the updated messages
|
||||
// await this.saveClineMessages()
|
||||
// }
|
||||
}
|
||||
|
||||
// Tools
|
||||
@@ -1220,14 +1233,21 @@ export class Cline {
|
||||
throw new Error("MCP hub not available")
|
||||
}
|
||||
|
||||
let systemPrompt = await SYSTEM_PROMPT(
|
||||
cwd,
|
||||
this.api.getModel().info.supportsComputerUse ?? false,
|
||||
mcpHub,
|
||||
this.browserSettings,
|
||||
)
|
||||
const disableBrowserTool = vscode.workspace.getConfiguration("cline").get<boolean>("disableBrowserTool") ?? false
|
||||
const modelSupportsComputerUse = this.api.getModel().info.supportsComputerUse ?? false
|
||||
|
||||
const supportsComputerUse = modelSupportsComputerUse && !disableBrowserTool // only enable computer use if the model supports it and the user hasn't disabled it
|
||||
|
||||
let systemPrompt = await SYSTEM_PROMPT(cwd, supportsComputerUse, mcpHub, this.browserSettings)
|
||||
|
||||
let settingsCustomInstructions = this.customInstructions?.trim()
|
||||
const preferredLanguage = getLanguageKey(
|
||||
vscode.workspace.getConfiguration("cline").get<LanguageDisplay>("preferredLanguage"),
|
||||
)
|
||||
const preferredLanguageInstructions =
|
||||
preferredLanguage && preferredLanguage !== DEFAULT_LANGUAGE_SETTINGS
|
||||
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
|
||||
: ""
|
||||
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
let clineRulesFileInstructions: string | undefined
|
||||
if (await fileExistsAtPath(clineRulesFilePath)) {
|
||||
@@ -1247,9 +1267,19 @@ export class Cline {
|
||||
clineIgnoreInstructions = `# .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${clineIgnoreContent}\n.clineignore`
|
||||
}
|
||||
|
||||
if (settingsCustomInstructions || clineRulesFileInstructions) {
|
||||
if (
|
||||
settingsCustomInstructions ||
|
||||
clineRulesFileInstructions ||
|
||||
clineIgnoreInstructions ||
|
||||
preferredLanguageInstructions
|
||||
) {
|
||||
// altering the system prompt mid-task will break the prompt cache, but in the grand scheme this will not change often so it's better to not pollute user messages with it the way we have to with <potentially relevant details>
|
||||
systemPrompt += addUserInstructions(settingsCustomInstructions, clineRulesFileInstructions, clineIgnoreInstructions)
|
||||
systemPrompt += addUserInstructions(
|
||||
settingsCustomInstructions,
|
||||
clineRulesFileInstructions,
|
||||
clineIgnoreInstructions,
|
||||
preferredLanguageInstructions,
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1500,37 +1530,44 @@ export class Cline {
|
||||
this.didAlreadyUseTool = true
|
||||
}
|
||||
|
||||
// The user can approve, reject, or provide feedback (rejection). However the user may also send a message along with an approval, in which case we add a separate user message with this feedback.
|
||||
const pushAdditionalToolFeedback = (feedback?: string, images?: string[]) => {
|
||||
if (!feedback && !images) {
|
||||
return
|
||||
}
|
||||
const content = formatResponse.toolResult(
|
||||
`The user provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
|
||||
images,
|
||||
)
|
||||
if (typeof content === "string") {
|
||||
this.userMessageContent.push({
|
||||
type: "text",
|
||||
text: content,
|
||||
})
|
||||
} else {
|
||||
this.userMessageContent.push(...content)
|
||||
}
|
||||
}
|
||||
|
||||
const askApproval = async (type: ClineAsk, partialMessage?: string) => {
|
||||
const { response, text, images } = await this.ask(type, partialMessage, false)
|
||||
if (response !== "yesButtonClicked") {
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
pushToolResult(formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images))
|
||||
// this.userMessageContent.push({
|
||||
// type: "text",
|
||||
// text: `${toolDescription()}`,
|
||||
// })
|
||||
// this.toolResults.push({
|
||||
// type: "tool_result",
|
||||
// tool_use_id: toolUseId,
|
||||
// content: this.formatToolResponseWithImages(
|
||||
// await this.formatToolDeniedFeedback(text),
|
||||
// images
|
||||
// ),
|
||||
// })
|
||||
this.didRejectTool = true
|
||||
return false
|
||||
}
|
||||
// User pressed reject button or responded with a message, which we treat as a rejection
|
||||
pushToolResult(formatResponse.toolDenied())
|
||||
// this.toolResults.push({
|
||||
// type: "tool_result",
|
||||
// tool_use_id: toolUseId,
|
||||
// content: await this.formatToolDenied(),
|
||||
// })
|
||||
this.didRejectTool = true
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
}
|
||||
this.didRejectTool = true // Prevent further tool uses in this message
|
||||
return false
|
||||
} else {
|
||||
// User hit the approve button, and may have provided feedback
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const showNotificationForApprovalIfAutoApprovalEnabled = (message: string) => {
|
||||
@@ -1601,7 +1638,7 @@ export class Cline {
|
||||
if (!accessAllowed) {
|
||||
await this.say("clineignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1705,21 +1742,21 @@ export class Cline {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError(block.name, "path"))
|
||||
await this.diffViewProvider.reset()
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
if (block.name === "replace_in_file" && !diff) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("replace_in_file", "diff"))
|
||||
await this.diffViewProvider.reset()
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
if (block.name === "write_to_file" && !content) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("write_to_file", "content"))
|
||||
await this.diffViewProvider.reset()
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1769,30 +1806,29 @@ export class Cline {
|
||||
let didApprove = true
|
||||
const { response, text, images } = await this.ask("tool", completeMessage, false)
|
||||
if (response !== "yesButtonClicked") {
|
||||
// User either sent a message or pressed reject button
|
||||
// TODO: add similar context for other tool denial responses, to emphasize ie that a command was not run
|
||||
const fileDeniedNote = fileExists
|
||||
? "The file was not updated, and maintains its original contents."
|
||||
: "The file was not created."
|
||||
if (response === "messageResponse") {
|
||||
pushToolResult(`The user denied this operation. ${fileDeniedNote}`)
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
}
|
||||
this.didRejectTool = true
|
||||
didApprove = false
|
||||
} else {
|
||||
// User hit the approve button, and may have provided feedback
|
||||
if (text || images?.length) {
|
||||
pushAdditionalToolFeedback(text, images)
|
||||
await this.say("user_feedback", text, images)
|
||||
pushToolResult(
|
||||
formatResponse.toolResult(
|
||||
`The user denied this operation. ${fileDeniedNote}\nThe user provided the following feedback:\n<feedback>\n${text}\n</feedback>`,
|
||||
images,
|
||||
),
|
||||
)
|
||||
this.didRejectTool = true
|
||||
didApprove = false
|
||||
} else {
|
||||
pushToolResult(`The user denied this operation. ${fileDeniedNote}`)
|
||||
this.didRejectTool = true
|
||||
didApprove = false
|
||||
}
|
||||
}
|
||||
|
||||
if (!didApprove) {
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1841,14 +1877,16 @@ export class Cline {
|
||||
}
|
||||
|
||||
await this.diffViewProvider.reset()
|
||||
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("writing file", error)
|
||||
await this.diffViewProvider.revertChanges()
|
||||
await this.diffViewProvider.reset()
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1876,7 +1914,7 @@ export class Cline {
|
||||
if (!relPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1884,7 +1922,7 @@ export class Cline {
|
||||
if (!accessAllowed) {
|
||||
await this.say("clineignore_error", relPath)
|
||||
pushToolResult(formatResponse.toolError(formatResponse.clineIgnoreError(relPath)))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1905,19 +1943,18 @@ export class Cline {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
// now execute the tool like normal
|
||||
const content = await extractTextFromFile(absolutePath)
|
||||
pushToolResult(content)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("reading file", error)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1947,7 +1984,7 @@ export class Cline {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("list_files", "path"))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -1977,17 +2014,16 @@ export class Cline {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
pushToolResult(result)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("listing files", error)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2015,7 +2051,7 @@ export class Cline {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("list_code_definition_names", "path"))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2042,17 +2078,16 @@ export class Cline {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
pushToolResult(result)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("parsing source code definitions", error)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2084,13 +2119,13 @@ export class Cline {
|
||||
if (!relDirPath) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "path"))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
if (!regex) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("search_files", "regex"))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2119,17 +2154,16 @@ export class Cline {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "tool")
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
pushToolResult(results)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("searching files", error)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2188,7 +2222,7 @@ export class Cline {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "url"))
|
||||
await this.browserSession.closeBrowser()
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2204,7 +2238,6 @@ export class Cline {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "browser_action_launch")
|
||||
const didApprove = await askApproval("browser_action_launch", url)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2223,7 +2256,7 @@ export class Cline {
|
||||
await this.sayAndCreateMissingParamError("browser_action", "coordinate"),
|
||||
)
|
||||
await this.browserSession.closeBrowser()
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break // can't be within an inner switch
|
||||
}
|
||||
}
|
||||
@@ -2232,7 +2265,7 @@ export class Cline {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("browser_action", "text"))
|
||||
await this.browserSession.closeBrowser()
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2281,7 +2314,7 @@ export class Cline {
|
||||
browserActionResult.screenshot ? [browserActionResult.screenshot] : [],
|
||||
),
|
||||
)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
case "close":
|
||||
pushToolResult(
|
||||
@@ -2289,17 +2322,16 @@ export class Cline {
|
||||
`The browser has been closed. You may now proceed to using other tools.`,
|
||||
),
|
||||
)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await this.browserSession.closeBrowser() // if any error occurs, the browser session is terminated
|
||||
await handleError("executing browser action", error)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2327,7 +2359,7 @@ export class Cline {
|
||||
if (!command) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("execute_command", "command"))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
if (!requiresApprovalRaw) {
|
||||
@@ -2335,7 +2367,7 @@ export class Cline {
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("execute_command", "requires_approval"),
|
||||
)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2346,7 +2378,7 @@ export class Cline {
|
||||
pushToolResult(
|
||||
formatResponse.toolError(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess)),
|
||||
)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -2368,7 +2400,6 @@ export class Cline {
|
||||
`${this.shouldAutoApproveTool(block.name) && requiresApproval ? COMMAND_REQ_APP_STRING : ""}`, // ugly hack until we refactor combineCommandSequences
|
||||
)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2397,12 +2428,14 @@ export class Cline {
|
||||
this.providerRef.deref()?.workspaceTracker?.populateFilePaths()
|
||||
|
||||
pushToolResult(result)
|
||||
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("executing command", error)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2432,13 +2465,13 @@ export class Cline {
|
||||
if (!server_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "server_name"))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
if (!tool_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
// arguments are optional, but if they are provided they must be valid JSON
|
||||
@@ -2462,7 +2495,7 @@ export class Cline {
|
||||
formatResponse.invalidMcpToolArgumentError(server_name, tool_name),
|
||||
),
|
||||
)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2490,7 +2523,6 @@ export class Cline {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
|
||||
const didApprove = await askApproval("use_mcp_server", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2519,12 +2551,14 @@ export class Cline {
|
||||
.join("\n\n") || "(No response)"
|
||||
await this.say("mcp_server_response", toolResultPretty)
|
||||
pushToolResult(formatResponse.toolResult(toolResultPretty))
|
||||
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("executing MCP tool", error)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2552,13 +2586,13 @@ export class Cline {
|
||||
if (!server_name) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "server_name"))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
if (!uri) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("access_mcp_resource", "uri"))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2579,7 +2613,6 @@ export class Cline {
|
||||
this.removeLastPartialMessageIfExistsWithType("say", "use_mcp_server")
|
||||
const didApprove = await askApproval("use_mcp_server", completeMessage)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2599,12 +2632,12 @@ export class Cline {
|
||||
.join("\n\n") || "(Empty response)"
|
||||
await this.say("mcp_server_response", resourceResultPretty)
|
||||
pushToolResult(formatResponse.toolResult(resourceResultPretty))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("accessing MCP resource", error)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2618,7 +2651,7 @@ export class Cline {
|
||||
if (!question) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("ask_followup_question", "question"))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2633,12 +2666,12 @@ export class Cline {
|
||||
const { text, images } = await this.ask("followup", question, false)
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("asking question", error)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2654,7 +2687,7 @@ export class Cline {
|
||||
if (!response) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("plan_mode_response", "response"))
|
||||
// await this.saveCheckpoint()
|
||||
//
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2667,28 +2700,39 @@ export class Cline {
|
||||
// }
|
||||
|
||||
this.isAwaitingPlanResponse = true
|
||||
const { text, images } = await this.ask("plan_mode_response", response, false)
|
||||
let { text, images } = await this.ask("plan_mode_response", response, false)
|
||||
this.isAwaitingPlanResponse = false
|
||||
|
||||
// webview invoke sendMessage will send this marker in order to put webview into the proper state (responding to an ask) and as a flag to extension that the user switched to ACT mode.
|
||||
if (text === "PLAN_MODE_TOGGLE_RESPONSE") {
|
||||
text = ""
|
||||
}
|
||||
|
||||
if (this.didRespondToPlanAskBySwitchingMode) {
|
||||
// await this.say("user_feedback", text ?? "", images)
|
||||
pushToolResult(
|
||||
formatResponse.toolResult(
|
||||
`[The user has switched to ACT MODE, so you may now proceed with the task.]`,
|
||||
`[The user has switched to ACT MODE, so you may now proceed with the task.]` +
|
||||
(text
|
||||
? `\n\nThe user also provided the following message when switching to ACT MODE:\n<user_message>\n${text}\n</user_message>`
|
||||
: ""),
|
||||
images,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
// if we didn't switch to ACT MODE, then we can just send the user_feedback message
|
||||
pushToolResult(formatResponse.toolResult(`<user_message>\n${text}\n</user_message>`, images))
|
||||
}
|
||||
|
||||
// await this.saveCheckpoint()
|
||||
if (text || images?.length) {
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
}
|
||||
|
||||
//
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("responding to inquiry", error)
|
||||
// await this.saveCheckpoint()
|
||||
//
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2749,7 +2793,7 @@ export class Cline {
|
||||
// last message is completion_result
|
||||
// we have command string, which means we have the result as well, so finish it (doesnt have to exist yet)
|
||||
await this.say("completion_result", removeClosingTag("result", result), undefined, false)
|
||||
await this.saveCheckpoint()
|
||||
await this.saveCheckpoint(true)
|
||||
await addNewChangesFlagToLastCompletionResultMessage()
|
||||
await this.ask("command", removeClosingTag("command", command), block.partial).catch(
|
||||
() => {},
|
||||
@@ -2769,7 +2813,6 @@ export class Cline {
|
||||
if (!result) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("attempt_completion", "result"))
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
@@ -2786,31 +2829,29 @@ export class Cline {
|
||||
if (lastMessage && lastMessage.ask !== "command") {
|
||||
// havent sent a command message yet so first send completion_result then command
|
||||
await this.say("completion_result", result, undefined, false)
|
||||
await this.saveCheckpoint()
|
||||
await this.saveCheckpoint(true)
|
||||
await addNewChangesFlagToLastCompletionResultMessage()
|
||||
} else {
|
||||
// we already sent a command message, meaning the complete completion message has also been sent
|
||||
await this.saveCheckpoint()
|
||||
await this.saveCheckpoint(true)
|
||||
}
|
||||
|
||||
// complete command message
|
||||
const didApprove = await askApproval("command", command)
|
||||
if (!didApprove) {
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
const [userRejected, execCommandResult] = await this.executeCommandTool(command!)
|
||||
if (userRejected) {
|
||||
this.didRejectTool = true
|
||||
pushToolResult(execCommandResult)
|
||||
await this.saveCheckpoint()
|
||||
break
|
||||
}
|
||||
// user didn't reject, but the command may have output
|
||||
commandResult = execCommandResult
|
||||
} else {
|
||||
await this.say("completion_result", result, undefined, false)
|
||||
await this.saveCheckpoint()
|
||||
await this.saveCheckpoint(true)
|
||||
await addNewChangesFlagToLastCompletionResultMessage()
|
||||
}
|
||||
|
||||
@@ -2844,12 +2885,12 @@ export class Cline {
|
||||
})
|
||||
this.userMessageContent.push(...toolResults)
|
||||
|
||||
// await this.saveCheckpoint()
|
||||
//
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("attempting completion", error)
|
||||
await this.saveCheckpoint()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2908,7 +2949,7 @@ export class Cline {
|
||||
"mistake_limit_reached",
|
||||
this.api.getModel().id.includes("claude")
|
||||
? `This may indicate a failure in his thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. "Try breaking down the task into smaller steps").`
|
||||
: "Cline uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use Claude 3.5 Sonnet for its advanced agentic coding capabilities.",
|
||||
: "Cline uses complex prompts and iterative task execution that may be challenging for less capable models. For best results, it's recommended to use Claude 3.7 Sonnet for its advanced agentic coding capabilities.",
|
||||
)
|
||||
if (response === "messageResponse") {
|
||||
userContent.push(
|
||||
@@ -2945,6 +2986,12 @@ export class Cline {
|
||||
// get previous api req's index to check token usage and determine if we need to truncate conversation history
|
||||
const previousApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
|
||||
|
||||
// Save checkpoint if this is the first API request
|
||||
const isFirstRequest = this.clineMessages.filter((m) => m.say === "api_req_started").length === 0
|
||||
if (isFirstRequest) {
|
||||
await this.say("checkpoint_created") // no hash since we need to wait for CheckpointTracker to be initialized
|
||||
}
|
||||
|
||||
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
|
||||
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
|
||||
await this.say(
|
||||
@@ -2968,6 +3015,16 @@ export class Cline {
|
||||
}
|
||||
}
|
||||
|
||||
// Now that checkpoint tracker is initialized, update the dummy checkpoint_created message with the commit hash. (This is necessary since we use the API request loading as an opportunity to initialize the checkpoint tracker, which can take some time)
|
||||
if (isFirstRequest) {
|
||||
const commitHash = await this.checkpointTracker?.commit()
|
||||
const lastCheckpointMessage = findLast(this.clineMessages, (m) => m.say === "checkpoint_created")
|
||||
if (lastCheckpointMessage) {
|
||||
lastCheckpointMessage.lastCheckpointHash = commitHash
|
||||
await this.saveClineMessages()
|
||||
}
|
||||
}
|
||||
|
||||
const [parsedUserContent, environmentDetails] = await this.loadContext(userContent, includeFileDetails)
|
||||
userContent = parsedUserContent
|
||||
// add environment details as its own text block, separate from tool results
|
||||
@@ -3005,7 +3062,13 @@ export class Cline {
|
||||
cacheReads: cacheReadTokens,
|
||||
cost:
|
||||
totalCost ??
|
||||
calculateApiCost(this.api.getModel().info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens),
|
||||
calculateApiCostAnthropic(
|
||||
this.api.getModel().info,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cacheWriteTokens,
|
||||
cacheReadTokens,
|
||||
),
|
||||
cancelReason,
|
||||
streamingFailedMessage,
|
||||
} satisfies ClineApiReqInfo)
|
||||
@@ -3071,7 +3134,6 @@ export class Cline {
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
if (!chunk) {
|
||||
// Sometimes chunk is undefined, no idea that can cause it, but this workaround seems to fix it
|
||||
continue
|
||||
}
|
||||
switch (chunk.type) {
|
||||
|
||||
@@ -7,27 +7,32 @@ import fs from "fs/promises"
|
||||
import { extractTextFromFile } from "../../integrations/misc/extract-text"
|
||||
import { isBinaryFile } from "isbinaryfile"
|
||||
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
|
||||
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
|
||||
import { getCommitInfo } from "../../utils/git"
|
||||
import { getWorkingState } from "../../utils/git"
|
||||
|
||||
export function openMention(mention?: string): void {
|
||||
if (!mention) {
|
||||
return
|
||||
}
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mention.startsWith("/")) {
|
||||
const relPath = mention.slice(1)
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
const absPath = path.resolve(cwd, relPath)
|
||||
if (mention.endsWith("/")) {
|
||||
vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
|
||||
// vscode.commands.executeCommand("vscode.openFolder", , { forceNewWindow: false }) opens in new window
|
||||
} else {
|
||||
openFile(absPath)
|
||||
}
|
||||
} else if (mention === "problems") {
|
||||
vscode.commands.executeCommand("workbench.actions.view.problems")
|
||||
} else if (mention === "terminal") {
|
||||
vscode.commands.executeCommand("workbench.action.terminal.focus")
|
||||
} else if (mention.startsWith("http")) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(mention))
|
||||
}
|
||||
@@ -46,6 +51,12 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
|
||||
: `'${mentionPath}' (see below for file content)`
|
||||
} else if (mention === "problems") {
|
||||
return `Workspace Problems (see below for diagnostics)`
|
||||
} else if (mention === "terminal") {
|
||||
return `Terminal Output (see below for output)`
|
||||
} else if (mention === "git-changes") {
|
||||
return `Working directory changes (see below for details)`
|
||||
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
|
||||
return `Git commit '${mention}' (see below for commit info)`
|
||||
}
|
||||
return match
|
||||
})
|
||||
@@ -99,6 +110,27 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
|
||||
}
|
||||
} else if (mention === "terminal") {
|
||||
try {
|
||||
const terminalOutput = await getLatestTerminalOutput()
|
||||
parsedText += `\n\n<terminal_output>\n${terminalOutput}\n</terminal_output>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<terminal_output>\nError fetching terminal output: ${error.message}\n</terminal_output>`
|
||||
}
|
||||
} else if (mention === "git-changes") {
|
||||
try {
|
||||
const workingState = await getWorkingState(cwd)
|
||||
parsedText += `\n\n<git_working_state>\n${workingState}\n</git_working_state>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<git_working_state>\nError fetching working state: ${error.message}\n</git_working_state>`
|
||||
}
|
||||
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
|
||||
try {
|
||||
const commitInfo = await getCommitInfo(mention, cwd)
|
||||
parsedText += `\n\n<git_commit hash="${mention}">\n${commitInfo}\n</git_commit>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<git_commit hash="${mention}">\nError fetching commit info: ${error.message}\n</git_commit>`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,6 @@ import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreCo
|
||||
export const formatResponse = {
|
||||
toolDenied: () => `The user denied this operation.`,
|
||||
|
||||
toolDeniedWithFeedback: (feedback?: string) =>
|
||||
`The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`,
|
||||
|
||||
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
|
||||
|
||||
clineIgnoreError: (path: string) =>
|
||||
|
||||
@@ -331,7 +331,24 @@ ${
|
||||
<access_mcp_resource>
|
||||
<server_name>weather-server</server_name>
|
||||
<uri>weather://san-francisco/current</uri>
|
||||
</access_mcp_resource>`
|
||||
</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>
|
||||
<server_name>github.com/modelcontextprotocol/servers/tree/main/src/github</server_name>
|
||||
<tool_name>create_issue</tool_name>
|
||||
<arguments>
|
||||
{
|
||||
"owner": "octocat",
|
||||
"repo": "hello-world",
|
||||
"title": "Found a bug",
|
||||
"body": "I'm having a problem with this.",
|
||||
"labels": ["bug", "help wanted"],
|
||||
"assignees": ["octocat"]
|
||||
}
|
||||
</arguments>
|
||||
</use_mcp_tool>`
|
||||
: ""
|
||||
}
|
||||
|
||||
@@ -750,7 +767,7 @@ IMPORTANT: Regardless of what else you see in the MCP settings file, you must de
|
||||
|
||||
(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.
|
||||
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?"
|
||||
|
||||
@@ -865,9 +882,10 @@ In each user message, the environment_details will specify the current mode. The
|
||||
## What is PLAN MODE?
|
||||
|
||||
- While you are usually in ACT MODE, the user may switch to PLAN MODE in order to have a back and forth with you to plan how to best accomplish the task.
|
||||
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task.
|
||||
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task.
|
||||
- When starting in PLAN MODE, depending on the user's request, you may need to do some information gathering e.g. using read_file or search_files to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. You may return mermaid diagrams to visually display your understanding.
|
||||
- Once you've gained more context about the user's request, you should architect a detailed plan for how you will accomplish the task. Returning mermaid diagrams may be helpful here as well.
|
||||
- Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
|
||||
- If at any point a mermaid diagram would make your plan clearer to help the user quickly see the structure, you are encouraged to include a Mermaid code block in the response. (Note: if you use colors in your mermaid diagrams, be sure to use high contrast colors so the text is readable.)
|
||||
- Finally once it seems like you've reached a good plan, ask the user to switch you back to ACT MODE to implement the solution.
|
||||
|
||||
====
|
||||
@@ -961,8 +979,12 @@ export function addUserInstructions(
|
||||
settingsCustomInstructions?: string,
|
||||
clineRulesFileInstructions?: string,
|
||||
clineIgnoreInstructions?: string,
|
||||
preferredLanguageInstructions?: string,
|
||||
) {
|
||||
let customInstructions = ""
|
||||
if (preferredLanguageInstructions) {
|
||||
customInstructions += preferredLanguageInstructions + "\n\n"
|
||||
}
|
||||
if (settingsCustomInstructions) {
|
||||
customInstructions += settingsCustomInstructions + "\n\n"
|
||||
}
|
||||
|
||||
+703
-125
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,8 @@ import { Logger } from "./services/logging/Logger"
|
||||
import { createClineAPI } from "./exports"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
|
||||
import assert from "node:assert"
|
||||
import { telemetryService } from "./services/telemetry/TelemetryService"
|
||||
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
@@ -188,5 +190,25 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// 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 } = process.env
|
||||
|
||||
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/**/*"))
|
||||
|
||||
watcher.onDidChange(({ scheme, path }) => {
|
||||
console.info(`${scheme} ${path} changed. Reloading VSCode...`)
|
||||
|
||||
vscode.commands.executeCommand("workbench.action.reloadWindow")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as vscode from "vscode"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import CheckpointTracker from "./CheckpointTracker"
|
||||
|
||||
export async function createTestEnvironment() {
|
||||
// Create temp directory structure
|
||||
const tempDir = path.join(os.tmpdir(), `checkpoint-test-${Date.now()}`)
|
||||
await fs.mkdir(tempDir, { recursive: true })
|
||||
|
||||
// Create storage path outside of working directory to avoid submodule issues
|
||||
const globalStoragePath = path.join(os.tmpdir(), `storage-${Date.now()}`)
|
||||
await fs.mkdir(globalStoragePath, { recursive: true })
|
||||
|
||||
// Create test file in a subdirectory
|
||||
const testDir = path.join(tempDir, "src")
|
||||
await fs.mkdir(testDir, { recursive: true })
|
||||
const testFilePath = path.join(testDir, "test.txt")
|
||||
|
||||
// Create .gitignore to prevent git from treating directories as submodules
|
||||
await fs.writeFile(path.join(tempDir, ".gitignore"), "storage/\n")
|
||||
|
||||
// Mock VS Code workspace
|
||||
const mockWorkspaceFolders = [
|
||||
{
|
||||
uri: { fsPath: tempDir },
|
||||
name: "test",
|
||||
index: 0,
|
||||
},
|
||||
]
|
||||
|
||||
const originalDescriptor = Object.getOwnPropertyDescriptor(vscode.workspace, "workspaceFolders")
|
||||
Object.defineProperty(vscode.workspace, "workspaceFolders", {
|
||||
get: () => mockWorkspaceFolders,
|
||||
})
|
||||
|
||||
// Mock findFiles to return no nested git repos
|
||||
const originalFindFiles = vscode.workspace.findFiles
|
||||
vscode.workspace.findFiles = async () => []
|
||||
|
||||
// Mock VS Code configuration
|
||||
const originalGetConfiguration = vscode.workspace.getConfiguration
|
||||
vscode.workspace.getConfiguration = () =>
|
||||
({
|
||||
get: (key: string) => (key === "enableCheckpoints" ? true : undefined),
|
||||
}) as any
|
||||
|
||||
return {
|
||||
tempDir,
|
||||
globalStoragePath,
|
||||
testFilePath,
|
||||
originalDescriptor,
|
||||
originalFindFiles,
|
||||
originalGetConfiguration,
|
||||
cleanup: async () => {
|
||||
// Restore VS Code mocks
|
||||
if (originalDescriptor) {
|
||||
Object.defineProperty(vscode.workspace, "workspaceFolders", originalDescriptor)
|
||||
}
|
||||
vscode.workspace.getConfiguration = originalGetConfiguration
|
||||
vscode.workspace.findFiles = originalFindFiles
|
||||
|
||||
// Clean up temp directories
|
||||
await fs.rm(tempDir, { recursive: true, force: true })
|
||||
await fs.rm(globalStoragePath, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTestTracker(globalStoragePath?: string, taskId = "test-task-1") {
|
||||
return await CheckpointTracker.create(taskId, globalStoragePath)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { expect } from "chai"
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { createTestEnvironment, createTestTracker } from "./Checkpoint-test-utils"
|
||||
|
||||
describe("Checkpoint Commit Operations", () => {
|
||||
let env: Awaited<ReturnType<typeof createTestEnvironment>>
|
||||
|
||||
beforeEach(async () => {
|
||||
env = await createTestEnvironment()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await env.cleanup()
|
||||
})
|
||||
|
||||
it("should create commit with single file changes", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
if (!tracker) {throw new Error("Failed to create tracker")}
|
||||
|
||||
// Create initial file
|
||||
await fs.writeFile(env.testFilePath, "initial content")
|
||||
|
||||
// Create first commit
|
||||
const firstCommit = await tracker.commit()
|
||||
expect(firstCommit).to.be.a("string").and.not.empty
|
||||
|
||||
// Modify file
|
||||
await fs.writeFile(env.testFilePath, "modified content")
|
||||
|
||||
// Create second commit
|
||||
const secondCommit = await tracker.commit()
|
||||
expect(secondCommit).to.be.a("string").and.not.empty
|
||||
expect(secondCommit).to.not.equal(firstCommit)
|
||||
|
||||
// Verify commits are different
|
||||
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
|
||||
expect(diffSet).to.have.lengthOf(1)
|
||||
expect(diffSet[0].before).to.equal("initial content")
|
||||
expect(diffSet[0].after).to.equal("modified content")
|
||||
})
|
||||
|
||||
it("should create commit with multiple file changes", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
if (!tracker) {throw new Error("Failed to create tracker")}
|
||||
|
||||
// Create initial files with newlines
|
||||
const testFile2Path = path.join(env.tempDir, "src", "test2.txt")
|
||||
await fs.writeFile(env.testFilePath, "file1 initial\n")
|
||||
await fs.writeFile(testFile2Path, "file2 initial\n")
|
||||
|
||||
// Create first commit
|
||||
const firstCommit = await tracker.commit()
|
||||
expect(firstCommit).to.be.a("string").and.not.empty
|
||||
|
||||
// Modify both files with newlines
|
||||
await fs.writeFile(env.testFilePath, "file1 modified\n")
|
||||
await fs.writeFile(testFile2Path, "file2 modified\n")
|
||||
|
||||
// Create second commit
|
||||
const secondCommit = await tracker.commit()
|
||||
expect(secondCommit).to.be.a("string").and.not.empty
|
||||
expect(secondCommit).to.not.equal(firstCommit)
|
||||
|
||||
// Get diff between commits
|
||||
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
|
||||
expect(diffSet).to.have.lengthOf(2)
|
||||
|
||||
// Sort diffSet by path for consistent ordering
|
||||
const sortedDiffs = diffSet.sort((a, b) => a.relativePath.localeCompare(b.relativePath))
|
||||
|
||||
// Verify file paths
|
||||
expect(sortedDiffs[0].relativePath).to.equal("src/test.txt")
|
||||
expect(sortedDiffs[1].relativePath).to.equal("src/test2.txt")
|
||||
|
||||
// Verify file contents
|
||||
expect(sortedDiffs[0].before).to.equal("file1 initial\nfile2 initial\n")
|
||||
expect(sortedDiffs[0].after).to.equal("file1 modified\nfile2 modified\n")
|
||||
})
|
||||
|
||||
it("should create commit when files are deleted", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
if (!tracker) {throw new Error("Failed to create tracker")}
|
||||
|
||||
// Create and commit initial file
|
||||
await fs.writeFile(env.testFilePath, "initial content")
|
||||
const firstCommit = await tracker.commit()
|
||||
expect(firstCommit).to.be.a("string").and.not.empty
|
||||
|
||||
// Delete file
|
||||
await fs.unlink(env.testFilePath)
|
||||
|
||||
// Create second commit
|
||||
const secondCommit = await tracker.commit()
|
||||
expect(secondCommit).to.be.a("string").and.not.empty
|
||||
expect(secondCommit).to.not.equal(firstCommit)
|
||||
|
||||
// Verify file deletion was committed
|
||||
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
|
||||
expect(diffSet).to.have.lengthOf(1)
|
||||
expect(diffSet[0].before).to.equal("initial content")
|
||||
expect(diffSet[0].after).to.equal("")
|
||||
})
|
||||
|
||||
it("should create empty commit when no changes", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
if (!tracker) {throw new Error("Failed to create tracker")}
|
||||
|
||||
// Create and commit initial file
|
||||
await fs.writeFile(env.testFilePath, "test content")
|
||||
const firstCommit = await tracker.commit()
|
||||
expect(firstCommit).to.be.a("string").and.not.empty
|
||||
|
||||
// Create commit without changes
|
||||
const secondCommit = await tracker.commit()
|
||||
expect(secondCommit).to.be.a("string").and.not.empty
|
||||
expect(secondCommit).to.not.equal(firstCommit)
|
||||
|
||||
// Verify no changes between commits
|
||||
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
|
||||
expect(diffSet).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it("should handle files in nested directories", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
if (!tracker) {throw new Error("Failed to create tracker")}
|
||||
|
||||
// Create nested directory structure
|
||||
const nestedDir = path.join(env.tempDir, "src", "deep", "nested")
|
||||
await fs.mkdir(nestedDir, { recursive: true })
|
||||
const nestedFilePath = path.join(nestedDir, "nested.txt")
|
||||
|
||||
// Create and commit file in nested directory
|
||||
await fs.writeFile(nestedFilePath, "nested content")
|
||||
const firstCommit = await tracker.commit()
|
||||
expect(firstCommit).to.be.a("string").and.not.empty
|
||||
|
||||
// Modify nested file
|
||||
await fs.writeFile(nestedFilePath, "modified nested content")
|
||||
|
||||
// Create second commit
|
||||
const secondCommit = await tracker.commit()
|
||||
expect(secondCommit).to.be.a("string").and.not.empty
|
||||
|
||||
// Verify changes were committed
|
||||
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
|
||||
expect(diffSet).to.have.lengthOf(1)
|
||||
expect(diffSet[0].relativePath).to.equal("src/deep/nested/nested.txt")
|
||||
expect(diffSet[0].before).to.equal("nested content")
|
||||
expect(diffSet[0].after).to.equal("modified nested content")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { expect } from "chai"
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import { createTestEnvironment, createTestTracker } from "./Checkpoint-test-utils"
|
||||
import CheckpointTracker from "./CheckpointTracker"
|
||||
|
||||
describe("Checkpoint Creation", () => {
|
||||
let env: Awaited<ReturnType<typeof createTestEnvironment>>
|
||||
|
||||
beforeEach(async () => {
|
||||
env = await createTestEnvironment()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await env.cleanup()
|
||||
})
|
||||
|
||||
it("should create a new checkpoint tracker", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
expect(tracker).to.not.be.undefined
|
||||
expect(tracker).to.be.instanceOf(CheckpointTracker)
|
||||
|
||||
// Verify shadow git config
|
||||
const configWorkTree = await tracker?.getShadowGitConfigWorkTree()
|
||||
expect(configWorkTree).to.not.be.undefined
|
||||
})
|
||||
|
||||
it("should throw error when globalStoragePath is missing", async () => {
|
||||
try {
|
||||
await createTestTracker(undefined)
|
||||
expect.fail("Expected error was not thrown")
|
||||
} catch (error: any) {
|
||||
expect(error.message).to.equal("Global storage path is required to create a checkpoint tracker")
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import { expect } from "chai"
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import fs from "fs/promises"
|
||||
import { createTestEnvironment, createTestTracker } from "./Checkpoint-test-utils"
|
||||
|
||||
describe("Checkpoint Diff Operations", () => {
|
||||
let env: Awaited<ReturnType<typeof createTestEnvironment>>
|
||||
|
||||
beforeEach(async () => {
|
||||
env = await createTestEnvironment()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await env.cleanup()
|
||||
})
|
||||
|
||||
it("should detect file changes between commits", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
if (!tracker) {throw new Error("Failed to create tracker")}
|
||||
|
||||
// Create initial file
|
||||
await fs.writeFile(env.testFilePath, "initial content")
|
||||
|
||||
// Create first checkpoint
|
||||
const firstCommit = await tracker.commit()
|
||||
expect(firstCommit).to.not.be.undefined
|
||||
|
||||
// Modify file
|
||||
await fs.writeFile(env.testFilePath, "modified content")
|
||||
|
||||
// Create second checkpoint
|
||||
const secondCommit = await tracker.commit()
|
||||
expect(secondCommit).to.not.be.undefined
|
||||
|
||||
// Get diff between commits
|
||||
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
|
||||
|
||||
// Verify diff results
|
||||
expect(diffSet).to.have.lengthOf(1)
|
||||
expect(diffSet[0].relativePath).to.equal("src/test.txt")
|
||||
expect(diffSet[0].before).to.equal("initial content")
|
||||
expect(diffSet[0].after).to.equal("modified content")
|
||||
})
|
||||
|
||||
it("should detect changes between commit and working directory", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
if (!tracker) {throw new Error("Failed to create tracker")}
|
||||
|
||||
// Create initial file
|
||||
await fs.writeFile(env.testFilePath, "initial content")
|
||||
|
||||
// Create checkpoint
|
||||
const commit = await tracker.commit()
|
||||
expect(commit).to.not.be.undefined
|
||||
|
||||
// Modify file without committing
|
||||
await fs.writeFile(env.testFilePath, "working directory changes")
|
||||
|
||||
// Get diff between commit and working directory
|
||||
const diffSet = await tracker.getDiffSet(commit)
|
||||
|
||||
// Verify diff results
|
||||
expect(diffSet).to.have.lengthOf(1)
|
||||
expect(diffSet[0].relativePath).to.equal("src/test.txt")
|
||||
expect(diffSet[0].before).to.equal("initial content")
|
||||
expect(diffSet[0].after).to.equal("working directory changes")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
import { expect } from "chai"
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import fs from "fs/promises"
|
||||
import * as vscode from "vscode"
|
||||
import { createTestEnvironment, createTestTracker } from "./Checkpoint-test-utils"
|
||||
|
||||
describe("Checkpoint Disabled State", () => {
|
||||
let env: Awaited<ReturnType<typeof createTestEnvironment>>
|
||||
let originalGetConfiguration: typeof vscode.workspace.getConfiguration
|
||||
|
||||
beforeEach(async () => {
|
||||
env = await createTestEnvironment()
|
||||
originalGetConfiguration = vscode.workspace.getConfiguration
|
||||
|
||||
// Mock VS Code configuration to disable checkpoints
|
||||
vscode.workspace.getConfiguration = () =>
|
||||
({
|
||||
get: (key: string) => (key === "enableCheckpoints" ? false : undefined),
|
||||
}) as any
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await env.cleanup()
|
||||
// Restore original configuration
|
||||
vscode.workspace.getConfiguration = originalGetConfiguration
|
||||
})
|
||||
|
||||
it("should return undefined when creating tracker", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
expect(tracker).to.be.undefined
|
||||
})
|
||||
|
||||
it("should allow re-enabling checkpoints", async () => {
|
||||
// First verify disabled state
|
||||
const disabledTracker = await createTestTracker(env.globalStoragePath)
|
||||
expect(disabledTracker).to.be.undefined
|
||||
|
||||
// Mock configuration to enable checkpoints
|
||||
vscode.workspace.getConfiguration = () =>
|
||||
({
|
||||
get: (key: string) => (key === "enableCheckpoints" ? true : undefined),
|
||||
}) as any
|
||||
|
||||
// Verify tracker can be created when enabled
|
||||
const enabledTracker = await createTestTracker(env.globalStoragePath)
|
||||
expect(enabledTracker).to.not.be.undefined
|
||||
|
||||
// Verify operations work
|
||||
if (!enabledTracker) {throw new Error("Failed to create tracker")}
|
||||
await fs.writeFile(env.testFilePath, "test content")
|
||||
const commit = await enabledTracker.commit()
|
||||
expect(commit).to.be.a("string").and.not.empty
|
||||
})
|
||||
|
||||
it("should prevent operations when disabled mid-session", async () => {
|
||||
// Start with checkpoints enabled
|
||||
vscode.workspace.getConfiguration = () =>
|
||||
({
|
||||
get: (key: string) => (key === "enableCheckpoints" ? true : undefined),
|
||||
}) as any
|
||||
|
||||
// Create tracker and initial commit
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
expect(tracker).to.not.be.undefined
|
||||
if (!tracker) {throw new Error("Failed to create tracker")}
|
||||
|
||||
await fs.writeFile(env.testFilePath, "initial content")
|
||||
const firstCommit = await tracker.commit()
|
||||
expect(firstCommit).to.be.a("string").and.not.empty
|
||||
|
||||
// Disable checkpoints
|
||||
vscode.workspace.getConfiguration = () =>
|
||||
({
|
||||
get: (key: string) => (key === "enableCheckpoints" ? false : undefined),
|
||||
}) as any
|
||||
|
||||
// Verify new tracker cannot be created
|
||||
const disabledTracker = await createTestTracker(env.globalStoragePath)
|
||||
expect(disabledTracker).to.be.undefined
|
||||
|
||||
// Verify existing tracker still works
|
||||
// This is expected behavior since the tracker was created when enabled
|
||||
await fs.writeFile(env.testFilePath, "modified content")
|
||||
const secondCommit = await tracker.commit()
|
||||
expect(secondCommit).to.be.a("string").and.not.empty
|
||||
expect(secondCommit).to.not.equal(firstCommit)
|
||||
|
||||
// Verify diffs still work on existing tracker
|
||||
const diffSet = await tracker.getDiffSet(firstCommit, secondCommit)
|
||||
expect(diffSet).to.have.lengthOf(1)
|
||||
expect(diffSet[0].before).to.equal("initial content")
|
||||
expect(diffSet[0].after).to.equal("modified content")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,335 @@
|
||||
import fs from "fs/promises"
|
||||
import { join } from "path"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { GIT_DISABLED_SUFFIX } from "./CheckpointGitOperations"
|
||||
|
||||
/**
|
||||
* CheckpointExclusions Module
|
||||
*
|
||||
* A specialized module within Cline's Checkpoints system that manages file exclusion rules
|
||||
* for the checkpoint tracking process. It provides:
|
||||
*
|
||||
* File Filtering:
|
||||
* - File types (build artifacts, media, cache files, etc.)
|
||||
* - Git LFS patterns from workspace
|
||||
* - Environment and configuration files
|
||||
* - Temporary and cache files
|
||||
*
|
||||
* Pattern Management:
|
||||
* - Extensible category-based pattern system
|
||||
* - Comprehensive file type coverage
|
||||
* - Easy pattern updates and maintenance
|
||||
*
|
||||
* Git Integration:
|
||||
* - Seamless integration with Git's exclude mechanism
|
||||
* - Support for workspace-specific LFS patterns
|
||||
* - Automatic pattern updates during checkpoints
|
||||
*
|
||||
* The module ensures efficient checkpoint creation by preventing unnecessary tracking
|
||||
* of large files, binary files, and temporary artifacts while maintaining a clean
|
||||
* and organized checkpoint history.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface representing the result of a file exclusion check
|
||||
*/
|
||||
interface ExclusionResult {
|
||||
/** Whether the file should be excluded */
|
||||
excluded: boolean
|
||||
/** Optional reason for exclusion */
|
||||
reason?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default list of file and directory patterns to exclude from checkpoints.
|
||||
* Combines built-in patterns with workspace-specific LFS patterns.
|
||||
*
|
||||
* @param lfsPatterns - Optional array of Git LFS patterns from workspace
|
||||
* @returns Array of glob patterns to exclude
|
||||
* @todo Make this configurable by the user
|
||||
*/
|
||||
export const getDefaultExclusions = (lfsPatterns: string[] = []): string[] => [
|
||||
// Build and Development Artifacts
|
||||
".git/",
|
||||
`.git${GIT_DISABLED_SUFFIX}/`,
|
||||
...getBuildArtifactPatterns(),
|
||||
|
||||
// Media Files
|
||||
...getMediaFilePatterns(),
|
||||
|
||||
// Cache and Temporary Files
|
||||
...getCacheFilePatterns(),
|
||||
|
||||
// Environment and Config Files
|
||||
...getConfigFilePatterns(),
|
||||
|
||||
// Large Data Files
|
||||
...getLargeDataFilePatterns(),
|
||||
|
||||
// Database Files
|
||||
...getDatabaseFilePatterns(),
|
||||
|
||||
// Geospatial Datasets
|
||||
...getGeospatialPatterns(),
|
||||
|
||||
// Log Files
|
||||
...getLogFilePatterns(),
|
||||
|
||||
...lfsPatterns,
|
||||
]
|
||||
|
||||
/**
|
||||
* Returns patterns for common build and development artifact directories
|
||||
* @returns Array of glob patterns for build artifacts
|
||||
*/
|
||||
function getBuildArtifactPatterns(): string[] {
|
||||
return [
|
||||
".gradle/",
|
||||
".idea/",
|
||||
".parcel-cache/",
|
||||
".pytest_cache/",
|
||||
".next/",
|
||||
".nuxt/",
|
||||
".sass-cache/",
|
||||
".vs/",
|
||||
".vscode/",
|
||||
"Pods/",
|
||||
"__pycache__/",
|
||||
"bin/",
|
||||
"build/",
|
||||
"bundle/",
|
||||
"coverage/",
|
||||
"deps/",
|
||||
"dist/",
|
||||
"env/",
|
||||
"node_modules/",
|
||||
"obj/",
|
||||
"out/",
|
||||
"pkg/",
|
||||
"pycache/",
|
||||
"target/dependency/",
|
||||
"temp/",
|
||||
"vendor/",
|
||||
"venv/",
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns patterns for common media and image file types
|
||||
* @returns Array of glob patterns for media files
|
||||
*/
|
||||
function getMediaFilePatterns(): string[] {
|
||||
return [
|
||||
"*.jpg",
|
||||
"*.jpeg",
|
||||
"*.png",
|
||||
"*.gif",
|
||||
"*.bmp",
|
||||
"*.ico",
|
||||
"*.webp",
|
||||
"*.tiff",
|
||||
"*.tif",
|
||||
"*.svg",
|
||||
"*.raw",
|
||||
"*.heic",
|
||||
"*.avif",
|
||||
"*.eps",
|
||||
"*.psd",
|
||||
"*.3gp",
|
||||
"*.aac",
|
||||
"*.aiff",
|
||||
"*.asf",
|
||||
"*.avi",
|
||||
"*.divx",
|
||||
"*.flac",
|
||||
"*.m4a",
|
||||
"*.m4v",
|
||||
"*.mkv",
|
||||
"*.mov",
|
||||
"*.mp3",
|
||||
"*.mp4",
|
||||
"*.mpeg",
|
||||
"*.mpg",
|
||||
"*.ogg",
|
||||
"*.opus",
|
||||
"*.rm",
|
||||
"*.rmvb",
|
||||
"*.vob",
|
||||
"*.wav",
|
||||
"*.webm",
|
||||
"*.wma",
|
||||
"*.wmv",
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns patterns for cache, temporary, and system files
|
||||
* @returns Array of glob patterns for cache files
|
||||
*/
|
||||
function getCacheFilePatterns(): string[] {
|
||||
return [
|
||||
"*.DS_Store",
|
||||
"*.bak",
|
||||
"*.cache",
|
||||
"*.crdownload",
|
||||
"*.dmp",
|
||||
"*.dump",
|
||||
"*.eslintcache",
|
||||
"*.lock",
|
||||
"*.log",
|
||||
"*.old",
|
||||
"*.part",
|
||||
"*.partial",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.stackdump",
|
||||
"*.swo",
|
||||
"*.swp",
|
||||
"*.temp",
|
||||
"*.tmp",
|
||||
"*.Thumbs.db",
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns patterns for environment and configuration files
|
||||
* @returns Array of glob patterns for config files
|
||||
*/
|
||||
function getConfigFilePatterns(): string[] {
|
||||
return ["*.env*", "*.local", "*.development", "*.production"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns patterns for common large binary and archive files
|
||||
* @returns Array of glob patterns for large data files
|
||||
*/
|
||||
function getLargeDataFilePatterns(): string[] {
|
||||
return [
|
||||
"*.zip",
|
||||
"*.tar",
|
||||
"*.gz",
|
||||
"*.rar",
|
||||
"*.7z",
|
||||
"*.iso",
|
||||
"*.bin",
|
||||
"*.exe",
|
||||
"*.dll",
|
||||
"*.so",
|
||||
"*.dylib",
|
||||
"*.dat",
|
||||
"*.dmg",
|
||||
"*.msi",
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns patterns for database and data storage files
|
||||
* @returns Array of glob patterns for database files
|
||||
*/
|
||||
function getDatabaseFilePatterns(): string[] {
|
||||
return [
|
||||
"*.arrow",
|
||||
"*.accdb",
|
||||
"*.aof",
|
||||
"*.avro",
|
||||
"*.bak",
|
||||
"*.bson",
|
||||
"*.csv",
|
||||
"*.db",
|
||||
"*.dbf",
|
||||
"*.dmp",
|
||||
"*.frm",
|
||||
"*.ibd",
|
||||
"*.mdb",
|
||||
"*.myd",
|
||||
"*.myi",
|
||||
"*.orc",
|
||||
"*.parquet",
|
||||
"*.pdb",
|
||||
"*.rdb",
|
||||
"*.sql",
|
||||
"*.sqlite",
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns patterns for geospatial and mapping data files
|
||||
* @returns Array of glob patterns for geospatial files
|
||||
*/
|
||||
function getGeospatialPatterns(): string[] {
|
||||
return [
|
||||
"*.shp",
|
||||
"*.shx",
|
||||
"*.dbf",
|
||||
"*.prj",
|
||||
"*.sbn",
|
||||
"*.sbx",
|
||||
"*.shp.xml",
|
||||
"*.cpg",
|
||||
"*.gdb",
|
||||
"*.mdb",
|
||||
"*.gpkg",
|
||||
"*.kml",
|
||||
"*.kmz",
|
||||
"*.gml",
|
||||
"*.geojson",
|
||||
"*.dem",
|
||||
"*.asc",
|
||||
"*.img",
|
||||
"*.ecw",
|
||||
"*.las",
|
||||
"*.laz",
|
||||
"*.mxd",
|
||||
"*.qgs",
|
||||
"*.grd",
|
||||
"*.csv",
|
||||
"*.dwg",
|
||||
"*.dxf",
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns patterns for log and debug output files
|
||||
* @returns Array of glob patterns for log files
|
||||
*/
|
||||
function getLogFilePatterns(): string[] {
|
||||
return ["*.error", "*.log", "*.logs", "*.npm-debug.log*", "*.out", "*.stdout", "yarn-debug.log*", "yarn-error.log*"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the combined exclusion patterns to Git's exclude file.
|
||||
* Creates the info directory if it doesn't exist.
|
||||
*
|
||||
* @param gitPath - Path to the .git directory
|
||||
* @param lfsPatterns - Optional array of Git LFS patterns to include
|
||||
*/
|
||||
export const writeExcludesFile = async (gitPath: string, lfsPatterns: string[] = []): Promise<void> => {
|
||||
const excludesPath = join(gitPath, "info", "exclude")
|
||||
await fs.mkdir(join(gitPath, "info"), { recursive: true })
|
||||
|
||||
const patterns = getDefaultExclusions(lfsPatterns)
|
||||
await fs.writeFile(excludesPath, patterns.join("\n"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves Git LFS patterns from the workspace's .gitattributes file.
|
||||
* Returns an empty array if no patterns found or file doesn't exist.
|
||||
*
|
||||
* @param workspacePath - Path to the workspace root
|
||||
* @returns Array of Git LFS patterns found in .gitattributes
|
||||
*/
|
||||
export const getLfsPatterns = async (workspacePath: string): Promise<string[]> => {
|
||||
try {
|
||||
const attributesPath = join(workspacePath, ".gitattributes")
|
||||
if (await fileExistsAtPath(attributesPath)) {
|
||||
const attributesContent = await fs.readFile(attributesPath, "utf8")
|
||||
return attributesContent
|
||||
.split("\n")
|
||||
.filter((line) => line.includes("filter=lfs"))
|
||||
.map((line) => line.split(" ")[0].trim())
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to read .gitattributes:", error)
|
||||
}
|
||||
return []
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
import simpleGit, { SimpleGit } from "simple-git"
|
||||
import { getLfsPatterns, writeExcludesFile } from "./CheckpointExclusions"
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import * as vscode from "vscode"
|
||||
import { getWorkingDirectory, hashWorkingDir } from "./CheckpointUtils"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
|
||||
interface StorageProvider {
|
||||
context: {
|
||||
globalStorageUri: { fsPath: string }
|
||||
}
|
||||
}
|
||||
|
||||
interface CheckpointAddResult {
|
||||
success: boolean
|
||||
fileCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* GitOperations Class
|
||||
*
|
||||
* Handles git-specific operations for Cline's Checkpoints system.
|
||||
*
|
||||
* Key responsibilities:
|
||||
* - Git repository initialization and configuration
|
||||
* - Git settings management (user, LFS, etc.)
|
||||
* - Worktree configuration and management
|
||||
* - Task-specific branch management (creation, switching, deletion)
|
||||
* - Handling of both legacy and branch-per-task checkpoint structures
|
||||
* - Managing nested git repositories during checkpoint operations
|
||||
* - File staging and checkpoint creation
|
||||
* - Shadow git repository maintenance and cleanup
|
||||
*/
|
||||
export class GitOperations {
|
||||
private cwd: string
|
||||
private isLegacyCheckpoint: boolean
|
||||
|
||||
/**
|
||||
* Creates a new GitOperations instance.
|
||||
*
|
||||
* @param cwd - The current working directory for git operations
|
||||
* @param isLegacyCheckpoint - Whether this is operating in legacy checkpoint mode
|
||||
*/
|
||||
constructor(cwd: string, isLegacyCheckpoint: boolean) {
|
||||
this.cwd = cwd
|
||||
this.isLegacyCheckpoint = isLegacyCheckpoint
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes or verifies a shadow Git repository for checkpoint tracking.
|
||||
* Creates a new repository if one doesn't exist, or verifies the worktree
|
||||
* configuration if it does.
|
||||
*
|
||||
* Key operations:
|
||||
* - Creates/verifies shadow git repository
|
||||
* - Configures git settings (user, LFS, etc.)
|
||||
* - Sets up worktree to point to workspace
|
||||
* - Creates initial empty commit
|
||||
* - Handles both legacy and branch-per-task checkpoint structures
|
||||
*
|
||||
* @param gitPath - Path to the .git directory
|
||||
* @param cwd - The current working directory for git operations
|
||||
* @param isLegacyCheckpoint - Whether this is operating in legacy checkpoint mode
|
||||
* @returns Promise<string> Path to the initialized .git directory
|
||||
* @throws Error if:
|
||||
* - Worktree verification fails for existing repository
|
||||
* - Git initialization or configuration fails
|
||||
* - Unable to create initial commit
|
||||
* - LFS pattern setup fails
|
||||
*/
|
||||
public static async initShadowGit(gitPath: string, cwd: string, isLegacyCheckpoint: boolean): Promise<string> {
|
||||
console.info(`Initializing ${isLegacyCheckpoint ? "legacy" : "branch-per-task"} shadow git`)
|
||||
|
||||
// If repo exists, just verify worktree
|
||||
if (await fileExistsAtPath(gitPath)) {
|
||||
const git = simpleGit(path.dirname(gitPath))
|
||||
const worktree = await git.getConfig("core.worktree")
|
||||
if (worktree.value !== cwd) {
|
||||
throw new Error("Checkpoints can only be used in the original workspace: " + worktree.value)
|
||||
}
|
||||
console.warn(`Using existing ${isLegacyCheckpoint ? "legacy" : "branch-per-task"} shadow git at ${gitPath}`)
|
||||
return gitPath
|
||||
}
|
||||
|
||||
// Initialize new repo
|
||||
const checkpointsDir = path.dirname(gitPath)
|
||||
console.warn(`Creating new ${isLegacyCheckpoint ? "legacy" : "branch-per-task"} shadow git in ${checkpointsDir}`)
|
||||
|
||||
const git = simpleGit(checkpointsDir)
|
||||
await git.init()
|
||||
|
||||
// Configure repo
|
||||
await git.addConfig("core.worktree", cwd)
|
||||
await git.addConfig("commit.gpgSign", "false")
|
||||
await git.addConfig("user.name", "Cline Checkpoint")
|
||||
await git.addConfig("user.email", "checkpoint@cline.bot")
|
||||
await git.addConfig("core.quotePath", "false")
|
||||
await git.addConfig("core.precomposeunicode", "true")
|
||||
|
||||
// Set up LFS patterns
|
||||
const lfsPatterns = await getLfsPatterns(cwd)
|
||||
await writeExcludesFile(gitPath, lfsPatterns)
|
||||
|
||||
// Initial commit only on first repo creation
|
||||
await git.commit("initial commit", { "--allow-empty": null })
|
||||
|
||||
console.warn(`${isLegacyCheckpoint ? "Legacy" : "New"} shadow git initialization completed`)
|
||||
|
||||
return gitPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the worktree path from the shadow git configuration.
|
||||
* The worktree path indicates where the shadow git repository is tracking files,
|
||||
* which should match the current workspace directory.
|
||||
*
|
||||
* @param gitPath - Path to the .git directory
|
||||
* @returns Promise<string | undefined> The worktree path or undefined if not found
|
||||
* @throws Error if unable to get worktree path
|
||||
*/
|
||||
public async getShadowGitConfigWorkTree(gitPath: string): Promise<string | undefined> {
|
||||
try {
|
||||
const git = simpleGit(path.dirname(gitPath))
|
||||
const worktree = await git.getConfig("core.worktree")
|
||||
return worktree.value || undefined
|
||||
} catch (error) {
|
||||
console.error("Failed to get shadow git config worktree:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a shadow Git repository exists for the given task and workspace.
|
||||
* Checks both legacy checkpoint paths (tasks/{taskId}/checkpoints/.git) and
|
||||
* branch-per-task paths (checkpoints/{workspaceHash}/.git).
|
||||
*
|
||||
* @param taskId - The ID of the task whose shadow git to check
|
||||
* @param provider - The ClineProvider instance for accessing VS Code functionality
|
||||
* @returns Promise<boolean> True if either a legacy or branch-per-task shadow git exists, false otherwise
|
||||
*/
|
||||
public static async doesShadowGitExist(taskId: string, provider?: StorageProvider): Promise<boolean> {
|
||||
const globalStoragePath = provider?.context.globalStorageUri.fsPath
|
||||
if (!globalStoragePath) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check legacy checkpoint path to see if this is a legacy task
|
||||
const legacyGitPath = path.join(globalStoragePath, "tasks", taskId, "checkpoints", ".git")
|
||||
if (await fileExistsAtPath(legacyGitPath)) {
|
||||
console.info("Found legacy shadow git")
|
||||
return true
|
||||
}
|
||||
|
||||
// Check branch-per-task path for newer tasks
|
||||
const workingDir = await getWorkingDirectory()
|
||||
const cwdHash = hashWorkingDir(workingDir)
|
||||
const gitPath = path.join(globalStoragePath, "checkpoints", cwdHash, ".git")
|
||||
const exists = await fileExistsAtPath(gitPath)
|
||||
if (exists) {
|
||||
console.info("Found branch-per-task shadow git")
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a branch in the git repository, handling cases where the branch is currently checked out.
|
||||
* If the branch to be deleted is currently checked out, the method will:
|
||||
* 1. Save the current worktree configuration
|
||||
* 2. Temporarily unset the worktree to prevent workspace modifications
|
||||
* 3. Force switch to master/main branch
|
||||
* 4. Delete the target branch
|
||||
* 5. Restore the worktree configuration
|
||||
*
|
||||
* @param git - SimpleGit instance to use for operations
|
||||
* @param branchName - Name of the branch to delete
|
||||
* @param checkpointsDir - Directory containing the git repository
|
||||
* @throws Error if:
|
||||
* - Branch deletion fails
|
||||
* - Unable to switch to master/main branch after 3 retries
|
||||
* - Git operations fail during the process
|
||||
*/
|
||||
public static async deleteBranchForGit(git: SimpleGit, branchName: string, checkpointsDir: string): Promise<void> {
|
||||
// Check if branch exists
|
||||
const branches = await git.branchLocal()
|
||||
if (!branches.all.includes(branchName)) {
|
||||
console.error(`Task branch ${branchName} does not exist, nothing to delete`)
|
||||
return // Branch doesn't exist, nothing to delete
|
||||
}
|
||||
|
||||
// First, if we're on the branch to be deleted, switch to master/main
|
||||
const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"])
|
||||
console.info(`Current branch: ${currentBranch}, target branch to delete: ${branchName}`)
|
||||
|
||||
if (currentBranch === branchName) {
|
||||
console.debug("Currently on branch to be deleted, switching to master/main first")
|
||||
// Save the current worktree config
|
||||
const worktree = await git.getConfig("core.worktree")
|
||||
console.debug(`Saved current worktree config: ${worktree.value}`)
|
||||
|
||||
try {
|
||||
// Temporarily unset worktree to prevent workspace modifications
|
||||
console.debug("Temporarily unsetting worktree config")
|
||||
await git.raw(["config", "--unset", "core.worktree"])
|
||||
|
||||
// Force discard all changes
|
||||
console.debug("Discarding all changes")
|
||||
await git.reset(["--hard"])
|
||||
await git.clean("f", ["-d"]) // Clean mode 'f' for force, -d for directories
|
||||
|
||||
// Determine default branch (master or main)
|
||||
const defaultBranch = branches.all.includes("main") ? "main" : "master"
|
||||
console.debug(`Using ${defaultBranch} as default branch`)
|
||||
|
||||
// Switch to default branch and delete branch
|
||||
console.debug(`Attempting to force switch to ${defaultBranch} branch`)
|
||||
await git.checkout([defaultBranch, "--force"])
|
||||
|
||||
// Verify the switch completed
|
||||
let retries = 3
|
||||
while (retries > 0) {
|
||||
const newBranch = await git.revparse(["--abbrev-ref", "HEAD"])
|
||||
console.debug(`Verifying branch switch - current branch: ${newBranch}, attempts left: ${retries}`)
|
||||
if (newBranch === defaultBranch) {
|
||||
console.debug(`Successfully switched to ${defaultBranch} branch`)
|
||||
break
|
||||
}
|
||||
retries--
|
||||
if (retries === 0) {
|
||||
throw new Error(`Failed to switch to ${defaultBranch} branch`)
|
||||
}
|
||||
}
|
||||
|
||||
console.info(`Deleting branch: ${branchName}`)
|
||||
await git.raw(["branch", "-D", branchName])
|
||||
console.debug(`Successfully deleted branch: ${branchName}`)
|
||||
} finally {
|
||||
// Restore the worktree config
|
||||
if (worktree.value) {
|
||||
console.debug(`Restoring worktree config to: ${worktree.value}`)
|
||||
await git.addConfig("core.worktree", worktree.value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If we're not on the branch, we can safely delete it
|
||||
console.info(`Directly deleting branch ${branchName} since we're not on it`)
|
||||
await git.raw(["branch", "-D", branchName])
|
||||
console.debug(`Successfully deleted branch: ${branchName}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to delete a task's branch using stored workspace path.
|
||||
* Handles both branch-per-task and legacy checkpoint formats:
|
||||
* 1. First attempts to delete branch-per-task checkpoint if it exists
|
||||
* 2. Falls back to deleting legacy checkpoint directory if found
|
||||
*
|
||||
* @param taskId - The ID of the task whose branch should be deleted
|
||||
* @param historyItem - The history item containing the shadow git config
|
||||
* @param globalStoragePath - Path to VS Code's global storage
|
||||
* @throws Error if:
|
||||
* - Global storage path is invalid
|
||||
* - Branch deletion fails
|
||||
* - Legacy checkpoint directory deletion fails
|
||||
*/
|
||||
public static async deleteTaskBranchStatic(
|
||||
taskId: string,
|
||||
historyItem: HistoryItem,
|
||||
globalStoragePath: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
console.debug("Starting static task branch deletion process...")
|
||||
|
||||
if (!globalStoragePath) {
|
||||
throw new Error("Global storage uri is invalid")
|
||||
}
|
||||
|
||||
// First try to handle branch-per-task checkpoint
|
||||
let workingDir: string
|
||||
if (historyItem.shadowGitConfigWorkTree) {
|
||||
workingDir = historyItem.shadowGitConfigWorkTree
|
||||
} else {
|
||||
// Try to determine working directory from current state
|
||||
workingDir = await getWorkingDirectory()
|
||||
}
|
||||
|
||||
const cwdHash = hashWorkingDir(workingDir)
|
||||
const checkpointsDir = path.join(globalStoragePath, "checkpoints", cwdHash)
|
||||
const gitPath = path.join(checkpointsDir, ".git")
|
||||
|
||||
if (await fileExistsAtPath(gitPath)) {
|
||||
console.debug(`Found branch-per-task git repository at ${gitPath}`)
|
||||
const git = simpleGit(path.dirname(gitPath))
|
||||
const branchName = `task-${taskId}`
|
||||
|
||||
// Check if the branch exists
|
||||
const branches = await git.branchLocal()
|
||||
if (branches.all.includes(branchName)) {
|
||||
console.info(`Found branch ${branchName} to delete`)
|
||||
await GitOperations.deleteBranchForGit(git, branchName, checkpointsDir)
|
||||
return
|
||||
}
|
||||
console.warn(`Branch ${branchName} not found in branch-per-task repository`)
|
||||
}
|
||||
|
||||
// Only check legacy checkpoint if we didn't find/delete a branch-per-task branch
|
||||
const legacyCheckpointsDir = path.join(globalStoragePath, "tasks", taskId, "checkpoints")
|
||||
const legacyGitPath = path.join(legacyCheckpointsDir, ".git")
|
||||
|
||||
if (await fileExistsAtPath(legacyGitPath)) {
|
||||
console.info("Found legacy checkpoint, deleting directory")
|
||||
try {
|
||||
await fs.rm(legacyCheckpointsDir, { recursive: true, force: true })
|
||||
console.debug("Successfully deleted legacy checkpoint directory")
|
||||
return
|
||||
} catch (error) {
|
||||
console.error("Failed to delete legacy checkpoint directory:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
console.info("No checkpoints found to delete")
|
||||
} catch (error) {
|
||||
console.error("Failed to delete task branch:", error)
|
||||
throw new Error(`Failed to delete task branch: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Since we use git to track checkpoints, we need to temporarily disable nested git repos to work around git's
|
||||
* requirement of using submodules for nested repos.
|
||||
*
|
||||
* This method renames nested .git directories by adding/removing a suffix to temporarily disable/enable them.
|
||||
* The root .git directory is preserved. Uses VS Code's workspace API to find nested .git directories and
|
||||
* only processes actual directories (not files named .git).
|
||||
*
|
||||
* @param disable - If true, adds suffix to disable nested git repos. If false, removes suffix to re-enable them.
|
||||
* @throws Error if renaming any .git directory fails
|
||||
*/
|
||||
public async renameNestedGitRepos(disable: boolean): Promise<void> {
|
||||
// Find all .git directories that are not at the root level using VS Code API
|
||||
const gitFiles = await vscode.workspace.findFiles(
|
||||
new vscode.RelativePattern(this.cwd, "**/.git" + (disable ? "" : GIT_DISABLED_SUFFIX)),
|
||||
new vscode.RelativePattern(this.cwd, ".git/**"), // Exclude root .git
|
||||
)
|
||||
|
||||
// Filter to only include directories
|
||||
const gitPaths: string[] = []
|
||||
for (const file of gitFiles) {
|
||||
const relativePath = path.relative(this.cwd, file.fsPath)
|
||||
try {
|
||||
const stats = await fs.stat(path.join(this.cwd, relativePath))
|
||||
if (stats.isDirectory()) {
|
||||
gitPaths.push(relativePath)
|
||||
}
|
||||
} catch {
|
||||
// Skip if stat fails
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// For each nested .git directory, rename it based on the disable flag
|
||||
for (const gitPath of gitPaths) {
|
||||
const fullPath = path.join(this.cwd, gitPath)
|
||||
let newPath: string
|
||||
if (disable) {
|
||||
newPath = fullPath + GIT_DISABLED_SUFFIX
|
||||
} else {
|
||||
newPath = fullPath.endsWith(GIT_DISABLED_SUFFIX) ? fullPath.slice(0, -GIT_DISABLED_SUFFIX.length) : fullPath
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.rename(fullPath, newPath)
|
||||
console.info(`${disable ? "Disabled" : "Enabled"} nested git repo ${gitPath}`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to ${disable ? "disable" : "enable"} nested git repo ${gitPath}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to or creates a task-specific branch in the shadow Git repository.
|
||||
* For legacy checkpoints, this is a no-op since they use separate repositories.
|
||||
* For branch-per-task checkpoints, this ensures we're on the correct task branch before operations.
|
||||
*
|
||||
* The method performs the following:
|
||||
* 1. Gets the shadow git path and initializes simple-git
|
||||
* 2. Constructs the branch name using the task ID
|
||||
* 3. Checks if the branch exists:
|
||||
* - If not, creates a new branch
|
||||
* - If yes, switches to the existing branch
|
||||
* 4. Verifies the branch switch completed successfully
|
||||
*
|
||||
* Branch naming convention:
|
||||
* task-{taskId}
|
||||
*
|
||||
* @param taskId - The ID of the task whose branch to switch to
|
||||
* @param gitPath - Path to the .git directory
|
||||
* @returns Promise<void>
|
||||
* @throws Error if branch operations fail or git commands error
|
||||
*/
|
||||
public async switchToTaskBranch(taskId: string, gitPath: string): Promise<void> {
|
||||
const git = simpleGit(path.dirname(gitPath))
|
||||
const branchName = `task-${taskId}`
|
||||
|
||||
// Create new task-specific branch, or switch to one if it already exists.
|
||||
const branches = await git.branchLocal()
|
||||
if (!branches.all.includes(branchName)) {
|
||||
console.info(`Creating new task branch: ${branchName}`)
|
||||
await git.checkoutLocalBranch(branchName)
|
||||
} else {
|
||||
console.info(`Switching to existing task branch: ${branchName}`)
|
||||
await git.checkout(branchName)
|
||||
}
|
||||
|
||||
const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"])
|
||||
console.info(`Current Checkpoint branch after switch: ${currentBranch}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds files to the shadow git repository while handling nested git repos.
|
||||
* Uses git commands to list files and stages them for commit.
|
||||
* Respects .gitignore and handles LFS patterns.
|
||||
*
|
||||
* Process:
|
||||
* 1. Updates exclude patterns from LFS config
|
||||
* 2. Temporarily disables nested git repos
|
||||
* 3. Gets list of tracked and untracked files from git (respecting .gitignore)
|
||||
* 4. Adds all files to git staging
|
||||
* 5. Re-enables nested git repos
|
||||
*
|
||||
* @param git - SimpleGit instance configured for the shadow git repo
|
||||
* @param gitPath - Path to the .git directory
|
||||
* @returns Promise<CheckpointAddResult> Object containing success status, message, and file count
|
||||
* @throws Error if:
|
||||
* - File operations fail
|
||||
* - Git commands error
|
||||
* - LFS pattern updates fail
|
||||
* - Nested git repo handling fails
|
||||
*/
|
||||
public async addCheckpointFiles(git: SimpleGit, gitPath: string): Promise<CheckpointAddResult> {
|
||||
try {
|
||||
// Update exclude patterns before each commit
|
||||
await writeExcludesFile(gitPath, await getLfsPatterns(this.cwd))
|
||||
await this.renameNestedGitRepos(true)
|
||||
//console.info("Starting checkpoint add operation...")
|
||||
|
||||
// Get list of all files git would track (respects .gitignore)
|
||||
await git.addConfig("core.quotePath", "false")
|
||||
await git.addConfig("core.precomposeunicode", "true")
|
||||
const gitFiles = (await git.raw(["ls-files", "--others", "--exclude-standard", "--cached"]))
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
|
||||
// Add filtered files
|
||||
if (gitFiles.length === 0) {
|
||||
console.info("No files to add to checkpoint")
|
||||
return { success: true, fileCount: 0 }
|
||||
}
|
||||
|
||||
try {
|
||||
console.info(`Adding ${gitFiles.length} files to checkpoint`)
|
||||
await git.addConfig("core.quotePath", "false")
|
||||
await git.addConfig("core.precomposeunicode", "true")
|
||||
await git.add(gitFiles)
|
||||
console.info("Checkpoint add operation completed successfully")
|
||||
return { success: true, fileCount: gitFiles.length }
|
||||
} catch (error) {
|
||||
console.error("Checkpoint add operation failed:", error)
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to add files to checkpoint", error)
|
||||
throw error
|
||||
} finally {
|
||||
await this.renameNestedGitRepos(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const GIT_DISABLED_SUFFIX = "_disabled"
|
||||
@@ -0,0 +1,95 @@
|
||||
import { expect } from "chai"
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { createTestEnvironment, createTestTracker } from "./Checkpoint-test-utils"
|
||||
|
||||
describe("Checkpoint Revert Operations", () => {
|
||||
let env: Awaited<ReturnType<typeof createTestEnvironment>>
|
||||
|
||||
beforeEach(async () => {
|
||||
env = await createTestEnvironment()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await env.cleanup()
|
||||
})
|
||||
|
||||
it("should revert working directory to a previous checkpoint state", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
if (!tracker) {throw new Error("Failed to create tracker")}
|
||||
|
||||
// Create and commit initial state
|
||||
await fs.writeFile(env.testFilePath, "initial content")
|
||||
const firstCommit = await tracker.commit()
|
||||
expect(firstCommit).to.not.be.undefined
|
||||
|
||||
// Create and commit changes
|
||||
await fs.writeFile(env.testFilePath, "modified content")
|
||||
const secondCommit = await tracker.commit()
|
||||
expect(secondCommit).to.not.be.undefined
|
||||
|
||||
// Make more changes without committing
|
||||
await fs.writeFile(env.testFilePath, "uncommitted changes")
|
||||
|
||||
// Revert to first commit
|
||||
await tracker.resetHead(firstCommit!)
|
||||
|
||||
// Verify file content matches initial state
|
||||
const resetContent = await fs.readFile(env.testFilePath, "utf8")
|
||||
expect(resetContent).to.equal("initial content")
|
||||
})
|
||||
|
||||
it("should handle reverting with multiple files", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
if (!tracker) {throw new Error("Failed to create tracker")}
|
||||
|
||||
// Create and commit initial state with multiple files
|
||||
const testFile2Path = path.join(env.tempDir, "src", "test2.txt")
|
||||
await fs.writeFile(env.testFilePath, "file1 initial")
|
||||
await fs.writeFile(testFile2Path, "file2 initial")
|
||||
const firstCommit = await tracker.commit()
|
||||
expect(firstCommit).to.not.be.undefined
|
||||
|
||||
// Modify both files and commit
|
||||
await fs.writeFile(env.testFilePath, "file1 modified")
|
||||
await fs.writeFile(testFile2Path, "file2 modified")
|
||||
const secondCommit = await tracker.commit()
|
||||
expect(secondCommit).to.not.be.undefined
|
||||
|
||||
// Make more changes
|
||||
await fs.writeFile(env.testFilePath, "file1 uncommitted")
|
||||
await fs.writeFile(testFile2Path, "file2 uncommitted")
|
||||
|
||||
// Reset to first commit
|
||||
await tracker.resetHead(firstCommit!)
|
||||
|
||||
// Verify both files match initial state
|
||||
const file1Content = await fs.readFile(env.testFilePath, "utf8")
|
||||
const file2Content = await fs.readFile(testFile2Path, "utf8")
|
||||
expect(file1Content).to.equal("file1 initial")
|
||||
expect(file2Content).to.equal("file2 initial")
|
||||
})
|
||||
|
||||
it("should handle reverting when files are deleted", async () => {
|
||||
const tracker = await createTestTracker(env.globalStoragePath)
|
||||
if (!tracker) {throw new Error("Failed to create tracker")}
|
||||
|
||||
// Create and commit initial state
|
||||
await fs.writeFile(env.testFilePath, "initial content")
|
||||
const firstCommit = await tracker.commit()
|
||||
expect(firstCommit).to.not.be.undefined
|
||||
|
||||
// Delete file and commit
|
||||
await fs.unlink(env.testFilePath)
|
||||
const secondCommit = await tracker.commit()
|
||||
expect(secondCommit).to.not.be.undefined
|
||||
|
||||
// Revert to first commit
|
||||
await tracker.resetHead(firstCommit!)
|
||||
|
||||
// Verify file is restored with original content
|
||||
const resetContent = await fs.readFile(env.testFilePath, "utf8")
|
||||
expect(resetContent).to.equal("initial content")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
import { expect } from "chai"
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import fs from "fs/promises"
|
||||
import { createTestEnvironment, createTestTracker } from "./Checkpoint-test-utils"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import CheckpointTracker from "./CheckpointTracker"
|
||||
|
||||
describe("Checkpoint Task Switching", () => {
|
||||
let env: Awaited<ReturnType<typeof createTestEnvironment>>
|
||||
let taskId1: string
|
||||
let taskId2: string
|
||||
let tracker1: CheckpointTracker | undefined
|
||||
let tracker2: CheckpointTracker | undefined
|
||||
|
||||
beforeEach(async () => {
|
||||
env = await createTestEnvironment()
|
||||
taskId1 = "task-1"
|
||||
taskId2 = "task-2"
|
||||
tracker1 = await createTestTracker(env.globalStoragePath, taskId1)
|
||||
if (!tracker1) {throw new Error("Failed to create tracker1")}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await env.cleanup()
|
||||
})
|
||||
|
||||
it("should maintain separate history for each task", async () => {
|
||||
if (!tracker1) {throw new Error("Failed to create tracker1")}
|
||||
|
||||
// Create and commit file in first task
|
||||
await fs.writeFile(env.testFilePath, "task1 initial")
|
||||
const task1Commit1 = await tracker1.commit()
|
||||
expect(task1Commit1).to.be.a("string").and.not.empty
|
||||
|
||||
// Modify and commit again in first task
|
||||
await fs.writeFile(env.testFilePath, "task1 modified")
|
||||
const task1Commit2 = await tracker1.commit()
|
||||
expect(task1Commit2).to.be.a("string").and.not.empty
|
||||
|
||||
// Create second task tracker
|
||||
tracker2 = await createTestTracker(env.globalStoragePath, taskId2)
|
||||
if (!tracker2) {throw new Error("Failed to create tracker2")}
|
||||
|
||||
// Create and commit file in second task
|
||||
await fs.writeFile(env.testFilePath, "task2 initial")
|
||||
const task2Commit1 = await tracker2.commit()
|
||||
expect(task2Commit1).to.be.a("string").and.not.empty
|
||||
|
||||
// Create another commit to establish history
|
||||
await fs.writeFile(env.testFilePath, "task2 modified")
|
||||
const task2Commit2 = await tracker2.commit()
|
||||
expect(task2Commit2).to.be.a("string").and.not.empty
|
||||
|
||||
// Verify second task's history
|
||||
const task2Diff = await tracker2.getDiffSet(task2Commit1, task2Commit2)
|
||||
expect(task2Diff).to.have.lengthOf(1)
|
||||
expect(task2Diff[0].before).to.equal("task2 initial")
|
||||
expect(task2Diff[0].after).to.equal("task2 modified")
|
||||
|
||||
// Switch back to first task by creating new tracker
|
||||
const tracker1Again = await createTestTracker(env.globalStoragePath, taskId1)
|
||||
if (!tracker1Again) {throw new Error("Failed to create tracker1Again")}
|
||||
|
||||
// Verify first task's history is preserved
|
||||
const task1Diff = await tracker1Again.getDiffSet(task1Commit1, task1Commit2)
|
||||
expect(task1Diff[0].before).to.equal("task1 initial")
|
||||
expect(task1Diff[0].after).to.equal("task1 modified")
|
||||
|
||||
// Reset first task to initial state
|
||||
if (!task1Commit1) {throw new Error("Failed to create initial commit")}
|
||||
await tracker1Again.resetHead(task1Commit1)
|
||||
const resetContent = await fs.readFile(env.testFilePath, "utf8")
|
||||
expect(resetContent).to.equal("task1 initial")
|
||||
})
|
||||
|
||||
it("should handle task deletion and recreation", async () => {
|
||||
if (!tracker1) {throw new Error("Failed to create tracker1")}
|
||||
|
||||
// Create and commit file in first task
|
||||
await fs.writeFile(env.testFilePath, "task1 content")
|
||||
const task1Commit = await tracker1.commit()
|
||||
expect(task1Commit).to.be.a("string").and.not.empty
|
||||
|
||||
// Create second task
|
||||
tracker2 = await createTestTracker(env.globalStoragePath, taskId2)
|
||||
if (!tracker2) {throw new Error("Failed to create tracker2")}
|
||||
await fs.writeFile(env.testFilePath, "task2 content")
|
||||
const task2Commit = await tracker2.commit()
|
||||
expect(task2Commit).to.be.a("string").and.not.empty
|
||||
|
||||
// Delete second task's checkpoints
|
||||
const historyItem: HistoryItem = {
|
||||
id: `test-${Date.now()}`,
|
||||
ts: Date.now(),
|
||||
task: taskId2,
|
||||
shadowGitConfigWorkTree: env.tempDir,
|
||||
tokensIn: 0,
|
||||
tokensOut: 0,
|
||||
totalCost: 0,
|
||||
}
|
||||
await CheckpointTracker.deleteCheckpoints(taskId2, historyItem, env.globalStoragePath)
|
||||
|
||||
// Recreate second task
|
||||
const tracker2Again = await createTestTracker(env.globalStoragePath, taskId2)
|
||||
if (!tracker2Again) {throw new Error("Failed to create tracker2Again")}
|
||||
|
||||
// Create new commit in recreated task
|
||||
await fs.writeFile(env.testFilePath, "task2 new content")
|
||||
const newCommit = await tracker2Again.commit()
|
||||
expect(newCommit).to.be.a("string").and.not.empty
|
||||
|
||||
// Switch back to first task and verify its history is intact
|
||||
const tracker1Again = await createTestTracker(env.globalStoragePath, taskId1)
|
||||
if (!tracker1Again) {throw new Error("Failed to create tracker1Again")}
|
||||
if (!task1Commit) {throw new Error("Failed to create initial commit")}
|
||||
await tracker1Again.resetHead(task1Commit)
|
||||
const resetContent = await fs.readFile(env.testFilePath, "utf8")
|
||||
expect(resetContent).to.equal("task1 content")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,453 @@
|
||||
import fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import simpleGit, { SimpleGit } from "simple-git"
|
||||
import * as vscode from "vscode"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { GitOperations } from "./CheckpointGitOperations"
|
||||
import { getShadowGitPath, hashWorkingDir, getWorkingDirectory, detectLegacyCheckpoint } from "./CheckpointUtils"
|
||||
|
||||
/**
|
||||
* CheckpointTracker Module
|
||||
*
|
||||
* Core implementation of Cline's Checkpoints system that provides version control
|
||||
* capabilities without interfering with the user's main Git repository. Key features:
|
||||
*
|
||||
* Shadow Git Repository:
|
||||
* - Creates and manages an isolated Git repository for tracking checkpoints
|
||||
* - Handles nested Git repositories by temporarily disabling them
|
||||
* - Configures Git settings automatically (identity, LFS, etc.)
|
||||
*
|
||||
* File Management:
|
||||
* - Integrates with CheckpointExclusions for file filtering
|
||||
* - Handles workspace validation and path resolution
|
||||
* - Manages Git worktree configuration
|
||||
*
|
||||
* Checkpoint Operations:
|
||||
* - Creates checkpoints (commits) of the current state
|
||||
* - Provides diff capabilities between checkpoints
|
||||
* - Supports resetting to previous checkpoints
|
||||
*
|
||||
* Safety Features:
|
||||
* - Prevents usage in sensitive directories (home, desktop, etc.)
|
||||
* - Validates workspace configuration
|
||||
* - Handles cleanup and resource disposal
|
||||
*
|
||||
* Checkpoint Architecture:
|
||||
* - Uses a branch-per-task model to consolidate shadow git repositories
|
||||
* - Each task gets its own branch within a single shadow git per workspace
|
||||
* - Maintains backward compatibility with legacy checkpoint structure
|
||||
* - Automatically cleans up by deleting task branches when tasks are removed
|
||||
*/
|
||||
|
||||
class CheckpointTracker {
|
||||
private globalStoragePath: string
|
||||
private taskId: string
|
||||
private cwd: string
|
||||
private cwdHash: string
|
||||
private lastRetrievedShadowGitConfigWorkTree?: string
|
||||
private lastCheckpointHash?: string
|
||||
private isLegacyCheckpoint: boolean = false
|
||||
private gitOperations: GitOperations
|
||||
|
||||
/**
|
||||
* Creates a new CheckpointTracker instance to manage checkpoints for a specific task.
|
||||
* The constructor is private - use the static create() method to instantiate.
|
||||
*
|
||||
* @param taskId - Unique identifier for the task being tracked
|
||||
* @param cwd - The current working directory to track files in
|
||||
* @param cwdHash - Hash of the working directory path for shadow git organization
|
||||
*/
|
||||
private constructor(globalStoragePath: string, taskId: string, cwd: string, cwdHash: string) {
|
||||
this.globalStoragePath = globalStoragePath
|
||||
this.taskId = taskId
|
||||
this.cwd = cwd
|
||||
this.cwdHash = cwdHash
|
||||
this.gitOperations = new GitOperations(cwd, false) // Initialize with non-legacy mode
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new CheckpointTracker instance for tracking changes in a task.
|
||||
* Handles initialization of the shadow git repository and branch setup.
|
||||
*
|
||||
* @param taskId - Unique identifier for the task to track
|
||||
* @param globalStoragePath - the globalStorage path
|
||||
* @returns Promise resolving to new CheckpointTracker instance, or undefined if checkpoints are disabled
|
||||
* @throws Error if:
|
||||
* - globalStoragePath is not supplied
|
||||
* - Git is not installed
|
||||
* - Working directory is invalid or in a protected location
|
||||
* - Shadow git initialization fails
|
||||
*
|
||||
* Key operations:
|
||||
* - Validates git installation and settings
|
||||
* - Creates/initializes shadow git repository
|
||||
* - Detects and handles legacy checkpoint structure
|
||||
* - Sets up task-specific branch for new checkpoints
|
||||
*
|
||||
* Configuration:
|
||||
* - Respects 'cline.enableCheckpoints' VS Code setting
|
||||
* - Uses branch-per-task architecture for new checkpoints
|
||||
* - Maintains backwards compatibility with legacy structure
|
||||
*/
|
||||
public static async create(taskId: string, globalStoragePath: string | undefined): Promise<CheckpointTracker | undefined> {
|
||||
if (!globalStoragePath) {
|
||||
throw new Error("Global storage path is required to create a checkpoint tracker")
|
||||
}
|
||||
try {
|
||||
console.info(`Creating new CheckpointTracker for task ${taskId}`)
|
||||
|
||||
// Check if checkpoints are disabled in VS Code settings
|
||||
const enableCheckpoints = vscode.workspace.getConfiguration("cline").get<boolean>("enableCheckpoints") ?? true
|
||||
if (!enableCheckpoints) {
|
||||
return undefined // Don't create tracker when disabled
|
||||
}
|
||||
|
||||
// Check if git is installed by attempting to get version
|
||||
try {
|
||||
await simpleGit().version()
|
||||
} catch (error) {
|
||||
throw new Error("Git must be installed to use checkpoints.") // FIXME: must match what we check for in TaskHeader to show link
|
||||
}
|
||||
|
||||
const workingDir = await getWorkingDirectory()
|
||||
const cwdHash = hashWorkingDir(workingDir)
|
||||
console.debug(`Repository ID (cwdHash): ${cwdHash}`)
|
||||
|
||||
const newTracker = new CheckpointTracker(globalStoragePath, taskId, workingDir, cwdHash)
|
||||
|
||||
// Check if this is a legacy task
|
||||
newTracker.isLegacyCheckpoint = await detectLegacyCheckpoint(newTracker.globalStoragePath, newTracker.taskId)
|
||||
if (newTracker.isLegacyCheckpoint) {
|
||||
console.debug("Using legacy checkpoint path structure")
|
||||
const gitPath = await getShadowGitPath(
|
||||
newTracker.globalStoragePath,
|
||||
newTracker.taskId,
|
||||
newTracker.cwdHash,
|
||||
newTracker.isLegacyCheckpoint,
|
||||
)
|
||||
await GitOperations.initShadowGit(gitPath, workingDir, newTracker.isLegacyCheckpoint)
|
||||
await newTracker.gitOperations.switchToTaskBranch(newTracker.taskId, gitPath)
|
||||
return newTracker
|
||||
}
|
||||
|
||||
// Branch-per-task structure
|
||||
const gitPath = await getShadowGitPath(
|
||||
newTracker.globalStoragePath,
|
||||
newTracker.taskId,
|
||||
newTracker.cwdHash,
|
||||
newTracker.isLegacyCheckpoint,
|
||||
)
|
||||
await GitOperations.initShadowGit(gitPath, workingDir, newTracker.isLegacyCheckpoint)
|
||||
await newTracker.gitOperations.switchToTaskBranch(newTracker.taskId, gitPath)
|
||||
return newTracker
|
||||
} catch (error) {
|
||||
console.error("Failed to create CheckpointTracker:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new checkpoint commit in the shadow git repository.
|
||||
*
|
||||
* Key behaviors:
|
||||
* - Creates commit with checkpoint files in shadow git repo
|
||||
* - Handles both legacy and branch-per-task checkpoint structures
|
||||
* - For new tasks, switches to task-specific branch first
|
||||
* - Caches the created commit hash
|
||||
*
|
||||
* Commit structure:
|
||||
* - Legacy: Simple "checkpoint" message
|
||||
* - Branch-per-task: "checkpoint-{cwdHash}-{taskId}"
|
||||
* - Always allows empty commits
|
||||
*
|
||||
* Dependencies:
|
||||
* - Requires initialized shadow git (getShadowGitPath)
|
||||
* - For new checkpoints, requires task branch setup
|
||||
* - Uses addCheckpointFiles to stage changes
|
||||
*
|
||||
* @returns Promise<string | undefined> The created commit hash, or undefined if:
|
||||
* - Shadow git access fails
|
||||
* - Branch switch fails
|
||||
* - Staging files fails
|
||||
* - Commit creation fails
|
||||
* @throws Error if unable to:
|
||||
* - Access shadow git path
|
||||
* - Initialize simple-git
|
||||
* - Switch branches
|
||||
* - Stage or commit files
|
||||
*/
|
||||
public async commit(): Promise<string | undefined> {
|
||||
try {
|
||||
console.info(`Creating new checkpoint commit for task ${this.taskId}`)
|
||||
const gitPath = await getShadowGitPath(this.globalStoragePath, this.taskId, this.cwdHash, this.isLegacyCheckpoint)
|
||||
const git = simpleGit(path.dirname(gitPath))
|
||||
|
||||
console.info(`Using shadow git at: ${gitPath}`)
|
||||
|
||||
// Disable nested git repos before any operations
|
||||
await this.gitOperations.renameNestedGitRepos(true)
|
||||
|
||||
try {
|
||||
if (!this.isLegacyCheckpoint) {
|
||||
await this.gitOperations.switchToTaskBranch(this.taskId, gitPath)
|
||||
}
|
||||
await this.gitOperations.addCheckpointFiles(git, gitPath)
|
||||
|
||||
const commitMessage = this.isLegacyCheckpoint ? "checkpoint" : "checkpoint-" + this.cwdHash + "-" + this.taskId
|
||||
|
||||
console.info(
|
||||
`Creating ${this.isLegacyCheckpoint ? "legacy" : "new"} checkpoint commit with message: ${commitMessage}`,
|
||||
)
|
||||
const result = await git.commit(commitMessage, {
|
||||
"--allow-empty": null,
|
||||
})
|
||||
const commitHash = result.commit || ""
|
||||
this.lastCheckpointHash = commitHash
|
||||
console.warn(`Checkpoint commit created.`)
|
||||
return commitHash
|
||||
} finally {
|
||||
// Always re-enable nested git repos
|
||||
await this.gitOperations.renameNestedGitRepos(false)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create checkpoint:", {
|
||||
taskId: this.taskId,
|
||||
error,
|
||||
isLegacyCheckpoint: this.isLegacyCheckpoint,
|
||||
})
|
||||
throw new Error(`Failed to create checkpoint: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the worktree path from the shadow git configuration.
|
||||
* The worktree path indicates where the shadow git repository is tracking files,
|
||||
* which should match the current workspace directory.
|
||||
*
|
||||
* Key behaviors:
|
||||
* - Caches result in lastRetrievedShadowGitConfigWorkTree to avoid repeated reads
|
||||
* - Returns cached value if available
|
||||
* - Reads git config if no cached value exists
|
||||
* - Handles both legacy and new checkpoint structures
|
||||
*
|
||||
* Configuration read:
|
||||
* - Uses simple-git to read core.worktree config
|
||||
* - Operates on shadow git at path from getShadowGitPath()
|
||||
*
|
||||
* @returns Promise<string | undefined> The configured worktree path, or undefined if:
|
||||
* - Shadow git repository doesn't exist
|
||||
* - Config read fails
|
||||
* - No worktree is configured
|
||||
* @throws Error if unable to:
|
||||
* - Access shadow git path
|
||||
* - Initialize simple-git
|
||||
* - Read git configuration
|
||||
*/
|
||||
public async getShadowGitConfigWorkTree(): Promise<string | undefined> {
|
||||
if (this.lastRetrievedShadowGitConfigWorkTree) {
|
||||
return this.lastRetrievedShadowGitConfigWorkTree
|
||||
}
|
||||
try {
|
||||
const gitPath = await getShadowGitPath(this.globalStoragePath, this.taskId, this.cwdHash, this.isLegacyCheckpoint)
|
||||
this.lastRetrievedShadowGitConfigWorkTree = await this.gitOperations.getShadowGitConfigWorkTree(gitPath)
|
||||
return this.lastRetrievedShadowGitConfigWorkTree
|
||||
} catch (error) {
|
||||
console.error("Failed to get shadow git config worktree:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the shadow git repository's HEAD to a specific checkpoint commit.
|
||||
* This will discard all changes after the target commit and restore the
|
||||
* working directory to that checkpoint's state.
|
||||
*
|
||||
* Dependencies:
|
||||
* - Requires initialized shadow git (getShadowGitPath)
|
||||
* - For new checkpoints, requires task branch setup
|
||||
* - Must be called with a valid commit hash from this task's history
|
||||
*
|
||||
* @param commitHash - The hash of the checkpoint commit to reset to
|
||||
* @returns Promise<void> Resolves when reset is complete
|
||||
* @throws Error if unable to:
|
||||
* - Access shadow git path
|
||||
* - Initialize simple-git
|
||||
* - Switch to task branch
|
||||
* - Reset to target commit
|
||||
*/
|
||||
public async resetHead(commitHash: string): Promise<void> {
|
||||
console.info(`Resetting to checkpoint: ${commitHash}`)
|
||||
const gitPath = await getShadowGitPath(this.globalStoragePath, this.taskId, this.cwdHash, this.isLegacyCheckpoint)
|
||||
const git = simpleGit(path.dirname(gitPath))
|
||||
console.debug(`Using shadow git at: ${gitPath}`)
|
||||
await this.gitOperations.switchToTaskBranch(this.taskId, gitPath)
|
||||
await git.reset(["--hard", commitHash]) // Hard reset to target commit
|
||||
console.debug(`Successfully reset to checkpoint: ${commitHash}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array describing changed files between one commit and either:
|
||||
* - another commit, or
|
||||
* - the current working directory (including uncommitted changes).
|
||||
*
|
||||
* If `rhsHash` is omitted, compares `lhsHash` to the working directory.
|
||||
* If you want truly untracked files to appear, `git add` them first.
|
||||
*
|
||||
* @param lhsHash - The commit to compare from (older commit)
|
||||
* @param rhsHash - The commit to compare to (newer commit).
|
||||
* If omitted, we compare to the working directory.
|
||||
* @returns Array of file changes with before/after content
|
||||
*/
|
||||
public async getDiffSet(
|
||||
lhsHash?: string,
|
||||
rhsHash?: string,
|
||||
): Promise<
|
||||
Array<{
|
||||
relativePath: string
|
||||
absolutePath: string
|
||||
before: string
|
||||
after: string
|
||||
}>
|
||||
> {
|
||||
const gitPath = await getShadowGitPath(this.globalStoragePath, this.taskId, this.cwdHash, this.isLegacyCheckpoint)
|
||||
const git = simpleGit(path.dirname(gitPath))
|
||||
|
||||
if (!this.isLegacyCheckpoint) {
|
||||
await this.gitOperations.switchToTaskBranch(this.taskId, gitPath)
|
||||
}
|
||||
|
||||
console.info(`Getting diff between commits: ${lhsHash || "initial"} -> ${rhsHash || "working directory"}`)
|
||||
|
||||
// If lhsHash is missing, use the initial commit of the repo
|
||||
let baseHash = lhsHash
|
||||
if (!baseHash) {
|
||||
const rootCommit = await git.raw(["rev-list", "--max-parents=0", "HEAD"])
|
||||
baseHash = rootCommit.trim()
|
||||
console.debug(`Using root commit as base: ${baseHash}`)
|
||||
}
|
||||
|
||||
// Stage all changes so that untracked files appear in diff summary
|
||||
await this.gitOperations.addCheckpointFiles(git, gitPath)
|
||||
|
||||
const diffSummary = rhsHash ? await git.diffSummary([`${baseHash}..${rhsHash}`]) : await git.diffSummary([baseHash])
|
||||
console.info(`Found ${diffSummary.files.length} changed files`)
|
||||
|
||||
// For each changed file, gather before/after content
|
||||
const result = []
|
||||
const cwdPath = (await this.getShadowGitConfigWorkTree()) || this.cwd || ""
|
||||
const files = diffSummary.files.map((f) => f.file)
|
||||
const batchSize = 50
|
||||
|
||||
// Get list of files that exist in base commit
|
||||
const existingFiles = await this.getExistingFiles(git, baseHash, files)
|
||||
|
||||
// Process files in batches
|
||||
for (let i = 0; i < files.length; i += batchSize) {
|
||||
const batch = files.slice(i, i + batchSize)
|
||||
|
||||
// Split batch into existing and new files
|
||||
const existingBatch = batch.filter((file) => existingFiles.has(file))
|
||||
const newBatch = batch.filter((file) => !existingFiles.has(file))
|
||||
|
||||
// Get before contents for existing files
|
||||
let beforeContents: string[] = new Array(batch.length).fill("")
|
||||
if (existingBatch.length > 0) {
|
||||
await git.addConfig("core.quotePath", "false")
|
||||
await git.addConfig("core.precomposeunicode", "true")
|
||||
const args = ["show", "--format="]
|
||||
existingBatch.forEach((file) => {
|
||||
args.push(`${baseHash}:${file}`)
|
||||
})
|
||||
const beforeResult = await git.raw(args)
|
||||
const existingContents = beforeResult.split("\n\0\n")
|
||||
// Map contents back to original batch positions
|
||||
existingBatch.forEach((file, index) => {
|
||||
const batchIndex = batch.indexOf(file)
|
||||
if (batchIndex !== -1) {
|
||||
beforeContents[batchIndex] = existingContents[index] || ""
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Get after contents
|
||||
let afterContents: string[] = []
|
||||
if (rhsHash) {
|
||||
// Split after files into existing and new in target commit
|
||||
const afterExistingFiles = await this.getExistingFiles(git, rhsHash, batch)
|
||||
const afterExistingBatch = batch.filter((file) => afterExistingFiles.has(file))
|
||||
|
||||
if (afterExistingBatch.length > 0) {
|
||||
const args = ["show", "--format="]
|
||||
afterExistingBatch.forEach((file) => {
|
||||
args.push(`${rhsHash}:${file}`)
|
||||
})
|
||||
const afterResult = await git.raw(args)
|
||||
const existingContents = afterResult.split("\n\0\n")
|
||||
afterContents = new Array(batch.length).fill("")
|
||||
afterExistingBatch.forEach((file, index) => {
|
||||
const batchIndex = batch.indexOf(file)
|
||||
if (batchIndex !== -1) {
|
||||
afterContents[batchIndex] = existingContents[index] || ""
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Read from disk for working directory changes
|
||||
afterContents = await Promise.all(
|
||||
batch.map(async (filePath) => {
|
||||
try {
|
||||
return await fs.readFile(path.join(cwdPath, filePath), "utf8")
|
||||
} catch (_) {
|
||||
return ""
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Add results for this batch
|
||||
for (let j = 0; j < batch.length; j++) {
|
||||
const filePath = batch[j]
|
||||
const absolutePath = path.join(cwdPath, filePath)
|
||||
result.push({
|
||||
relativePath: filePath,
|
||||
absolutePath,
|
||||
before: beforeContents[j] || "",
|
||||
after: afterContents[j] || "",
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all checkpoint data for a given task.
|
||||
* Handles both legacy checkpoints and branch-per-task checkpoints.
|
||||
*
|
||||
* @param taskId - The ID of the task whose checkpoints should be deleted
|
||||
* @param historyItem - The history item containing the shadow git config for this task
|
||||
* @param globalStoragePath - the globalStorage path
|
||||
* @throws Error if deletion fails
|
||||
*/
|
||||
public static async deleteCheckpoints(taskId: string, historyItem: HistoryItem, globalStoragePath: string): Promise<void> {
|
||||
if (!globalStoragePath) {
|
||||
throw new Error("Global storage uri is invalid")
|
||||
}
|
||||
await GitOperations.deleteTaskBranchStatic(taskId, historyItem, globalStoragePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get a set of files that exist in a given commit
|
||||
*/
|
||||
private async getExistingFiles(git: SimpleGit, commitHash: string, files: string[]): Promise<Set<string>> {
|
||||
try {
|
||||
const result = await git.raw(["ls-tree", "-r", "--name-only", commitHash])
|
||||
const existingFiles = new Set<string>(result.split("\n"))
|
||||
return existingFiles
|
||||
} catch (error) {
|
||||
console.error("Error getting existing files:", error)
|
||||
return new Set()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CheckpointTracker
|
||||
@@ -0,0 +1,159 @@
|
||||
import { mkdir } from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import os from "os"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
|
||||
/**
|
||||
* Gets the path to the legacy shadow Git repository in globalStorage.
|
||||
* Legacy checkpoints stored each task's checkpoints in a separate git repository
|
||||
* under the tasks/{taskId}/checkpoints directory.
|
||||
*
|
||||
* Legacy path structure:
|
||||
* globalStorage/
|
||||
* tasks/
|
||||
* {taskId}/
|
||||
* checkpoints/
|
||||
* .git/
|
||||
*
|
||||
* @param globalStoragePath - The VS Code global storage path
|
||||
* @param taskId - The ID of the task
|
||||
* @returns Promise<string> The absolute path to the legacy shadow git directory
|
||||
* @throws Error if global storage path is invalid
|
||||
*/
|
||||
export async function getLegacyShadowGitPath(globalStoragePath: string, taskId: string): Promise<string> {
|
||||
if (!globalStoragePath) {
|
||||
throw new Error("Global storage uri is invalid")
|
||||
}
|
||||
const checkpointsDir = path.join(globalStoragePath, "tasks", taskId, "checkpoints")
|
||||
await mkdir(checkpointsDir, { recursive: true })
|
||||
const gitPath = path.join(checkpointsDir, ".git")
|
||||
console.info(`Legacy shadow git path: ${gitPath}`)
|
||||
return gitPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the shadow Git repository in globalStorage.
|
||||
* For legacy checkpoints, delegates to getLegacyShadowGitPath().
|
||||
* For new checkpoints, uses the consolidated branch-per-task structure.
|
||||
*
|
||||
* Branch-per-task path structure:
|
||||
* globalStorage/
|
||||
* checkpoints/
|
||||
* {cwdHash}/
|
||||
* .git/
|
||||
*
|
||||
* @param globalStoragePath - The VS Code global storage path
|
||||
* @param taskId - The ID of the task
|
||||
* @param cwdHash - Hash of the working directory path
|
||||
* @param isLegacyCheckpoint - Whether this is a legacy checkpoint
|
||||
* @returns Promise<string> The absolute path to the shadow git directory
|
||||
* @throws Error if global storage path is invalid
|
||||
*/
|
||||
export async function getShadowGitPath(
|
||||
globalStoragePath: string,
|
||||
taskId: string,
|
||||
cwdHash: string,
|
||||
isLegacyCheckpoint: boolean,
|
||||
): Promise<string> {
|
||||
if (isLegacyCheckpoint) {
|
||||
return getLegacyShadowGitPath(globalStoragePath, taskId)
|
||||
}
|
||||
if (!globalStoragePath) {
|
||||
throw new Error("Global storage uri is invalid")
|
||||
}
|
||||
const checkpointsDir = path.join(globalStoragePath, "checkpoints", cwdHash)
|
||||
await mkdir(checkpointsDir, { recursive: true })
|
||||
const gitPath = path.join(checkpointsDir, ".git")
|
||||
return gitPath
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Protected directories:
|
||||
* - User's home directory
|
||||
* - Desktop
|
||||
* - Documents
|
||||
* - Downloads
|
||||
*
|
||||
* @returns Promise<string> The absolute path to the current working directory
|
||||
* @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.")
|
||||
}
|
||||
const homedir = os.homedir()
|
||||
const desktopPath = path.join(homedir, "Desktop")
|
||||
const documentsPath = path.join(homedir, "Documents")
|
||||
const downloadsPath = path.join(homedir, "Downloads")
|
||||
|
||||
switch (cwd) {
|
||||
case homedir:
|
||||
throw new Error("Cannot use checkpoints in home directory")
|
||||
case desktopPath:
|
||||
throw new Error("Cannot use checkpoints in Desktop directory")
|
||||
case documentsPath:
|
||||
throw new Error("Cannot use checkpoints in Documents directory")
|
||||
case downloadsPath:
|
||||
throw new Error("Cannot use checkpoints in Downloads directory")
|
||||
default:
|
||||
return cwd
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the current working directory to a 13-character numeric hash.
|
||||
* @param workingDir - The absolute path to the working directory
|
||||
* @returns A 13-character numeric hash string used to identify the workspace
|
||||
* @throws {Error} If the working directory path is empty or invalid
|
||||
*/
|
||||
export function hashWorkingDir(workingDir: string): string {
|
||||
if (!workingDir) {
|
||||
throw new Error("Working directory path cannot be empty")
|
||||
}
|
||||
let hash = 0
|
||||
for (let i = 0; i < workingDir.length; i++) {
|
||||
hash = (hash * 31 + workingDir.charCodeAt(i)) >>> 0
|
||||
}
|
||||
const bigHash = BigInt(hash)
|
||||
const numericHash = bigHash.toString().slice(0, 13)
|
||||
return numericHash
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a task uses the legacy checkpoint structure.
|
||||
* Legacy checkpoints stored each task's checkpoints in a separate git repository
|
||||
* under the tasks/{taskId}/checkpoints directory. New checkpoints use a single
|
||||
* repository with branches per task.
|
||||
*
|
||||
* @param globalStoragePath - The VS Code global storage path
|
||||
* @param taskId - The ID of the task to check
|
||||
* @returns Promise<boolean> True if task uses legacy checkpoint structure, false otherwise
|
||||
*
|
||||
* Legacy path structure:
|
||||
* globalStorage/
|
||||
* tasks/
|
||||
* {taskId}/
|
||||
* checkpoints/
|
||||
* .git/
|
||||
*
|
||||
* Branch-per-task structure:
|
||||
* globalStorage/
|
||||
* checkpoints/
|
||||
* {cwdHash}/
|
||||
* .git/
|
||||
*/
|
||||
export async function detectLegacyCheckpoint(globalStoragePath: string | undefined, taskId: string): Promise<boolean> {
|
||||
if (!globalStoragePath) {
|
||||
return false
|
||||
}
|
||||
const legacyGitPath = path.join(globalStoragePath, "tasks", taskId, "checkpoints", ".git")
|
||||
const isLegacy = await fileExistsAtPath(legacyGitPath)
|
||||
console.info(`Legacy checkpoint detection result: ${isLegacy}`)
|
||||
return isLegacy
|
||||
}
|
||||
@@ -35,21 +35,26 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
|
||||
})
|
||||
|
||||
if (saveUri) {
|
||||
// Write content to the selected location
|
||||
await vscode.workspace.fs.writeFile(saveUri, Buffer.from(markdownContent))
|
||||
vscode.window.showTextDocument(saveUri, { preview: true })
|
||||
try {
|
||||
// Write content to the selected location
|
||||
await vscode.workspace.fs.writeFile(saveUri, new TextEncoder().encode(markdownContent))
|
||||
vscode.window.showTextDocument(saveUri, { preview: true })
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to save markdown file: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function formatContentBlockToMarkdown(
|
||||
block: Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam,
|
||||
// messages: Anthropic.MessageParam[]
|
||||
): string {
|
||||
export function formatContentBlockToMarkdown(block: Anthropic.ContentBlockParam): string {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
return block.text
|
||||
case "image":
|
||||
return `[Image]`
|
||||
case "document":
|
||||
return `[Document]`
|
||||
case "tool_use":
|
||||
let input: string
|
||||
if (typeof block.input === "object" && block.input !== null) {
|
||||
@@ -61,32 +66,16 @@ export function formatContentBlockToMarkdown(
|
||||
}
|
||||
return `[Tool Use: ${block.name}]\n${input}`
|
||||
case "tool_result":
|
||||
// For now we're not doing tool name lookup since we don't use tools anymore
|
||||
// const toolName = findToolName(block.tool_use_id, messages)
|
||||
const toolName = "Tool"
|
||||
if (typeof block.content === "string") {
|
||||
return `[${toolName}${block.is_error ? " (Error)" : ""}]\n${block.content}`
|
||||
return `[Tool${block.is_error ? " (Error)" : ""}]\n${block.content}`
|
||||
} else if (Array.isArray(block.content)) {
|
||||
return `[${toolName}${block.is_error ? " (Error)" : ""}]\n${block.content
|
||||
return `[Tool${block.is_error ? " (Error)" : ""}]\n${block.content
|
||||
.map((contentBlock) => formatContentBlockToMarkdown(contentBlock))
|
||||
.join("\n")}`
|
||||
} else {
|
||||
return `[${toolName}${block.is_error ? " (Error)" : ""}]`
|
||||
return `[Tool${block.is_error ? " (Error)" : ""}]`
|
||||
}
|
||||
default:
|
||||
return "[Unexpected content type]"
|
||||
}
|
||||
}
|
||||
|
||||
export function findToolName(toolCallId: string, messages: Anthropic.MessageParam[]): string {
|
||||
for (const message of messages) {
|
||||
if (Array.isArray(message.content)) {
|
||||
for (const block of message.content) {
|
||||
if (block.type === "tool_use" && block.id === toolCallId) {
|
||||
return block.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Unknown Tool"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import axios from "axios"
|
||||
import ogs from "open-graph-scraper"
|
||||
|
||||
export interface OpenGraphData {
|
||||
title?: string
|
||||
description?: string
|
||||
image?: string
|
||||
url?: string
|
||||
siteName?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches Open Graph metadata from a URL
|
||||
* @param url The URL to fetch metadata from
|
||||
* @returns Promise resolving to OpenGraphData
|
||||
*/
|
||||
export async function fetchOpenGraphData(url: string): Promise<OpenGraphData> {
|
||||
try {
|
||||
const options = {
|
||||
url: url,
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
"user-agent": "Mozilla/5.0 (compatible; VSCodeExtension/1.0; +https://cline.bot)",
|
||||
},
|
||||
onlyGetOpenGraphInfo: false, // Get all metadata, not just Open Graph
|
||||
fetchOptions: {
|
||||
redirect: "follow", // Follow redirects
|
||||
} as any,
|
||||
}
|
||||
|
||||
const { result } = await ogs(options)
|
||||
|
||||
// Use type assertion to avoid TypeScript errors
|
||||
const data = result as any
|
||||
|
||||
// Handle image URLs
|
||||
let imageUrl = data.ogImage?.[0]?.url || data.twitterImage?.[0]?.url
|
||||
|
||||
// If the image URL is relative, make it absolute
|
||||
if (imageUrl && (imageUrl.startsWith("/") || imageUrl.startsWith("./"))) {
|
||||
try {
|
||||
// Extract the base URL and make the relative URL absolute
|
||||
const urlObj = new URL(url)
|
||||
const baseUrl = `${urlObj.protocol}//${urlObj.hostname}`
|
||||
imageUrl = new URL(imageUrl, baseUrl).href
|
||||
} catch (error) {
|
||||
console.error(`Error converting relative URL to absolute: ${imageUrl}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: data.ogTitle || data.twitterTitle || data.dcTitle || data.title || new URL(url).hostname,
|
||||
description:
|
||||
data.ogDescription ||
|
||||
data.twitterDescription ||
|
||||
data.dcDescription ||
|
||||
data.description ||
|
||||
"No description available",
|
||||
image: imageUrl,
|
||||
url: data.ogUrl || url,
|
||||
siteName: data.ogSiteName || new URL(url).hostname,
|
||||
type: data.ogType,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching Open Graph data for ${url}:`, error)
|
||||
// Return basic information based on the URL
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
return {
|
||||
title: urlObj.hostname,
|
||||
description: url,
|
||||
url: url,
|
||||
siteName: urlObj.hostname,
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
title: url,
|
||||
description: url,
|
||||
url: url,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a URL is an image by making a HEAD request and checking the content type
|
||||
* @param url The URL to check
|
||||
* @returns Promise resolving to boolean indicating if the URL is an image
|
||||
*/
|
||||
export async function isImageUrl(url: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await axios.head(url, {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; VSCodeExtension/1.0; +https://cline.bot)",
|
||||
},
|
||||
timeout: 3000,
|
||||
})
|
||||
|
||||
const contentType = response.headers["content-type"]
|
||||
return contentType && contentType.startsWith("image/")
|
||||
} catch (error) {
|
||||
console.error(`Error checking if URL is an image: ${url}`, error)
|
||||
// If we can't determine, fall back to checking the file extension
|
||||
return /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(url)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Gets the contents of the active terminal
|
||||
* @returns The terminal contents as a string
|
||||
*/
|
||||
export async function getLatestTerminalOutput(): Promise<string> {
|
||||
// Store original clipboard content to restore later
|
||||
const originalClipboard = await vscode.env.clipboard.readText()
|
||||
|
||||
try {
|
||||
// Select terminal content
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.selectAll")
|
||||
|
||||
// Copy selection to clipboard
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
|
||||
|
||||
// Clear the selection
|
||||
await vscode.commands.executeCommand("workbench.action.terminal.clearSelection")
|
||||
|
||||
// Get terminal contents from clipboard
|
||||
let terminalContents = (await vscode.env.clipboard.readText()).trim()
|
||||
|
||||
// Check if there's actually a terminal open
|
||||
if (terminalContents === originalClipboard) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Clean up command separation
|
||||
const lines = terminalContents.split("\n")
|
||||
const lastLine = lines.pop()?.trim()
|
||||
if (lastLine) {
|
||||
let i = lines.length - 1
|
||||
while (i >= 0 && !lines[i].trim().startsWith(lastLine)) {
|
||||
i--
|
||||
}
|
||||
terminalContents = lines.slice(Math.max(i, 0)).join("\n")
|
||||
}
|
||||
|
||||
return terminalContents
|
||||
} finally {
|
||||
// Restore original clipboard content
|
||||
await vscode.env.clipboard.writeText(originalClipboard)
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,15 @@ export class BrowserSession {
|
||||
await fs.mkdir(puppeteerDir, { recursive: true })
|
||||
}
|
||||
|
||||
// if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
|
||||
// if it does exist it will return the path to existing chromium
|
||||
const stats: PCRStats = await PCR({
|
||||
downloadPath: puppeteerDir,
|
||||
})
|
||||
const chromeExecutablePath = vscode.workspace.getConfiguration("cline").get<string>("chromeExecutablePath")
|
||||
if (chromeExecutablePath && !(await fileExistsAtPath(chromeExecutablePath))) {
|
||||
throw new Error(`Chrome executable not found at path: ${chromeExecutablePath}`)
|
||||
}
|
||||
const stats: PCRStats = chromeExecutablePath
|
||||
? { puppeteer: require("puppeteer-core"), executablePath: chromeExecutablePath }
|
||||
: // if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots")
|
||||
// if it does exist it will return the path to existing chromium
|
||||
await PCR({ downloadPath: puppeteerDir })
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
|
||||
".*", // '!**/.*' excludes hidden directories, while '!**/.*/**' excludes only their contents. This way we are at least aware of the existence of hidden directories.
|
||||
].map((dir) => `**/${dir}/**`)
|
||||
|
||||
const options = {
|
||||
const options: Options = {
|
||||
cwd: dirPath,
|
||||
dot: true, // do not ignore hidden files/directories
|
||||
absolute: true,
|
||||
@@ -44,6 +44,7 @@ export async function listFiles(dirPath: string, recursive: boolean, limit: numb
|
||||
gitignore: recursive, // globby ignores any files that are gitignored
|
||||
ignore: recursive ? dirsToIgnore : undefined, // just in case there is no gitignore, we ignore sensible defaults
|
||||
onlyFiles: false, // true by default, false means it will list directories on their own too
|
||||
suppressErrors: true,
|
||||
}
|
||||
|
||||
// * globs all files in one dir, ** globs files in nested directories
|
||||
|
||||
+85
-29
@@ -16,6 +16,7 @@ import * as vscode from "vscode"
|
||||
import { z } from "zod"
|
||||
import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
|
||||
import {
|
||||
DEFAULT_MCP_TIMEOUT_SECONDS,
|
||||
McpMode,
|
||||
McpResource,
|
||||
McpResourceResponse,
|
||||
@@ -23,10 +24,11 @@ import {
|
||||
McpServer,
|
||||
McpTool,
|
||||
McpToolCallResponse,
|
||||
MIN_MCP_TIMEOUT_SECONDS,
|
||||
} from "../../shared/mcp"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { arePathsEqual } from "../../utils/path"
|
||||
|
||||
import { secondsToMs } from "../../utils/time"
|
||||
export type McpConnection = {
|
||||
server: McpServer
|
||||
client: Client
|
||||
@@ -35,13 +37,13 @@ export type McpConnection = {
|
||||
|
||||
const AutoApproveSchema = z.array(z.string()).default([])
|
||||
|
||||
// StdioServerParameters
|
||||
const StdioConfigSchema = z.object({
|
||||
command: z.string(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.record(z.string()).optional(),
|
||||
autoApprove: AutoApproveSchema.optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
timeout: z.number().min(MIN_MCP_TIMEOUT_SECONDS).optional().default(DEFAULT_MCP_TIMEOUT_SECONDS),
|
||||
})
|
||||
|
||||
const McpSettingsSchema = z.object({
|
||||
@@ -242,28 +244,6 @@ export class McpHub {
|
||||
}
|
||||
transport.start = async () => {} // No-op now, .connect() won't fail
|
||||
|
||||
// // Set up notification handlers
|
||||
// client.setNotificationHandler(
|
||||
// // @ts-ignore-next-line
|
||||
// { method: "notifications/tools/list_changed" },
|
||||
// async () => {
|
||||
// console.log(`Tools changed for server: ${name}`)
|
||||
// connection.server.tools = await this.fetchTools(name)
|
||||
// await this.notifyWebviewOfServerChanges()
|
||||
// },
|
||||
// )
|
||||
|
||||
// client.setNotificationHandler(
|
||||
// // @ts-ignore-next-line
|
||||
// { method: "notifications/resources/list_changed" },
|
||||
// async () => {
|
||||
// console.log(`Resources changed for server: ${name}`)
|
||||
// connection.server.resources = await this.fetchResources(name)
|
||||
// connection.server.resourceTemplates = await this.fetchResourceTemplates(name)
|
||||
// await this.notifyWebviewOfServerChanges()
|
||||
// },
|
||||
// )
|
||||
|
||||
// Connect
|
||||
await client.connect(transport)
|
||||
connection.server.status = "connected"
|
||||
@@ -343,10 +323,6 @@ export class McpHub {
|
||||
const connection = this.connections.find((conn) => conn.server.name === name)
|
||||
if (connection) {
|
||||
try {
|
||||
// connection.client.removeNotificationHandler("notifications/tools/list_changed")
|
||||
// connection.client.removeNotificationHandler("notifications/resources/list_changed")
|
||||
// connection.client.removeNotificationHandler("notifications/stderr")
|
||||
// connection.client.removeNotificationHandler("notifications/stderr")
|
||||
await connection.transport.close()
|
||||
await connection.client.close()
|
||||
} catch (error) {
|
||||
@@ -472,6 +448,10 @@ export class McpHub {
|
||||
})
|
||||
}
|
||||
|
||||
async sendLatestMcpServers() {
|
||||
await this.notifyWebviewOfServerChanges()
|
||||
}
|
||||
|
||||
// Using server
|
||||
|
||||
// Public methods for server management
|
||||
@@ -559,6 +539,7 @@ export class McpHub {
|
||||
if (connection.server.disabled) {
|
||||
throw new Error(`Server "${serverName}" is disabled`)
|
||||
}
|
||||
|
||||
return await connection.client.request(
|
||||
{
|
||||
method: "resources/read",
|
||||
@@ -582,6 +563,16 @@ export class McpHub {
|
||||
throw new Error(`Server "${serverName}" is disabled and cannot be used`)
|
||||
}
|
||||
|
||||
let timeout = secondsToMs(DEFAULT_MCP_TIMEOUT_SECONDS) // sdk expects ms
|
||||
|
||||
try {
|
||||
const config = JSON.parse(connection.server.config)
|
||||
const parsedConfig = StdioConfigSchema.parse(config)
|
||||
timeout = secondsToMs(parsedConfig.timeout)
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse timeout configuration for server ${serverName}: ${error}`)
|
||||
}
|
||||
|
||||
return await connection.client.request(
|
||||
{
|
||||
method: "tools/call",
|
||||
@@ -591,6 +582,9 @@ export class McpHub {
|
||||
},
|
||||
},
|
||||
CallToolResultSchema,
|
||||
{
|
||||
timeout,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -616,7 +610,6 @@ export class McpHub {
|
||||
autoApprove.splice(toolIndex, 1)
|
||||
}
|
||||
|
||||
// Write updated config back to file
|
||||
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
|
||||
|
||||
// Update the tools list to reflect the change
|
||||
@@ -632,6 +625,69 @@ export class McpHub {
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteServer(serverName: string) {
|
||||
try {
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
||||
config.mcpServers = {}
|
||||
}
|
||||
if (config.mcpServers[serverName]) {
|
||||
delete config.mcpServers[serverName]
|
||||
const updatedConfig = {
|
||||
mcpServers: config.mcpServers,
|
||||
}
|
||||
await fs.writeFile(settingsPath, JSON.stringify(updatedConfig, null, 2))
|
||||
await this.updateServerConnections(config.mcpServers)
|
||||
vscode.window.showInformationMessage(`Deleted ${serverName} MCP server`)
|
||||
} else {
|
||||
vscode.window.showWarningMessage(`${serverName} not found in MCP configuration`)
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to delete MCP server: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public async updateServerTimeout(serverName: string, timeout: number): Promise<void> {
|
||||
try {
|
||||
// Validate timeout against schema
|
||||
const setConfigResult = StdioConfigSchema.shape.timeout.safeParse(timeout)
|
||||
if (!setConfigResult.success) {
|
||||
throw new Error(`Invalid timeout value: ${timeout}. Must be at minimum ${MIN_MCP_TIMEOUT_SECONDS} seconds.`)
|
||||
}
|
||||
|
||||
const settingsPath = await this.getMcpSettingsFilePath()
|
||||
const content = await fs.readFile(settingsPath, "utf-8")
|
||||
const config = JSON.parse(content)
|
||||
|
||||
if (!config.mcpServers?.[serverName]) {
|
||||
throw new Error(`Server "${serverName}" not found in settings`)
|
||||
}
|
||||
|
||||
config.mcpServers[serverName] = {
|
||||
...config.mcpServers[serverName],
|
||||
timeout,
|
||||
}
|
||||
|
||||
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
|
||||
|
||||
await this.updateServerConnections(config.mcpServers)
|
||||
} catch (error) {
|
||||
console.error("Failed to update server timeout:", error)
|
||||
if (error instanceof Error) {
|
||||
console.error("Error details:", error.message, error.stack)
|
||||
}
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to update server timeout: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.removeAllFileWatchers()
|
||||
for (const connection of this.connections) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { PostHog } from "posthog-node"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
class PostHogClient {
|
||||
private static instance: PostHogClient
|
||||
private client: PostHog
|
||||
private distinctId: string = vscode.env.machineId
|
||||
private telemetryEnabled: boolean = false
|
||||
|
||||
private constructor() {
|
||||
this.client = new PostHog("phc_qfOAGxZw2TL5O8p9KYd9ak3bPBFzfjC8fy5L6jNWY7K", {
|
||||
host: "https://us.i.posthog.com",
|
||||
enableExceptionAutocapture: false,
|
||||
})
|
||||
}
|
||||
|
||||
public updateTelemetryState(didUserOptIn: boolean): void {
|
||||
this.telemetryEnabled = false
|
||||
|
||||
// First check global telemetry level - telemetry should only be enabled when level is "all"
|
||||
const telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
|
||||
const globalTelemetryEnabled = telemetryLevel === "all"
|
||||
|
||||
// We only enable telemetry if global vscode telemetry is enabled
|
||||
if (globalTelemetryEnabled) {
|
||||
this.telemetryEnabled = didUserOptIn
|
||||
}
|
||||
|
||||
// Update PostHog client state based on telemetry preference and use machineId to tie it to the webview
|
||||
if (this.telemetryEnabled) {
|
||||
this.client.optIn()
|
||||
this.client.identify({ distinctId: this.distinctId })
|
||||
// console.log("Telemetry enabled")
|
||||
} else {
|
||||
this.client.optOut()
|
||||
// console.log("Telemetry disabled")
|
||||
}
|
||||
}
|
||||
|
||||
public static getInstance(): PostHogClient {
|
||||
if (!PostHogClient.instance) {
|
||||
PostHogClient.instance = new PostHogClient()
|
||||
}
|
||||
return PostHogClient.instance
|
||||
}
|
||||
|
||||
public capture(event: { event: string; properties?: any }): void {
|
||||
// Only send events if telemetry is enabled
|
||||
if (this.telemetryEnabled) {
|
||||
this.client.capture({ distinctId: this.distinctId, event: event.event, properties: event.properties })
|
||||
// console.log("Captured event", { distinctId: this.distinctId, event: event.event, properties: event.properties })
|
||||
}
|
||||
}
|
||||
|
||||
public isTelemetryEnabled(): boolean {
|
||||
return this.telemetryEnabled
|
||||
}
|
||||
|
||||
public async shutdown(): Promise<void> {
|
||||
await this.client.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
// Export a single instance
|
||||
export const telemetryService = PostHogClient.getInstance()
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ChatContent {
|
||||
message?: string
|
||||
images?: string[]
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
// type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonClicked' or 'settingsButtonClicked' or 'hello'
|
||||
|
||||
import { GitCommit } from "../utils/git"
|
||||
import { ApiConfiguration, ModelInfo } from "./api"
|
||||
import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { HistoryItem } from "./HistoryItem"
|
||||
import { McpServer } from "./mcp"
|
||||
import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp"
|
||||
import { TelemetrySetting } from "./TelemetrySetting"
|
||||
|
||||
// webview will hold state
|
||||
export interface ExtensionMessage {
|
||||
@@ -26,6 +28,11 @@ export interface ExtensionMessage {
|
||||
| "vsCodeLmModels"
|
||||
| "requestVsCodeLmModels"
|
||||
| "emailSubscribed"
|
||||
| "mcpMarketplaceCatalog"
|
||||
| "mcpDownloadDetails"
|
||||
| "commitSearchResults"
|
||||
| "openGraphData"
|
||||
| "isImageUrlResult"
|
||||
text?: string
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
@@ -46,6 +53,20 @@ export interface ExtensionMessage {
|
||||
openRouterModels?: Record<string, ModelInfo>
|
||||
openAiModels?: string[]
|
||||
mcpServers?: McpServer[]
|
||||
mcpMarketplaceCatalog?: McpMarketplaceCatalog
|
||||
error?: string
|
||||
mcpDownloadDetails?: McpDownloadResponse
|
||||
commits?: GitCommit[]
|
||||
openGraphData?: {
|
||||
title?: string
|
||||
description?: string
|
||||
image?: string
|
||||
url?: string
|
||||
siteName?: string
|
||||
type?: string
|
||||
}
|
||||
url?: string
|
||||
isImage?: boolean
|
||||
}
|
||||
|
||||
export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown"
|
||||
@@ -72,6 +93,9 @@ export interface ExtensionState {
|
||||
email: string | null
|
||||
photoURL: string | null
|
||||
}
|
||||
mcpMarketplaceEnabled?: boolean
|
||||
telemetrySetting: TelemetrySetting
|
||||
vscMachineId: string
|
||||
}
|
||||
|
||||
export interface ClineMessage {
|
||||
@@ -84,6 +108,7 @@ export interface ClineMessage {
|
||||
images?: string[]
|
||||
partial?: boolean
|
||||
lastCheckpointHash?: string
|
||||
isCheckpointCheckedOut?: boolean
|
||||
conversationHistoryIndex?: number
|
||||
conversationHistoryDeletedRange?: [number, number] // for when conversation history is truncated for API requests
|
||||
}
|
||||
@@ -127,6 +152,7 @@ export type ClineSay =
|
||||
| "diff_error"
|
||||
| "deleted_api_reqs"
|
||||
| "clineignore_error"
|
||||
| "checkpoint_created"
|
||||
|
||||
export interface ClineSayTool {
|
||||
tool:
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
export type LanguageKey =
|
||||
| "en"
|
||||
| "ar"
|
||||
| "pt-BR"
|
||||
| "cs"
|
||||
| "fr"
|
||||
| "de"
|
||||
| "hi"
|
||||
| "hu"
|
||||
| "it"
|
||||
| "ja"
|
||||
| "ko"
|
||||
| "pl"
|
||||
| "pt-PT"
|
||||
| "ru"
|
||||
| "zh-CN"
|
||||
| "es"
|
||||
| "zh-TW"
|
||||
| "tr"
|
||||
|
||||
export type LanguageDisplay =
|
||||
| "English"
|
||||
| "Arabic - العربية"
|
||||
| "Portuguese - Português (Brasil)"
|
||||
| "Czech - Čeština"
|
||||
| "French - Français"
|
||||
| "German - Deutsch"
|
||||
| "Hindi - हिन्दी"
|
||||
| "Hungarian - Magyar"
|
||||
| "Italian - Italiano"
|
||||
| "Japanese - 日本語"
|
||||
| "Korean - 한국어"
|
||||
| "Polish - Polski"
|
||||
| "Portuguese - Português (Portugal)"
|
||||
| "Russian - Русский"
|
||||
| "Simplified Chinese - 简体中文"
|
||||
| "Spanish - Español"
|
||||
| "Traditional Chinese - 繁體中文"
|
||||
| "Turkish - Türkçe"
|
||||
|
||||
export const DEFAULT_LANGUAGE_SETTINGS: LanguageKey = "en"
|
||||
|
||||
export const languageOptions: { key: LanguageKey; display: LanguageDisplay }[] = [
|
||||
{ key: "en", display: "English" },
|
||||
{ key: "ar", display: "Arabic - العربية" },
|
||||
{ key: "pt-BR", display: "Portuguese - Português (Brasil)" },
|
||||
{ key: "cs", display: "Czech - Čeština" },
|
||||
{ key: "fr", display: "French - Français" },
|
||||
{ key: "de", display: "German - Deutsch" },
|
||||
{ key: "hi", display: "Hindi - हिन्दी" },
|
||||
{ key: "hu", display: "Hungarian - Magyar" },
|
||||
{ key: "it", display: "Italian - Italiano" },
|
||||
{ key: "ja", display: "Japanese - 日本語" },
|
||||
{ key: "ko", display: "Korean - 한국어" },
|
||||
{ key: "pl", display: "Polish - Polski" },
|
||||
{ key: "pt-PT", display: "Portuguese - Português (Portugal)" },
|
||||
{ key: "ru", display: "Russian - Русский" },
|
||||
{ key: "zh-CN", display: "Simplified Chinese - 简体中文" },
|
||||
{ key: "es", display: "Spanish - Español" },
|
||||
{ key: "zh-TW", display: "Traditional Chinese - 繁體中文" },
|
||||
{ key: "tr", display: "Turkish - Türkçe" },
|
||||
]
|
||||
|
||||
export function getLanguageKey(display: LanguageDisplay | undefined): LanguageKey {
|
||||
if (!display) {
|
||||
return DEFAULT_LANGUAGE_SETTINGS
|
||||
}
|
||||
const languageOption = languageOptions.find((option) => option.display === display)
|
||||
if (languageOption) {
|
||||
return languageOption.key
|
||||
}
|
||||
return DEFAULT_LANGUAGE_SETTINGS
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type TelemetrySetting = "unset" | "enabled" | "disabled"
|
||||
@@ -2,6 +2,7 @@ import { ApiConfiguration } from "./api"
|
||||
import { AutoApprovalSettings } from "./AutoApprovalSettings"
|
||||
import { BrowserSettings } from "./BrowserSettings"
|
||||
import { ChatSettings } from "./ChatSettings"
|
||||
import { ChatContent } from "./ChatContent"
|
||||
|
||||
export interface WebviewMessage {
|
||||
type:
|
||||
@@ -21,6 +22,7 @@ export interface WebviewMessage {
|
||||
| "requestOllamaModels"
|
||||
| "requestLmStudioModels"
|
||||
| "openImage"
|
||||
| "openInBrowser"
|
||||
| "openFile"
|
||||
| "openMention"
|
||||
| "cancelTask"
|
||||
@@ -28,9 +30,10 @@ export interface WebviewMessage {
|
||||
| "refreshOpenAiModels"
|
||||
| "openMcpSettings"
|
||||
| "restartMcpServer"
|
||||
| "deleteMcpServer"
|
||||
| "autoApprovalSettings"
|
||||
| "browserSettings"
|
||||
| "chatSettings"
|
||||
| "togglePlanActMode"
|
||||
| "checkpointDiff"
|
||||
| "checkpointRestore"
|
||||
| "taskCompletionViewChanges"
|
||||
@@ -42,6 +45,17 @@ export interface WebviewMessage {
|
||||
| "accountLoginClicked"
|
||||
| "accountLogoutClicked"
|
||||
| "subscribeEmail"
|
||||
| "fetchMcpMarketplace"
|
||||
| "downloadMcp"
|
||||
| "silentlyRefreshMcpMarketplace"
|
||||
| "searchCommits"
|
||||
| "showMcpView"
|
||||
| "fetchLatestMcpServersFromHub"
|
||||
| "telemetrySetting"
|
||||
| "openSettings"
|
||||
| "updateMcpTimeout"
|
||||
| "fetchOpenGraphData"
|
||||
| "checkIsImageUrl"
|
||||
// | "relaunchChromeDebugMode"
|
||||
text?: string
|
||||
disabled?: boolean
|
||||
@@ -53,11 +67,16 @@ export interface WebviewMessage {
|
||||
autoApprovalSettings?: AutoApprovalSettings
|
||||
browserSettings?: BrowserSettings
|
||||
chatSettings?: ChatSettings
|
||||
|
||||
chatContent?: ChatContent
|
||||
mcpId?: string
|
||||
timeout?: number
|
||||
// For toggleToolAutoApprove
|
||||
serverName?: string
|
||||
toolName?: string
|
||||
autoApprove?: boolean
|
||||
|
||||
// For openInBrowser
|
||||
url?: string
|
||||
}
|
||||
|
||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||
|
||||
+432
-60
@@ -15,12 +15,15 @@ export type ApiProvider =
|
||||
| "mistral"
|
||||
| "vscode-lm"
|
||||
| "litellm"
|
||||
| "asksage"
|
||||
| "xai"
|
||||
|
||||
export interface ApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
apiKey?: string // anthropic
|
||||
liteLlmBaseUrl?: string
|
||||
liteLlmModelId?: string
|
||||
liteLlmApiKey?: string
|
||||
anthropicBaseUrl?: string
|
||||
openRouterApiKey?: string
|
||||
openRouterModelId?: string
|
||||
@@ -30,6 +33,7 @@ export interface ApiHandlerOptions {
|
||||
awsSessionToken?: string
|
||||
awsRegion?: string
|
||||
awsUseCrossRegionInference?: boolean
|
||||
awsBedrockUsePromptCache?: boolean
|
||||
awsUseProfile?: boolean
|
||||
awsProfile?: string
|
||||
vertexProjectId?: string
|
||||
@@ -37,8 +41,10 @@ export interface ApiHandlerOptions {
|
||||
openAiBaseUrl?: string
|
||||
openAiApiKey?: string
|
||||
openAiModelId?: string
|
||||
openAiModelInfo?: ModelInfo
|
||||
ollamaModelId?: string
|
||||
ollamaBaseUrl?: string
|
||||
ollamaApiOptionsCtxNum?: string
|
||||
lmStudioModelId?: string
|
||||
lmStudioBaseUrl?: string
|
||||
geminiApiKey?: string
|
||||
@@ -54,6 +60,10 @@ export interface ApiHandlerOptions {
|
||||
vsCodeLmModelSelector?: any
|
||||
o3MiniReasoningEffort?: string
|
||||
qwenApiLine?: string
|
||||
asksageApiUrl?: string
|
||||
asksageApiKey?: string
|
||||
xaiApiKey?: string
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
export type ApiConfiguration = ApiHandlerOptions & {
|
||||
@@ -78,8 +88,19 @@ export interface ModelInfo {
|
||||
// Anthropic
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models // prices updated 2025-01-02
|
||||
export type AnthropicModelId = keyof typeof anthropicModels
|
||||
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-5-sonnet-20241022"
|
||||
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219"
|
||||
export const anthropicModels = {
|
||||
"claude-3-7-sonnet-20250219": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
},
|
||||
"claude-3-5-sonnet-20241022": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
@@ -126,24 +147,39 @@ export const anthropicModels = {
|
||||
// AWS Bedrock
|
||||
// https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html
|
||||
export type BedrockModelId = keyof typeof bedrockModels
|
||||
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0"
|
||||
export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-7-sonnet-20250219-v1:0"
|
||||
export const bedrockModels = {
|
||||
"anthropic.claude-3-7-sonnet-20250219-v1:0": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
},
|
||||
"anthropic.claude-3-5-sonnet-20241022-v2:0": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
},
|
||||
"anthropic.claude-3-5-haiku-20241022-v1:0": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.0,
|
||||
outputPrice: 5.0,
|
||||
cacheWritesPrice: 1.0,
|
||||
cacheReadsPrice: 0.08,
|
||||
},
|
||||
"anthropic.claude-3-5-sonnet-20240620-v1:0": {
|
||||
maxTokens: 8192,
|
||||
@@ -181,7 +217,7 @@ export const bedrockModels = {
|
||||
|
||||
// OpenRouter
|
||||
// https://openrouter.ai/models?order=newest&supported_parameters=tools
|
||||
export const openRouterDefaultModelId = "anthropic/claude-3.5-sonnet" // will always exist in openRouterModels
|
||||
export const openRouterDefaultModelId = "anthropic/claude-3.7-sonnet" // will always exist in openRouterModels
|
||||
export const openRouterDefaultModelInfo: ModelInfo = {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
@@ -193,54 +229,145 @@ export const openRouterDefaultModelInfo: ModelInfo = {
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
description:
|
||||
"The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal",
|
||||
"Claude 3.7 Sonnet is an advanced large language model with improved reasoning, coding, and problem-solving capabilities. It introduces a hybrid reasoning approach, allowing users to choose between rapid responses and extended, step-by-step processing for complex tasks. The model demonstrates notable improvements in coding, particularly in front-end development and full-stack updates, and excels in agentic workflows, where it can autonomously navigate multi-step processes. \n\nClaude 3.7 Sonnet maintains performance parity with its predecessor in standard mode while offering an extended reasoning mode for enhanced accuracy in math, coding, and instruction-following tasks.\n\nRead more at the [blog post here](https://www.anthropic.com/news/claude-3-7-sonnet)",
|
||||
}
|
||||
|
||||
// Vertex AI
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models
|
||||
export type VertexModelId = keyof typeof vertexModels
|
||||
export const vertexDefaultModelId: VertexModelId = "claude-3-5-sonnet-v2@20241022"
|
||||
export const vertexDefaultModelId: VertexModelId = "claude-3-7-sonnet@20250219"
|
||||
export const vertexModels = {
|
||||
"claude-3-7-sonnet@20250219": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
},
|
||||
"claude-3-5-sonnet-v2@20241022": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
},
|
||||
"claude-3-5-sonnet@20240620": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
},
|
||||
"claude-3-5-haiku@20241022": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.0,
|
||||
outputPrice: 5.0,
|
||||
cacheWritesPrice: 1.25,
|
||||
cacheReadsPrice: 0.1,
|
||||
},
|
||||
"claude-3-opus@20240229": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 15.0,
|
||||
outputPrice: 75.0,
|
||||
cacheWritesPrice: 18.75,
|
||||
cacheReadsPrice: 1.5,
|
||||
},
|
||||
"claude-3-haiku@20240307": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.25,
|
||||
outputPrice: 1.25,
|
||||
cacheWritesPrice: 0.3,
|
||||
cacheReadsPrice: 0.03,
|
||||
},
|
||||
"gemini-2.0-flash-001": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.1,
|
||||
outputPrice: 0.4,
|
||||
},
|
||||
"gemini-2.0-flash-thinking-exp-1219": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 32_767,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-2.0-flash-exp": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-exp-1206": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 2_097_152,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-1.5-flash-002": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-1.5-flash-exp-0827": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-1.5-flash-8b-exp-0827": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 1_048_576,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-1.5-pro-002": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 2_097_152,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gemini-1.5-pro-exp-0827": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 2_097_152,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
@@ -365,9 +492,10 @@ export const openAiNativeModels = {
|
||||
maxTokens: 100_000,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
cacheReadsPrice: 0.55,
|
||||
},
|
||||
// don't support tool use yet
|
||||
o1: {
|
||||
@@ -377,38 +505,51 @@ export const openAiNativeModels = {
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 15,
|
||||
outputPrice: 60,
|
||||
cacheReadsPrice: 7.5,
|
||||
},
|
||||
"o1-preview": {
|
||||
maxTokens: 32_768,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 15,
|
||||
outputPrice: 60,
|
||||
cacheReadsPrice: 7.5,
|
||||
},
|
||||
"o1-mini": {
|
||||
maxTokens: 65_536,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 1.1,
|
||||
outputPrice: 4.4,
|
||||
cacheReadsPrice: 0.55,
|
||||
},
|
||||
"gpt-4o": {
|
||||
maxTokens: 4_096,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 2.5,
|
||||
outputPrice: 10,
|
||||
cacheReadsPrice: 1.25,
|
||||
},
|
||||
"gpt-4o-mini": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.15,
|
||||
outputPrice: 0.6,
|
||||
cacheReadsPrice: 0.075,
|
||||
},
|
||||
"gpt-4.5-preview": {
|
||||
maxTokens: 16_384,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 75,
|
||||
outputPrice: 150,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
@@ -426,18 +567,18 @@ export const deepSeekModels = {
|
||||
maxTokens: 8_000,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true, // supports context caching, but not in the way anthropic does it (deepseek reports input tokens and reads/writes in the same usage report) FIXME: we need to show users cache stats how deepseek does it
|
||||
inputPrice: 0, // technically there is no input price, it's all either a cache hit or miss (ApiOptions will not show this)
|
||||
outputPrice: 0.28,
|
||||
cacheWritesPrice: 0.14,
|
||||
cacheReadsPrice: 0.014,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.27,
|
||||
outputPrice: 1.1,
|
||||
cacheWritesPrice: 0.27,
|
||||
cacheReadsPrice: 0.07,
|
||||
},
|
||||
"deepseek-reasoner": {
|
||||
maxTokens: 8_000,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true, // supports context caching, but not in the way anthropic does it (deepseek reports input tokens and reads/writes in the same usage report) FIXME: we need to show users cache stats how deepseek does it
|
||||
inputPrice: 0, // technically there is no input price, it's all either a cache hit or miss (ApiOptions will not show this)
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0.55,
|
||||
outputPrice: 2.19,
|
||||
cacheWritesPrice: 0.55,
|
||||
cacheReadsPrice: 0.14,
|
||||
@@ -449,85 +590,205 @@ export const deepSeekModels = {
|
||||
export type QwenModelId = keyof typeof qwenModels
|
||||
export const qwenDefaultModelId: QwenModelId = "qwen-coder-plus-latest"
|
||||
export const qwenModels = {
|
||||
"qwen2.5-coder-32b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.002,
|
||||
outputPrice: 0.006,
|
||||
cacheWritesPrice: 0.002,
|
||||
cacheReadsPrice: 0.006,
|
||||
},
|
||||
"qwen2.5-coder-14b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.002,
|
||||
outputPrice: 0.006,
|
||||
cacheWritesPrice: 0.002,
|
||||
cacheReadsPrice: 0.006,
|
||||
},
|
||||
"qwen2.5-coder-7b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.001,
|
||||
outputPrice: 0.002,
|
||||
cacheWritesPrice: 0.001,
|
||||
cacheReadsPrice: 0.002,
|
||||
},
|
||||
"qwen2.5-coder-3b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
cacheWritesPrice: 0.0,
|
||||
cacheReadsPrice: 0.0,
|
||||
},
|
||||
"qwen2.5-coder-1.5b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
cacheWritesPrice: 0.0,
|
||||
cacheReadsPrice: 0.0,
|
||||
},
|
||||
"qwen2.5-coder-0.5b-instruct": {
|
||||
maxTokens: 8_192,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0,
|
||||
outputPrice: 0.0,
|
||||
cacheWritesPrice: 0.0,
|
||||
cacheReadsPrice: 0.0,
|
||||
},
|
||||
"qwen-coder-plus-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0035,
|
||||
outputPrice: 0.007,
|
||||
cacheWritesPrice: 0.0035,
|
||||
cacheReadsPrice: 0.007,
|
||||
inputPrice: 3.5,
|
||||
outputPrice: 7,
|
||||
cacheWritesPrice: 3.5,
|
||||
cacheReadsPrice: 7,
|
||||
},
|
||||
"qwen-plus-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0008,
|
||||
outputPrice: 0.002,
|
||||
cacheWritesPrice: 0.0004,
|
||||
cacheReadsPrice: 0.001,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 2,
|
||||
cacheWritesPrice: 0.8,
|
||||
cacheReadsPrice: 0.2,
|
||||
},
|
||||
"qwen-turbo-latest": {
|
||||
maxTokens: 1_000_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0003,
|
||||
outputPrice: 0.0006,
|
||||
cacheWritesPrice: 0.00015,
|
||||
cacheReadsPrice: 0.0003,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 2,
|
||||
cacheWritesPrice: 0.8,
|
||||
cacheReadsPrice: 2,
|
||||
},
|
||||
"qwen-max-latest": {
|
||||
maxTokens: 30_720,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0112,
|
||||
outputPrice: 0.0448,
|
||||
cacheWritesPrice: 0.0056,
|
||||
cacheReadsPrice: 0.0224,
|
||||
inputPrice: 2.4,
|
||||
outputPrice: 9.6,
|
||||
cacheWritesPrice: 2.4,
|
||||
cacheReadsPrice: 9.6,
|
||||
},
|
||||
"qwen-coder-plus": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0035,
|
||||
outputPrice: 0.007,
|
||||
cacheWritesPrice: 0.0035,
|
||||
cacheReadsPrice: 0.007,
|
||||
inputPrice: 3.5,
|
||||
outputPrice: 7,
|
||||
cacheWritesPrice: 3.5,
|
||||
cacheReadsPrice: 7,
|
||||
},
|
||||
"qwen-plus": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0008,
|
||||
outputPrice: 0.002,
|
||||
cacheWritesPrice: 0.0004,
|
||||
cacheReadsPrice: 0.001,
|
||||
inputPrice: 0.8,
|
||||
outputPrice: 2,
|
||||
cacheWritesPrice: 0.8,
|
||||
cacheReadsPrice: 0.2,
|
||||
},
|
||||
"qwen-turbo": {
|
||||
maxTokens: 1_000_000,
|
||||
contextWindow: 1_000_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0003,
|
||||
outputPrice: 0.0006,
|
||||
cacheWritesPrice: 0.00015,
|
||||
cacheReadsPrice: 0.0003,
|
||||
inputPrice: 0.3,
|
||||
outputPrice: 0.6,
|
||||
cacheWritesPrice: 0.3,
|
||||
cacheReadsPrice: 0.6,
|
||||
},
|
||||
"qwen-max": {
|
||||
maxTokens: 30_720,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0.0112,
|
||||
outputPrice: 0.0448,
|
||||
cacheWritesPrice: 0.0056,
|
||||
cacheReadsPrice: 0.0224,
|
||||
inputPrice: 2.4,
|
||||
outputPrice: 9.6,
|
||||
cacheWritesPrice: 2.4,
|
||||
cacheReadsPrice: 9.6,
|
||||
},
|
||||
"deepseek-v3": {
|
||||
maxTokens: 8_000,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0.28,
|
||||
cacheWritesPrice: 0.14,
|
||||
cacheReadsPrice: 0.014,
|
||||
},
|
||||
"deepseek-r1": {
|
||||
maxTokens: 8_000,
|
||||
contextWindow: 64_000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 0,
|
||||
outputPrice: 2.19,
|
||||
cacheWritesPrice: 0.55,
|
||||
cacheReadsPrice: 0.14,
|
||||
},
|
||||
"qwen-vl-max": {
|
||||
maxTokens: 30_720,
|
||||
contextWindow: 32_768,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3,
|
||||
outputPrice: 9,
|
||||
cacheWritesPrice: 3,
|
||||
cacheReadsPrice: 9,
|
||||
},
|
||||
"qwen-vl-max-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3,
|
||||
outputPrice: 9,
|
||||
cacheWritesPrice: 3,
|
||||
cacheReadsPrice: 9,
|
||||
},
|
||||
"qwen-vl-plus": {
|
||||
maxTokens: 6_000,
|
||||
contextWindow: 8_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.5,
|
||||
outputPrice: 4.5,
|
||||
cacheWritesPrice: 1.5,
|
||||
cacheReadsPrice: 4.5,
|
||||
},
|
||||
"qwen-vl-plus-latest": {
|
||||
maxTokens: 129_024,
|
||||
contextWindow: 131_072,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 1.5,
|
||||
outputPrice: 4.5,
|
||||
cacheWritesPrice: 1.5,
|
||||
cacheReadsPrice: 4.5,
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
@@ -615,10 +876,121 @@ export const mistralModels = {
|
||||
export type LiteLLMModelId = string
|
||||
export const liteLlmDefaultModelId = "gpt-3.5-turbo"
|
||||
export const liteLlmModelInfoSaneDefaults: ModelInfo = {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 8192,
|
||||
supportsImages: false,
|
||||
maxTokens: -1,
|
||||
contextWindow: 128_000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
}
|
||||
|
||||
// AskSage Models
|
||||
// https://docs.asksage.ai/
|
||||
export type AskSageModelId = keyof typeof askSageModels
|
||||
export const askSageDefaultModelId: AskSageModelId = "claude-35-sonnet"
|
||||
export const askSageDefaultURL: string = "https://api.asksage.ai/server"
|
||||
export const askSageModels = {
|
||||
"gpt-4o": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"gpt-4o-gov": {
|
||||
maxTokens: 4096,
|
||||
contextWindow: 128000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
"claude-35-sonnet": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200000,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
},
|
||||
}
|
||||
|
||||
// X AI
|
||||
// https://docs.x.ai/docs/api-reference
|
||||
export type XAIModelId = keyof typeof xaiModels
|
||||
export const xaiDefaultModelId: XAIModelId = "grok-2-latest"
|
||||
export const xaiModels = {
|
||||
"grok-2-latest": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
description: "X AI's Grok-2 model - latest version with 131K context window",
|
||||
},
|
||||
"grok-2": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
description: "X AI's Grok-2 model with 131K context window",
|
||||
},
|
||||
"grok-2-1212": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
description: "X AI's Grok-2 model (version 1212) with 131K context window",
|
||||
},
|
||||
"grok-2-vision-latest": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 32768,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
description: "X AI's Grok-2 Vision model - latest version with image support and 32K context window",
|
||||
},
|
||||
"grok-2-vision": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 32768,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
description: "X AI's Grok-2 Vision model with image support and 32K context window",
|
||||
},
|
||||
"grok-2-vision-1212": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 32768,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 2.0,
|
||||
outputPrice: 10.0,
|
||||
description: "X AI's Grok-2 Vision model (version 1212) with image support and 32K context window",
|
||||
},
|
||||
"grok-vision-beta": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 8192,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 5.0,
|
||||
outputPrice: 15.0,
|
||||
description: "X AI's Grok Vision Beta model with image support and 8K context window",
|
||||
},
|
||||
"grok-beta": {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 131072,
|
||||
supportsImages: false,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 5.0,
|
||||
outputPrice: 15.0,
|
||||
description: "X AI's Grok Beta model (legacy) with 131K context window",
|
||||
},
|
||||
} as const satisfies Record<string, ModelInfo>
|
||||
|
||||
@@ -7,42 +7,48 @@ Mention regex:
|
||||
|
||||
- **Regex Breakdown**:
|
||||
- `/@`:
|
||||
- **@**: The mention must start with the '@' symbol.
|
||||
- **@**: The mention must start with the '@' symbol.
|
||||
|
||||
- `((?:\/|\w+:\/\/)[^\s]+?|problems\b)`:
|
||||
- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
|
||||
- `(?:\/|\w+:\/\/)`:
|
||||
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
|
||||
- `\/`:
|
||||
- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
|
||||
- `|`: Logical OR.
|
||||
- `\w+:\/\/`:
|
||||
- **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc.
|
||||
- `[^\s]+?`:
|
||||
- **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace.
|
||||
- **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
|
||||
- `|`: Logical OR.
|
||||
- `problems\b`:
|
||||
- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
|
||||
- `(?:\/|\w+:\/\/)`:
|
||||
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
|
||||
- `\/`:
|
||||
- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
|
||||
- `|`: Logical OR.
|
||||
- `\w+:\/\/`:
|
||||
- **Protocol (`\w+://`)**: Matches URLs that start with a word character sequence followed by '://', such as 'http://', 'https://', 'ftp://', etc.
|
||||
- `[^\s]+?`:
|
||||
- **Non-Whitespace Characters (`[^\s]+`)**: Matches one or more characters that are not whitespace.
|
||||
- **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
|
||||
- `|`: Logical OR.
|
||||
- `problems\b`:
|
||||
- **Exact Word ('problems')**: Matches the exact word 'problems'.
|
||||
- **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic').
|
||||
- `terminal\b`:
|
||||
- **Exact Word ('terminal')**: Matches the exact word 'terminal'.
|
||||
- **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
|
||||
|
||||
- `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
|
||||
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
|
||||
- `[.,;:!?]?`:
|
||||
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
|
||||
- `(?=[\s\r\n]|$)`:
|
||||
- **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
|
||||
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
|
||||
- `[.,;:!?]?`:
|
||||
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
|
||||
- `(?=[\s\r\n]|$)`:
|
||||
- **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
|
||||
|
||||
- **Summary**:
|
||||
- The regex effectively matches:
|
||||
- Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path).
|
||||
- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
|
||||
- The exact word 'problems'.
|
||||
- Mentions that are file or folder paths starting with '/' and containing any non-whitespace characters (including periods within the path).
|
||||
- URLs that start with a protocol (like 'http://') followed by any non-whitespace characters (including query parameters).
|
||||
- The exact word 'problems'.
|
||||
- The exact word 'terminal'.
|
||||
- The exact word 'git-changes'.
|
||||
- It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text.
|
||||
|
||||
- **Global Regex**:
|
||||
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
|
||||
|
||||
*/
|
||||
export const mentionRegex = /@((?:\/|\w+:\/\/)[^\s]+?|problems\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
|
||||
export const mentionRegex =
|
||||
/@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|terminal\b|git-changes\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
|
||||
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export const DEFAULT_MCP_TIMEOUT_SECONDS = 60 // matches Anthropic's default timeout in their MCP SDK
|
||||
export const MIN_MCP_TIMEOUT_SECONDS = 1
|
||||
export type McpMode = "full" | "server-use-only" | "off"
|
||||
|
||||
export type McpServer = {
|
||||
@@ -9,6 +11,7 @@ export type McpServer = {
|
||||
resources?: McpResource[]
|
||||
resourceTemplates?: McpResourceTemplate[]
|
||||
disabled?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export type McpTool = {
|
||||
@@ -66,3 +69,39 @@ export type McpToolCallResponse = {
|
||||
>
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
export interface McpMarketplaceItem {
|
||||
mcpId: string
|
||||
githubUrl: string
|
||||
name: string
|
||||
author: string
|
||||
description: string
|
||||
codiconIcon: string
|
||||
logoUrl: string
|
||||
category: string
|
||||
tags: string[]
|
||||
requiresApiKey: boolean
|
||||
readmeContent?: string
|
||||
llmsInstallationContent?: string
|
||||
isRecommended: boolean
|
||||
githubStars: number
|
||||
downloadCount: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
lastGithubSync: string
|
||||
}
|
||||
|
||||
export interface McpMarketplaceCatalog {
|
||||
items: McpMarketplaceItem[]
|
||||
}
|
||||
|
||||
export interface McpDownloadResponse {
|
||||
mcpId: string
|
||||
githubUrl: string
|
||||
name: string
|
||||
author: string
|
||||
description: string
|
||||
readmeContent: string
|
||||
llmsInstallationContent: string
|
||||
requiresApiKey: boolean
|
||||
}
|
||||
|
||||
@@ -4,6 +4,25 @@ const vscode = require("vscode")
|
||||
describe("Extension Tests", function () {
|
||||
this.timeout(60000) // Increased timeout for extension operations
|
||||
|
||||
let originalGetConfiguration
|
||||
|
||||
beforeEach(() => {
|
||||
// Save original configuration
|
||||
originalGetConfiguration = vscode.workspace.getConfiguration
|
||||
// Setup mock configuration
|
||||
const mockUpdate = async () => Promise.resolve()
|
||||
const mockConfig = {
|
||||
get: () => true,
|
||||
update: mockUpdate,
|
||||
}
|
||||
vscode.workspace.getConfiguration = () => mockConfig
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original configuration
|
||||
vscode.workspace.getConfiguration = originalGetConfiguration
|
||||
})
|
||||
|
||||
it("should activate extension successfully", async () => {
|
||||
// Get the extension
|
||||
const extension = vscode.extensions.getExtension("saoudrizwan.claude-dev")
|
||||
@@ -34,4 +53,14 @@ describe("Extension Tests", function () {
|
||||
await vscode.commands.executeCommand("cline.historyButtonClicked")
|
||||
// Success if no error thrown
|
||||
})
|
||||
|
||||
it("should handle advanced settings configuration", async () => {
|
||||
// Test browser session setting
|
||||
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", true, true)
|
||||
const updatedConfig = vscode.workspace.getConfiguration("cline")
|
||||
expect(updatedConfig.get("disableBrowserTool")).to.be.true
|
||||
|
||||
// Reset settings
|
||||
await vscode.workspace.getConfiguration().update("cline.disableBrowserTool", undefined, true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -28,7 +28,13 @@ describe("Chat Integration Tests", () => {
|
||||
vscode.postMessage({ type: 'newTask', text: message.text });
|
||||
break;
|
||||
case 'toggleMode':
|
||||
vscode.postMessage({ type: 'chatSettings', chatSettings: { mode: 'act' } });
|
||||
vscode.postMessage({
|
||||
type: 'togglePlanActMode',
|
||||
chatSettings: { mode: 'act' },
|
||||
chatContent: {
|
||||
message: "message test",
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'invoke':
|
||||
if (message.invoke === 'primaryButtonClick') {
|
||||
@@ -78,7 +84,7 @@ describe("Chat Integration Tests", () => {
|
||||
// Set up state change listener
|
||||
const stateChangePromise = new Promise<any>((resolve) => {
|
||||
panel.webview.onDidReceiveMessage((message) => {
|
||||
if (message.type === "chatSettings") {
|
||||
if (message.type === "togglePlanActMode") {
|
||||
resolve(message)
|
||||
}
|
||||
})
|
||||
@@ -92,6 +98,25 @@ describe("Chat Integration Tests", () => {
|
||||
assert.equal(stateChange.chatSettings.mode, "act")
|
||||
})
|
||||
|
||||
it("should toggle between plan and act modes with messages", async () => {
|
||||
// Set up state change listener
|
||||
const stateChangePromise = new Promise<any>((resolve) => {
|
||||
panel.webview.onDidReceiveMessage((message) => {
|
||||
if (message.type === "togglePlanActMode") {
|
||||
resolve(message)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Trigger mode toggle
|
||||
await panel.webview.postMessage({ type: "toggleMode" })
|
||||
|
||||
// Verify mode changed
|
||||
const stateChange = await stateChangePromise
|
||||
assert.equal(stateChange.chatSettings.mode, "act")
|
||||
assert.equal(stateChange.chatContent.message, "message test")
|
||||
})
|
||||
|
||||
it("should handle tool approval flow", async () => {
|
||||
// Set up approval listener
|
||||
const approvalPromise = new Promise<any>((resolve) => {
|
||||
|
||||
+67
-6
@@ -1,10 +1,10 @@
|
||||
import { describe, it } from "mocha"
|
||||
import "should"
|
||||
import { calculateApiCost } from "./cost"
|
||||
import { calculateApiCostAnthropic, calculateApiCostOpenAI } from "./cost"
|
||||
import { ModelInfo } from "../shared/api"
|
||||
|
||||
describe("Cost Utilities", () => {
|
||||
describe("calculateApiCost", () => {
|
||||
describe("calculateApiCostAnthropic", () => {
|
||||
it("should calculate basic input/output costs", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
supportsPromptCache: false,
|
||||
@@ -12,7 +12,7 @@ describe("Cost Utilities", () => {
|
||||
outputPrice: 15.0, // $15 per million tokens
|
||||
}
|
||||
|
||||
const cost = calculateApiCost(modelInfo, 1000, 500)
|
||||
const cost = calculateApiCostAnthropic(modelInfo, 1000, 500)
|
||||
// Input: (3.0 / 1_000_000) * 1000 = 0.003
|
||||
// Output: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
// Total: 0.003 + 0.0075 = 0.0105
|
||||
@@ -25,7 +25,7 @@ describe("Cost Utilities", () => {
|
||||
// No prices specified
|
||||
}
|
||||
|
||||
const cost = calculateApiCost(modelInfo, 1000, 500)
|
||||
const cost = calculateApiCostAnthropic(modelInfo, 1000, 500)
|
||||
cost.should.equal(0)
|
||||
})
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("Cost Utilities", () => {
|
||||
cacheReadsPrice: 0.3,
|
||||
}
|
||||
|
||||
const cost = calculateApiCost(modelInfo, 2000, 1000, 1500, 500)
|
||||
const cost = calculateApiCostAnthropic(modelInfo, 2000, 1000, 1500, 500)
|
||||
// Cache writes: (3.75 / 1_000_000) * 1500 = 0.005625
|
||||
// Cache reads: (0.3 / 1_000_000) * 500 = 0.00015
|
||||
// Input: (3.0 / 1_000_000) * 2000 = 0.006
|
||||
@@ -60,7 +60,68 @@ describe("Cost Utilities", () => {
|
||||
cacheReadsPrice: 0.3,
|
||||
}
|
||||
|
||||
const cost = calculateApiCost(modelInfo, 0, 0, 0, 0)
|
||||
const cost = calculateApiCostAnthropic(modelInfo, 0, 0, 0, 0)
|
||||
cost.should.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("calculateApiCostOpenAI", () => {
|
||||
it("should calculate basic input/output costs", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 3.0, // $3 per million tokens
|
||||
outputPrice: 15.0, // $15 per million tokens
|
||||
}
|
||||
|
||||
const cost = calculateApiCostOpenAI(modelInfo, 1000, 500)
|
||||
// Input: (3.0 / 1_000_000) * 1000 = 0.003
|
||||
// Output: (15.0 / 1_000_000) * 500 = 0.0075
|
||||
// Total: 0.003 + 0.0075 = 0.0105
|
||||
cost.should.equal(0.0105)
|
||||
})
|
||||
|
||||
it("should handle missing prices", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
supportsPromptCache: true,
|
||||
// No prices specified
|
||||
}
|
||||
|
||||
const cost = calculateApiCostOpenAI(modelInfo, 1000, 500)
|
||||
cost.should.equal(0)
|
||||
})
|
||||
|
||||
it("should use real model configuration (Claude 3.5 Sonnet)", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 200_000,
|
||||
supportsImages: true,
|
||||
supportsComputerUse: true,
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
}
|
||||
|
||||
const cost = calculateApiCostOpenAI(modelInfo, 2100, 1000, 1500, 500)
|
||||
// Cache writes: (3.75 / 1_000_000) * 1500 = 0.005625
|
||||
// Cache reads: (0.3 / 1_000_000) * 500 = 0.00015
|
||||
// Input: (3.0 / 1_000_000) * (2100 - 1500 - 500) = 0.0003
|
||||
// Output: (15.0 / 1_000_000) * 1000 = 0.015
|
||||
// Total: 0.005625 + 0.00015 + 0.0003 + 0.015 = 0.021075
|
||||
cost.should.equal(0.021075)
|
||||
})
|
||||
|
||||
it("should handle zero token counts", () => {
|
||||
const modelInfo: ModelInfo = {
|
||||
supportsPromptCache: true,
|
||||
inputPrice: 3.0,
|
||||
outputPrice: 15.0,
|
||||
cacheWritesPrice: 3.75,
|
||||
cacheReadsPrice: 0.3,
|
||||
}
|
||||
|
||||
const cost = calculateApiCostOpenAI(modelInfo, 0, 0, 0, 0)
|
||||
cost.should.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
+40
-15
@@ -1,24 +1,49 @@
|
||||
import { ModelInfo } from "../shared/api"
|
||||
|
||||
export function calculateApiCost(
|
||||
function calculateApiCostInternal(
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
cacheCreationInputTokens: number,
|
||||
cacheReadInputTokens: number,
|
||||
): number {
|
||||
const cacheWritesCost = ((modelInfo.cacheWritesPrice || 0) / 1_000_000) * cacheCreationInputTokens
|
||||
const cacheReadsCost = ((modelInfo.cacheReadsPrice || 0) / 1_000_000) * cacheReadInputTokens
|
||||
const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
|
||||
const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
|
||||
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
|
||||
return totalCost
|
||||
}
|
||||
|
||||
// For Anthropic compliant usage, the input tokens count does NOT include the cached tokens
|
||||
export function calculateApiCostAnthropic(
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
cacheCreationInputTokens?: number,
|
||||
cacheReadInputTokens?: number,
|
||||
): number {
|
||||
const modelCacheWritesPrice = modelInfo.cacheWritesPrice
|
||||
let cacheWritesCost = 0
|
||||
if (cacheCreationInputTokens && modelCacheWritesPrice) {
|
||||
cacheWritesCost = (modelCacheWritesPrice / 1_000_000) * cacheCreationInputTokens
|
||||
}
|
||||
const modelCacheReadsPrice = modelInfo.cacheReadsPrice
|
||||
let cacheReadsCost = 0
|
||||
if (cacheReadInputTokens && modelCacheReadsPrice) {
|
||||
cacheReadsCost = (modelCacheReadsPrice / 1_000_000) * cacheReadInputTokens
|
||||
}
|
||||
const baseInputCost = ((modelInfo.inputPrice || 0) / 1_000_000) * inputTokens
|
||||
const outputCost = ((modelInfo.outputPrice || 0) / 1_000_000) * outputTokens
|
||||
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
|
||||
return totalCost
|
||||
const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
|
||||
const cacheReadInputTokensNum = cacheReadInputTokens || 0
|
||||
return calculateApiCostInternal(modelInfo, inputTokens, outputTokens, cacheCreationInputTokensNum, cacheReadInputTokensNum)
|
||||
}
|
||||
|
||||
// For OpenAI compliant usage, the input tokens count INCLUDES the cached tokens
|
||||
export function calculateApiCostOpenAI(
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
cacheCreationInputTokens?: number,
|
||||
cacheReadInputTokens?: number,
|
||||
): number {
|
||||
const cacheCreationInputTokensNum = cacheCreationInputTokens || 0
|
||||
const cacheReadInputTokensNum = cacheReadInputTokens || 0
|
||||
const nonCachedInputTokens = Math.max(0, inputTokens - cacheCreationInputTokensNum - cacheReadInputTokensNum)
|
||||
return calculateApiCostInternal(
|
||||
modelInfo,
|
||||
nonCachedInputTokens,
|
||||
outputTokens,
|
||||
cacheCreationInputTokensNum,
|
||||
cacheReadInputTokensNum,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { exec } from "child_process"
|
||||
import { promisify } from "util"
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
const GIT_OUTPUT_LINE_LIMIT = 500
|
||||
|
||||
export interface GitCommit {
|
||||
hash: string
|
||||
shortHash: string
|
||||
subject: string
|
||||
author: string
|
||||
date: string
|
||||
}
|
||||
|
||||
async function checkGitRepo(cwd: string): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git rev-parse --git-dir", { cwd })
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkGitInstalled(): Promise<boolean> {
|
||||
try {
|
||||
await execAsync("git --version")
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchCommits(query: string, cwd: string): Promise<GitCommit[]> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
console.error("Git is not installed")
|
||||
return []
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
console.error("Not a git repository")
|
||||
return []
|
||||
}
|
||||
|
||||
// Search commits by hash or message, limiting to 10 results
|
||||
const { stdout } = await execAsync(
|
||||
`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--grep="${query}" --regexp-ignore-case`,
|
||||
{ cwd },
|
||||
)
|
||||
|
||||
let output = stdout
|
||||
if (!output.trim() && /^[a-f0-9]+$/i.test(query)) {
|
||||
// If no results from grep search and query looks like a hash, try searching by hash
|
||||
const { stdout: hashStdout } = await execAsync(
|
||||
`git log -n 10 --format="%H%n%h%n%s%n%an%n%ad" --date=short ` + `--author-date-order ${query}`,
|
||||
{ cwd },
|
||||
).catch(() => ({ stdout: "" }))
|
||||
|
||||
if (!hashStdout.trim()) {
|
||||
return []
|
||||
}
|
||||
|
||||
output = hashStdout
|
||||
}
|
||||
|
||||
const commits: GitCommit[] = []
|
||||
const lines = output
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line !== "--")
|
||||
|
||||
for (let i = 0; i < lines.length; i += 5) {
|
||||
commits.push({
|
||||
hash: lines[i],
|
||||
shortHash: lines[i + 1],
|
||||
subject: lines[i + 2],
|
||||
author: lines[i + 3],
|
||||
date: lines[i + 4],
|
||||
})
|
||||
}
|
||||
|
||||
return commits
|
||||
} catch (error) {
|
||||
console.error("Error searching commits:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCommitInfo(hash: string, cwd: string): Promise<string> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
return "Git is not installed"
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
return "Not a git repository"
|
||||
}
|
||||
|
||||
// Get commit info, stats, and diff separately
|
||||
const { stdout: info } = await execAsync(`git show --format="%H%n%h%n%s%n%an%n%ad%n%b" --no-patch ${hash}`, {
|
||||
cwd,
|
||||
})
|
||||
const [fullHash, shortHash, subject, author, date, body] = info.trim().split("\n")
|
||||
|
||||
const { stdout: stats } = await execAsync(`git show --stat --format="" ${hash}`, { cwd })
|
||||
|
||||
const { stdout: diff } = await execAsync(`git show --format="" ${hash}`, { cwd })
|
||||
|
||||
const summary = [
|
||||
`Commit: ${shortHash} (${fullHash})`,
|
||||
`Author: ${author}`,
|
||||
`Date: ${date}`,
|
||||
`\nMessage: ${subject}`,
|
||||
body ? `\nDescription:\n${body}` : "",
|
||||
"\nFiles Changed:",
|
||||
stats.trim(),
|
||||
"\nFull Changes:",
|
||||
].join("\n")
|
||||
|
||||
const output = summary + "\n\n" + diff.trim()
|
||||
return truncateOutput(output)
|
||||
} catch (error) {
|
||||
console.error("Error getting commit info:", error)
|
||||
return `Failed to get commit info: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorkingState(cwd: string): Promise<string> {
|
||||
try {
|
||||
const isInstalled = await checkGitInstalled()
|
||||
if (!isInstalled) {
|
||||
return "Git is not installed"
|
||||
}
|
||||
|
||||
const isRepo = await checkGitRepo(cwd)
|
||||
if (!isRepo) {
|
||||
return "Not a git repository"
|
||||
}
|
||||
|
||||
// Get status of working directory
|
||||
const { stdout: status } = await execAsync("git status --short", { cwd })
|
||||
if (!status.trim()) {
|
||||
return "No changes in working directory"
|
||||
}
|
||||
|
||||
// Get all changes (both staged and unstaged) compared to HEAD
|
||||
const { stdout: diff } = await execAsync("git diff HEAD", { cwd })
|
||||
const output = `Working directory changes:\n\n${status}\n\n${diff}`.trim()
|
||||
return truncateOutput(output)
|
||||
} catch (error) {
|
||||
console.error("Error getting working state:", error)
|
||||
return `Failed to get working state: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
}
|
||||
|
||||
function truncateOutput(content: string): string {
|
||||
if (!GIT_OUTPUT_LINE_LIMIT) {
|
||||
return content
|
||||
}
|
||||
|
||||
const lines = content.split("\n")
|
||||
if (lines.length <= GIT_OUTPUT_LINE_LIMIT) {
|
||||
return content
|
||||
}
|
||||
|
||||
const beforeLimit = Math.floor(GIT_OUTPUT_LINE_LIMIT * 0.2) // 20% of lines before
|
||||
const afterLimit = GIT_OUTPUT_LINE_LIMIT - beforeLimit // remaining 80% after
|
||||
return [
|
||||
...lines.slice(0, beforeLimit),
|
||||
`\n[...${lines.length - GIT_OUTPUT_LINE_LIMIT} lines omitted...]\n`,
|
||||
...lines.slice(-afterLimit),
|
||||
].join("\n")
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function secondsToMs(seconds: number): number {
|
||||
return seconds * 1000
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { anthropicModels } from "../shared/api"
|
||||
|
||||
/**
|
||||
* Validates the thinking budget token value according to the specified rules:
|
||||
* - If disabled (0), return as is
|
||||
* - If enabled but less than minimum (1024), set to minimum
|
||||
* - If greater than or equal to max tokens, set to max tokens - 1
|
||||
* - Otherwise, return the original value
|
||||
*
|
||||
* @param value The thinking budget token value to validate
|
||||
* @param maxTokens The maximum tokens for the current model
|
||||
* @returns The validated thinking budget token value
|
||||
*/
|
||||
export function validateThinkingBudget(
|
||||
value: number,
|
||||
maxTokens: number = anthropicModels["claude-3-7-sonnet-20250219"].maxTokens,
|
||||
): number {
|
||||
// If disabled (0), return as is
|
||||
if (value === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// If enabled but less than minimum, set to minimum
|
||||
if (value > 0 && value < 1024) {
|
||||
return 1024
|
||||
}
|
||||
|
||||
// If greater than or equal to max allowed tokens (80% of max tokens), cap at that value
|
||||
const maxAllowedTokens = Math.floor(maxTokens * 0.8)
|
||||
if (value >= maxAllowedTokens) {
|
||||
return maxAllowedTokens
|
||||
}
|
||||
|
||||
// Otherwise, return the original value
|
||||
return value
|
||||
}
|
||||
+29
-20
@@ -1,23 +1,32 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
build
|
||||
*.local
|
||||
coverage
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import js from "@eslint/js"
|
||||
import globals from "globals"
|
||||
import reactHooks from "eslint-plugin-react-hooks"
|
||||
import reactRefresh from "eslint-plugin-react-refresh"
|
||||
import tseslint from "typescript-eslint"
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["build"] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
// "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
"no-case-declarations": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"prefer-const": "off",
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Cline Webview</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
// "Official" jest workaround for mocking window.matchMedia()
|
||||
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(), // Deprecated
|
||||
removeListener: vi.fn(), // Deprecated
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
})
|
||||
Generated
+2432
-15607
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user