mirror of
https://github.com/cline/cline.git
synced 2026-09-02 15:52:29 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24852c4cad |
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Added checkpointTrackerErrorMessage to HistoryItem - restored with task, prevents re-initialization if timed out before
|
||||
Never re-init checkpoint tracker if it timed out before
|
||||
Warning at 7s that it's taking awhile, timeout and give up at 15s
|
||||
Fixed click to open settings - now opens to correct tab
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: mcp servers are not started when disabled
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Global Rules directory documentation for Linux/WSL systems
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
DeepSeek R1 0528 support under Hugging Face
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
remove duplicate tool registration for claude4-experimental
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Introduce Claude Code support on Windows and fix E2BIG issues
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add Huawei Cloud MaaS Provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fixed token counting when using VSCode LM API provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: only focus on editor panel that is visible and active to stop input field stealing issue
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Add Cerebras Qwen 3 235B instruct
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
trim input value for URL fields
|
||||
@@ -716,7 +716,7 @@ The Controller class manages MCP servers through the McpHub service:
|
||||
class Controller {
|
||||
mcpHub?: McpHub
|
||||
|
||||
constructor(context: vscode.ExtensionContext, webviewProvider: WebviewProvider) {
|
||||
constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, webviewProvider: WebviewProvider) {
|
||||
this.mcpHub = new McpHub(this)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
"no-throw-literal": "warn",
|
||||
"semi": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"eslint-rules/no-protobuf-object-literals": "error",
|
||||
"eslint-rules/no-grpc-client-object-literals": "error",
|
||||
"eslint-rules/no-direct-vscode-api": "warn",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
|
||||
@@ -5,7 +5,7 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Important:** All bug reports must be reproducible using Claude 4 Sonnet. Cline uses complex prompts so less capable models may not work as expected.
|
||||
**Important:** All bug reports must be reproducible using Claude 3.5 Sonnet. Cline uses complex prompts so less capable models may not work as expected.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
@@ -24,7 +24,7 @@ body:
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
@@ -39,19 +39,20 @@ body:
|
||||
placeholder: "e.g., cline:anthropic/claude-3.7-sonnet, gemini:gemini-2.5-pro-exp-03-25"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: What operating system are you using?
|
||||
placeholder: "e.g., Windows 11, macOS Sonoma, Ubuntu 22.04"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
label: System Information
|
||||
description: What operating system and hardware are you using?
|
||||
placeholder: |
|
||||
Operating System: Windows 11, macOS Sonoma, Ubuntu 22.04, etc.
|
||||
Hardware: CPU, GPU, RAM specifications if relevant
|
||||
e.g.,
|
||||
OS: Windows 11
|
||||
CPU: Intel Core i7-11700K
|
||||
GPU: NVIDIA GeForce RTX 3070
|
||||
RAM: 32GB DDR4
|
||||
label: System Info
|
||||
description: What system information is relevant to the issue?
|
||||
placeholder: "e.g., CPU: Intel Core i7-11700K, GPU: NVIDIA GeForce RTX 3070, RAM: 32GB DDR4"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -62,3 +63,8 @@ body:
|
||||
placeholder: "e.g., 1.2.3"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here, such as screenshots or related issues.
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
name: 💡 Feature Proposal & Contribution
|
||||
description: Propose a new feature or improvement, and optionally offer to implement feature as a contributor
|
||||
labels: ["proposal"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Feature Proposal & Contribution for Cline**
|
||||
|
||||
Thank you for proposing a feature or improvement for Cline! This template helps us understand the problem, evaluate the solution, and coordinate implementation.
|
||||
|
||||
**For detailed proposals:** Please provide comprehensive information to enable fast prioritization and discussion.
|
||||
**For contribution offers:** You can indicate your willingness to implement the feature yourself.
|
||||
|
||||
Before submitting:
|
||||
- Search existing [Issues](https://github.com/cline/cline/issues) and [Discussions](https://github.com/cline/cline/discussions) to avoid duplicates
|
||||
- Read the [Contributing Guide](https://github.com/cline/cline/blob/main/CONTRIBUTING.md) if you plan to contribute
|
||||
- Don't start implementation until the proposal is reviewed and approved
|
||||
|
||||
- type: textarea
|
||||
id: problem-description
|
||||
attributes:
|
||||
label: What problem does this solve?
|
||||
description: |
|
||||
Describe the problem clearly from a user's point of view. Focus on why this matters, who it affects, and when it occurs.
|
||||
|
||||
✅ Good examples:
|
||||
- "LLM provider returns 400 error when nearing the context window instead of truncating"
|
||||
- "Submit button is invisible in dark mode"
|
||||
- "Users can't easily share their Cline configurations with team members"
|
||||
|
||||
❌ Avoid vague descriptions:
|
||||
- "Performance is bad"
|
||||
- "UI needs work"
|
||||
|
||||
Your description should include:
|
||||
- Who is affected?
|
||||
- When does it happen?
|
||||
- What's the current vs expected behavior?
|
||||
- What is the impact?
|
||||
placeholder: Be specific about the problem, who it affects, and the impact.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposed-solution
|
||||
attributes:
|
||||
label: What's the proposed solution?
|
||||
description: |
|
||||
Describe how the problem should be solved. Be specific about UX, system behavior, and any flows that would change.
|
||||
|
||||
✅ Good examples:
|
||||
- "Add error handling immediately after attempting to create the llm stream and retry after manually truncating"
|
||||
- "Update button styling to ensure contrast in all themes"
|
||||
- "Add export/import functionality in settings with JSON format"
|
||||
|
||||
❌ Avoid vague solutions:
|
||||
- "Improve performance"
|
||||
- "Fix the bug"
|
||||
|
||||
Your solution should include:
|
||||
- What exactly will change?
|
||||
- How will users interact with it?
|
||||
- What's the expected outcome?
|
||||
placeholder: Describe the proposed changes and how they solve the problem.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: contribution-intent
|
||||
attributes:
|
||||
label: Are you interested in implementing this?
|
||||
description: Let us know if you'd like to contribute to this feature
|
||||
options:
|
||||
- "No, just proposing the idea"
|
||||
- "Yes, I'd like to implement this myself"
|
||||
- "Yes, I'd like to collaborate with others"
|
||||
- "Maybe, depending on complexity and guidance"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
id: implementation-approach
|
||||
attributes:
|
||||
label: Implementation approach (if contributing)
|
||||
description: |
|
||||
**Only fill this out if you selected "Yes" above.**
|
||||
|
||||
How do you plan to implement this? Include:
|
||||
- High-level technical approach
|
||||
- Files/components that would be affected
|
||||
- Any new dependencies required
|
||||
- Potential challenges or considerations you've identified
|
||||
|
||||
This helps us provide better guidance and ensures alignment before you start coding.
|
||||
placeholder: "My implementation approach would be..."
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Proposal checklist
|
||||
options:
|
||||
- label: I've checked for existing issues or related proposals
|
||||
required: true
|
||||
- label: I understand this needs review before implementation can start
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: contribution-checklist
|
||||
attributes:
|
||||
label: Contribution checklist (if contributing)
|
||||
description: Only check these if you plan to contribute
|
||||
options:
|
||||
- label: I've read the [Contributing Guide](https://github.com/cline/cline/blob/main/CONTRIBUTING.md)
|
||||
- label: I'm willing to make changes based on feedback
|
||||
- label: I understand the code review process and requirements
|
||||
@@ -2,14 +2,15 @@
|
||||
Thank you for contributing to Cline!
|
||||
|
||||
⚠️ Important: Before submitting this PR, please ensure you have:
|
||||
- For feature requests: Created a discussion in our Feature Requests discussions board https://github.com/cline/cline/discussions/categories/feature-requests and received approval from core maintainers before implementation
|
||||
- For all changes: Link the associated issue/discussion in the "Related Issue" section below
|
||||
- Opened an issue and discussed your proposed changes with the community / contributors
|
||||
- Received approval from a core Cline contributor prior to proceeding with the implementation
|
||||
- Link the associated issue in the "Related Issue" section
|
||||
|
||||
Limited exceptions:
|
||||
Small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality may be submitted directly without prior discussion.
|
||||
Small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality may be submitted directly.
|
||||
|
||||
Why this requirement?
|
||||
We deeply appreciate all community contributions - they are essential to Cline's success! To ensure the best use of everyone's time and maintain project direction, we use our Feature Requests discussions board to gauge community interest and validate feature ideas before implementation begins. This helps us focus development efforts on features that will benefit the most users.
|
||||
We deeply appreciate all community contributions - they are the core reason we're able to operate successfully and keep innovating! We welcome community input and want to make it as easy as possible for people to submit quality work. This process helps our core maintainers review new ideas faster and saves contributor time by ensuring you have the go-ahead before spending time on implementation.
|
||||
-->
|
||||
|
||||
### Related Issue
|
||||
|
||||
@@ -56,29 +56,6 @@ jobs:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
# Cache VS Code installation
|
||||
- name: Cache VS Code
|
||||
uses: actions/cache@v4
|
||||
id: vscode-cache
|
||||
with:
|
||||
path: .vscode-test
|
||||
key: vscode-${{ runner.os }}-stable-${{ hashFiles('.vscode-test.mjs', 'package.json') }}
|
||||
restore-keys: |
|
||||
vscode-${{ runner.os }}-stable-
|
||||
|
||||
# Cache Playwright browsers
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
~/Library/Caches/ms-playwright
|
||||
~/AppData/Local/ms-playwright
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci
|
||||
@@ -91,14 +68,17 @@ jobs:
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
|
||||
# Run optimized E2E tests (eliminates redundant builds)
|
||||
# Build the extension before running tests
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
- name: Run E2E tests - Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: xvfb-run -a npm run test:e2e:optimal
|
||||
run: xvfb-run -a npm run test:e2e
|
||||
|
||||
- name: Run E2E tests - Non-Linux
|
||||
if: matrix.runner != 'ubuntu'
|
||||
run: npm run test:e2e:optimal
|
||||
run: npm run test:e2e
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() }}
|
||||
|
||||
+5
-4
@@ -15,9 +15,6 @@ pnpm-lock.yaml
|
||||
.venv
|
||||
.actrc
|
||||
|
||||
webview-ui/src/**/*.js
|
||||
webview-ui/src/**/*.js.map
|
||||
|
||||
# Ignore coverage directories and files
|
||||
coverage
|
||||
# But don't ignore the coverage scripts in .github/scripts/
|
||||
@@ -27,7 +24,11 @@ coverage
|
||||
|
||||
## Generated files ##
|
||||
src/generated/
|
||||
src/shared/proto/
|
||||
src/core/controller/*/methods.ts
|
||||
src/core/controller/*/index.ts
|
||||
src/core/controller/grpc-service-config.ts
|
||||
src/shared/proto/*.ts
|
||||
src/shared/proto/host/*.ts
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
|
||||
# E2E Tests
|
||||
|
||||
Vendored
+6
-30
@@ -6,7 +6,7 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension (production)",
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
|
||||
@@ -18,32 +18,6 @@
|
||||
"CLINE_ENVIRONMENT": "production"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (staging)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "staging"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (local)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-workspace-trust", "${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"CLINE_ENVIRONMENT": "local"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (Fresh Install Mode)",
|
||||
"type": "extensionHost",
|
||||
@@ -71,7 +45,7 @@
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Run cline-core service",
|
||||
"name": "Run Standalone Service",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"],
|
||||
@@ -82,9 +56,11 @@
|
||||
// Turns on grpc debug log.
|
||||
//"GRPC_TRACE": "all",
|
||||
//"GRPC_VERBOSITY": "DEBUG",
|
||||
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules"
|
||||
"NODE_PATH": "${workspaceFolder}/dist-standalone/node_modules",
|
||||
|
||||
"HOST_BRIDGE_ADDRESS": "localhost:50052"
|
||||
},
|
||||
"program": "cline-core.js"
|
||||
"program": "standalone.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+1
-5
@@ -9,9 +9,5 @@
|
||||
"dist": true // set this to false to include "dist" folder in search results
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
// Protobuf settings
|
||||
"protoc": {
|
||||
"options": ["--proto_path=proto"]
|
||||
}
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
}
|
||||
|
||||
+552
-579
File diff suppressed because it is too large
Load Diff
+8
-5
@@ -14,11 +14,14 @@ Bug reports help make Cline better for everyone! Before creating a new issue, pl
|
||||
## Before Contributing
|
||||
|
||||
All contributions must begin with a GitHub Issue, unless the change is for small bug fixes, typo corrections, minor wording improvements, or simple type fixes that don't change functionality.
|
||||
**For features and contributions**:
|
||||
- First check the [Feature Requests discussions board](https://github.com/cline/cline/discussions/categories/feature-requests) for similar ideas
|
||||
- If your idea is new, create a new feature request
|
||||
- Wait for approval from core maintainers before starting implementation
|
||||
- Once approved, feel free to begin working on a PR with the help of our community!
|
||||
|
||||
- **Check existing issues**: Search [GitHub Issues](https://github.com/cline/cline/issues).
|
||||
- **Create an issue**: Use appropriate templates:
|
||||
- **Contributions:** Use the "Contribution Request" template to propose what you'd like to work on.
|
||||
- **Bugs:** "Bug Report" template for reporting issues.
|
||||
- **Features:** "Detailed Feature Proposal" template for suggesting new features.
|
||||
- **Wait for approval**: A core Cline contributor must approve your contribution request before you start implementation.
|
||||
- **Claim issues**: Once approved, the issue will be assigned to you.
|
||||
|
||||
**PRs without approved issues may be closed.**
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ lint:
|
||||
|
||||
except: # Add exceptions for current patterns that contradict STANDARD settings
|
||||
- RPC_PASCAL_CASE # rpcs are camel case (start with lowercase)
|
||||
- PACKAGE_DIRECTORY_MATCH # the protos in the cline package are not in a dir named cline.
|
||||
- RPC_REQUEST_RESPONSE_UNIQUE # request messages are not unique.
|
||||
- RPC_REQUEST_STANDARD_NAME # request messages dont all end with Request
|
||||
- RPC_RESPONSE_STANDARD_NAME # response messages dont all end with Response
|
||||
|
||||
@@ -11,19 +11,7 @@ You can create a rule by clicking the `+` button in the Rules tab. This will ope
|
||||
Once you save the file:
|
||||
|
||||
- Your rule will be stored in the `.clinerules/` directory in your project (if it's a Workspace Rule)
|
||||
- Or in the Global Rules directory (if it's a Global Rule):
|
||||
|
||||
### Global Rules Directory Location
|
||||
|
||||
The location of your Global Rules directory depends on your operating system:
|
||||
|
||||
| Operating System | Default Location | Notes |
|
||||
|------------------|------------------|-------|
|
||||
| **Windows** | `Documents\Cline\Rules` | Uses system Documents folder |
|
||||
| **macOS** | `~/Documents/Cline/Rules` | Uses user Documents folder |
|
||||
| **Linux/WSL** | `~/Documents/Cline/Rules` | May fall back to `~/Cline/Rules` on some systems |
|
||||
|
||||
> **Note for Linux/WSL users**: If you don't find your global rules in `~/Documents/Cline/Rules`, check `~/Cline/Rules` as the location may vary depending on your system configuration and whether the Documents directory exists.
|
||||
- Or in the `Documents/Cline/Rules` directory (if it's a Global Rule).
|
||||
|
||||
You can also have Cline create a rule for you by using the [`/newrule` slash command](/features/slash-commands/new-rule) in the chat.
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ There are multiple places online to find MCP servers:
|
||||
- [mcpservers.org](https://mcpservers.org/)
|
||||
- [mcp.so](https://mcp.so/)
|
||||
- [glama.ai/mcp/servers](https://glama.ai/mcp/servers)
|
||||
- [mcp.composio.dev](https://mcp.composio.dev/)
|
||||
|
||||
These directories allow users to sort the servers by various criteria such as downloads, date, stars, and use case. Each entry provides information such as features, tools, and configuration instructions.
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ title: "Telemetry"
|
||||
|
||||
### Overview
|
||||
|
||||
To help make Cline better for everyone, we collect usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
|
||||
To help make Cline better for everyone, we collect anonymous usage data that helps us understand how developers are using our open-source AI coding agent. This feedback loop is crucial for improving Cline's capabilities and user experience.
|
||||
|
||||
We use PostHog, an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/cline/cline/blob/main/src/services/posthog/telemetry/TelemetryService.ts) to see exactly what we track.
|
||||
|
||||
### Tracking Policy
|
||||
|
||||
Privacy is our priority. By default, all collected data is anonymized. If you log in with a Cline account, your telemetry data will be associated with your account to help us improve the product and provide better support when you encounter issues. Your code, prompts, and conversation content always remain private and are never collected.
|
||||
Privacy is our priority. All collected data is anonymized before being sent to PostHog, with no personally identifiable information (PII) included. Your code, prompts, and conversation content always remain private and are never collected.
|
||||
|
||||
### What We Track
|
||||
|
||||
We collect basic usage data including:
|
||||
We collect basic anonymous usage data including:
|
||||
|
||||
**Task Interactions:** When tasks start and finish, conversation flow (without content)\
|
||||
**Mode and Tool Usage:** Switches between plan/act modes, which tools are being used\
|
||||
@@ -28,7 +28,7 @@ For complete transparency, you can inspect our [telemetry implementation](https:
|
||||
|
||||
Telemetry in Cline is entirely optional:
|
||||
|
||||
- When you update or install our VS Code extension, you'll see a message about our telemetry
|
||||
- When you update or install our VS Code extension, you'll see a message about our anonymous telemetry
|
||||
- You can change your preference anytime in settings
|
||||
|
||||
Cline also respects VS Code's global telemetry settings. If you've disabled telemetry at the VS Code level, Cline's telemetry will automatically be disabled as well.
|
||||
|
||||
@@ -244,60 +244,13 @@ Recent macOS versions have stricter terminal permissions:
|
||||
|
||||
### Windows Issues
|
||||
|
||||
If you're using Windows and still experiencing issues with shell integration after trying the previous steps, it's recommended you use Git Bash (or PowerShell).
|
||||
#### PowerShell Execution Policy
|
||||
|
||||
### Git Bash
|
||||
|
||||
Git Bash is a terminal emulator that provides a Unix-like command line experience on Windows. To use Git Bash, you need to:
|
||||
|
||||
1. Download and run the Git for Windows installer from [https://git-scm.com/downloads/win](https://git-scm.com/downloads/win)
|
||||
2. Quit and re-open VSCode
|
||||
3. Press `Ctrl + Shift + P` to open the Command Palette
|
||||
4. Type "Terminal: Select Default Profile" and choose it
|
||||
5. Select "Git Bash"
|
||||
|
||||
### PowerShell
|
||||
|
||||
If you'd still like to use PowerShell, make sure you're using an updated version (at least v7+).
|
||||
- Check your current PowerShell version by running: `$PSVersionTable.PSVersion`
|
||||
- If your version is below 7, [update PowerShell](https://learn.microsoft.com/en-us/powershell/scripting/whats-new/migrating-from-windows-powershell-51-to-powershell-7?view=powershell-7.4#installing-powershell-7).
|
||||
|
||||
You may also need to adjust your PowerShell execution policy. By default, PowerShell restricts script execution for security reasons.
|
||||
|
||||
#### Understanding PowerShell Execution Policies
|
||||
|
||||
PowerShell uses execution policies to determine which scripts can run on your system. Here are the most common policies:
|
||||
|
||||
- `Restricted`: No PowerShell scripts can run. This is the default setting.
|
||||
- `AllSigned`: All scripts, including local ones, must be signed by a trusted publisher.
|
||||
- `RemoteSigned`: Scripts created locally can run, but scripts downloaded from the internet must be signed.
|
||||
- `Unrestricted`: No restrictions. Any script can run, though you will be warned before running internet-downloaded scripts.
|
||||
|
||||
For development work in VSCode, the `RemoteSigned` policy is generally recommended. It allows locally created scripts to run without restrictions while maintaining security for downloaded scripts. To learn more about PowerShell execution policies and understand the security implications of changing them, visit Microsoft's documentation: [About Execution Policies](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies).
|
||||
|
||||
#### Steps to Change the Execution Policy
|
||||
|
||||
1. Open PowerShell as an Administrator: Press `Win + X` and select "Windows PowerShell (Administrator)" or "Windows Terminal (Administrator)".
|
||||
|
||||
2. Check Current Execution Policy by running this command:
|
||||
```powershell
|
||||
Get-ExecutionPolicy
|
||||
```
|
||||
- If the output is already `RemoteSigned`, `Unrestricted`, or `Bypass`, you likely don't need to change your execution policy. These policies should allow shell integration to work.
|
||||
- If the output is `Restricted` or `AllSigned`, you may need to change your policy to enable shell integration.
|
||||
|
||||
3. Change the Execution Policy by running the following command:
|
||||
```powershell
|
||||
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
```
|
||||
- This sets the policy to `RemoteSigned` for the current user only, which is safer than changing it system-wide.
|
||||
|
||||
4. Confirm the Change by typing `Y` and pressing Enter when prompted.
|
||||
|
||||
5. Verify the Policy Change by running `Get-ExecutionPolicy` again to confirm the new setting.
|
||||
|
||||
6. Restart VSCode and try the shell integration again.
|
||||
If commands fail silently:
|
||||
|
||||
```powershell
|
||||
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
```
|
||||
|
||||
#### WSL Integration
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import fs from "node:fs"
|
||||
import * as esbuild from "esbuild"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const esbuild = require("esbuild")
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
|
||||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
@@ -131,8 +127,10 @@ const baseConfig = {
|
||||
sourcemap: !production,
|
||||
logLevel: "silent",
|
||||
define: production
|
||||
? { "import.meta.url": "_importMetaUrl", "process.env.IS_DEV": JSON.stringify(!production) }
|
||||
: { "import.meta.url": "_importMetaUrl" },
|
||||
? {
|
||||
"process.env.IS_DEV": JSON.stringify(!production),
|
||||
}
|
||||
: undefined,
|
||||
tsconfig: path.resolve(__dirname, "tsconfig.json"),
|
||||
plugins: [
|
||||
copyWasmFiles,
|
||||
@@ -143,9 +141,6 @@ const baseConfig = {
|
||||
format: "cjs",
|
||||
sourcesContent: false,
|
||||
platform: "node",
|
||||
banner: {
|
||||
js: "const _importMetaUrl=require('url').pathToFileURL(__filename)",
|
||||
},
|
||||
}
|
||||
|
||||
// Extension-specific configuration
|
||||
@@ -0,0 +1,174 @@
|
||||
const { RuleTester: GrpcRuleTester } = require("eslint")
|
||||
const grpcRule = require("../no-grpc-client-object-literals")
|
||||
|
||||
const grpcRuleTester = new GrpcRuleTester({
|
||||
parser: require.resolve("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
grpcRuleTester.run("no-grpc-client-object-literals", grpcRule, {
|
||||
valid: [
|
||||
// Valid case: Using .create() method with gRPC client
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: PlanActMode.PLAN,
|
||||
preferredLanguage: 'en',
|
||||
},
|
||||
})
|
||||
);
|
||||
`,
|
||||
},
|
||||
// Valid case: Using .fromPartial() method with gRPC client
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
const chatSettings = ChatSettings.fromPartial({
|
||||
mode: PlanActMode.PLAN,
|
||||
preferredLanguage: 'en',
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode(
|
||||
TogglePlanActModeRequest.create({
|
||||
chatSettings: chatSettings,
|
||||
})
|
||||
);
|
||||
`,
|
||||
},
|
||||
// Valid case: Regular function call with object literal (not a gRPC client)
|
||||
{
|
||||
code: `
|
||||
function processData(data) {
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
processData({
|
||||
id: 123,
|
||||
name: 'test',
|
||||
});
|
||||
`,
|
||||
},
|
||||
// Valid case: Using proper nested protobuf objects
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
// Using proper nested protobuf objects
|
||||
const chatSettings = ChatSettings.create({
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
});
|
||||
|
||||
const request = TogglePlanActModeRequest.create({
|
||||
chatSettings: chatSettings,
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode(request);
|
||||
`,
|
||||
},
|
||||
// Valid case: Object literal in second parameter (should not be checked)
|
||||
{
|
||||
code: `
|
||||
import { StateSubscribeRequest } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
const request = StateSubscribeRequest.create({
|
||||
topics: ['apiConfig', 'tasks']
|
||||
});
|
||||
|
||||
// Second parameter is an object literal but should not trigger the rule
|
||||
StateServiceClient.subscribe(request, {
|
||||
metadata: {
|
||||
userId: 123,
|
||||
sessionId: "abc-123"
|
||||
}
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Invalid case: Using object literal directly with gRPC client
|
||||
{
|
||||
code: `
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
StateServiceClient.togglePlanActMode({
|
||||
chatSettings: {
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
},
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Using object literal with nested properties
|
||||
{
|
||||
code: `
|
||||
import { ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
const chatSettings = ChatSettings.create({
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode({
|
||||
chatSettings: {
|
||||
mode: 1,
|
||||
preferredLanguage: 'fr',
|
||||
},
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Nested object literal in protobuf create method
|
||||
{
|
||||
code: `
|
||||
import { TogglePlanActModeRequest, ChatSettings } from '@shared/proto/state';
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
// Using nested object literal instead of ChatSettings.create()
|
||||
const request = TogglePlanActModeRequest.create({
|
||||
chatSettings: {
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
},
|
||||
});
|
||||
|
||||
StateServiceClient.togglePlanActMode(request);
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Object literal as first parameter to subscribe method
|
||||
{
|
||||
code: `
|
||||
import { StateServiceClient } from '../services/grpc-client';
|
||||
|
||||
// First parameter is an object literal, which should trigger the rule
|
||||
StateServiceClient.subscribe({
|
||||
topics: ['apiConfig', 'tasks']
|
||||
}, {
|
||||
metadata: {
|
||||
userId: 123,
|
||||
sessionId: "abc-123"
|
||||
}
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -0,0 +1,214 @@
|
||||
const { RuleTester } = require("eslint")
|
||||
const rule = require("../no-protobuf-object-literals")
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ruleTester.run("no-protobuf-object-literals", rule, {
|
||||
valid: [
|
||||
// Valid case: Using .create() method
|
||||
{
|
||||
code: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
const state = State.create({
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
});
|
||||
`,
|
||||
},
|
||||
// Valid case: Using .fromPartial() method
|
||||
{
|
||||
code: `
|
||||
import { ChatSettings } from '@shared/proto/state';
|
||||
|
||||
const settings = ChatSettings.fromPartial({
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
openAiReasoningEffort: 'thorough'
|
||||
});
|
||||
`,
|
||||
},
|
||||
// Valid case: Object literal not used with protobuf type
|
||||
{
|
||||
code: `
|
||||
interface MyInterface {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const obj: MyInterface = {
|
||||
id: 123,
|
||||
name: 'test'
|
||||
};
|
||||
`,
|
||||
},
|
||||
// Valid case: Using object literal for non-protobuf import
|
||||
{
|
||||
code: `
|
||||
import { SomeType } from '@some/other/package';
|
||||
|
||||
const obj: SomeType = {
|
||||
id: 123,
|
||||
name: 'test'
|
||||
};
|
||||
`,
|
||||
},
|
||||
// Valid case: Regular function call with object literal (should not be flagged)
|
||||
{
|
||||
code: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
// This should not be flagged because it's a regular function call
|
||||
// not directly tied to a protobuf type
|
||||
process({
|
||||
id: 123,
|
||||
name: 'test',
|
||||
data: { nested: true }
|
||||
});
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Invalid case: Using object literal with imported protobuf type
|
||||
{
|
||||
code: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
const state: State = {
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
};
|
||||
`,
|
||||
output: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
const state: State = State.create({
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Using object literal with namespaced protobuf type
|
||||
{
|
||||
code: `
|
||||
import * as stateProto from '@shared/proto/state';
|
||||
|
||||
const state: stateProto.State = {
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
};
|
||||
`,
|
||||
output: `
|
||||
import * as stateProto from '@shared/proto/state';
|
||||
|
||||
const state: stateProto.State = stateProto.State.create({
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethodGeneric" }],
|
||||
},
|
||||
// Invalid case: Using object literal in a return statement (with protobuf return type)
|
||||
{
|
||||
code: `
|
||||
import { ChatSettings } from '@shared/proto/state';
|
||||
|
||||
function createSettings(): ChatSettings {
|
||||
return {
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
openAiReasoningEffort: 'thorough'
|
||||
};
|
||||
}
|
||||
`,
|
||||
output: `
|
||||
import { ChatSettings } from '@shared/proto/state';
|
||||
|
||||
function createSettings(): ChatSettings {
|
||||
return ChatSettings.create({
|
||||
mode: 0,
|
||||
preferredLanguage: 'en',
|
||||
openAiReasoningEffort: 'thorough'
|
||||
});
|
||||
}
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Invalid case: Using object literal in a function parameter (with protobuf types imported)
|
||||
{
|
||||
code: `
|
||||
import { ChatContent } from '@shared/proto/state';
|
||||
|
||||
function processContent(content: ChatContent) {
|
||||
// process the content
|
||||
}
|
||||
|
||||
processContent({
|
||||
message: 'Hello, this is a test message',
|
||||
images: ['image1.png', 'image2.jpg'],
|
||||
files: ['file1.txt', 'file2.pdf']
|
||||
});
|
||||
`,
|
||||
output: `
|
||||
import { ChatContent } from '@shared/proto/state';
|
||||
|
||||
function processContent(content: ChatContent) {
|
||||
// process the content
|
||||
}
|
||||
|
||||
processContent(ChatContent.create({
|
||||
message: 'Hello, this is a test message',
|
||||
images: ['image1.png', 'image2.jpg'],
|
||||
files: ['file1.txt', 'file2.pdf']
|
||||
}));
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethodGeneric" }],
|
||||
},
|
||||
// Invalid case: Using object literal in assignment expression
|
||||
{
|
||||
code: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
let state: State;
|
||||
state = {
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
};
|
||||
`,
|
||||
output: `
|
||||
import { State } from '@shared/proto/state';
|
||||
|
||||
let state: State;
|
||||
state = State.create({
|
||||
stateJson: '{"apiConfig":{"provider":"anthropic","model":"claude-3-haiku"}}'
|
||||
});
|
||||
`,
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
// Test with custom protobufPackages option
|
||||
{
|
||||
code: `
|
||||
import { CustomProto } from 'custom/proto/package';
|
||||
|
||||
const obj: CustomProto = {
|
||||
field1: 'value',
|
||||
field2: 123
|
||||
};
|
||||
`,
|
||||
output: `
|
||||
import { CustomProto } from 'custom/proto/package';
|
||||
|
||||
const obj: CustomProto = CustomProto.create({
|
||||
field1: 'value',
|
||||
field2: 123
|
||||
});
|
||||
`,
|
||||
options: [{ protobufPackages: ["custom/proto"] }],
|
||||
errors: [{ messageId: "useProtobufMethod" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -1,14 +1,20 @@
|
||||
// eslint-rules/index.js
|
||||
const noProtobufObjectLiterals = require("./no-protobuf-object-literals")
|
||||
const noGrpcClientObjectLiterals = require("./no-grpc-client-object-literals")
|
||||
const noDirectVscodeApi = require("./no-direct-vscode-api")
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
"no-protobuf-object-literals": noProtobufObjectLiterals,
|
||||
"no-grpc-client-object-literals": noGrpcClientObjectLiterals,
|
||||
"no-direct-vscode-api": noDirectVscodeApi,
|
||||
},
|
||||
configs: {
|
||||
recommended: {
|
||||
plugins: ["local"],
|
||||
rules: {
|
||||
"local/no-protobuf-object-literals": "error",
|
||||
"local/no-grpc-client-object-literals": "error",
|
||||
"local/no-direct-vscode-api": "warn",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,21 +29,20 @@ const disallowedApis = {
|
||||
"vscode.workspace.applyEdit": {
|
||||
messageId: "useHostBridge",
|
||||
},
|
||||
"vscode.window.onDidChangeActiveTextEditor": {
|
||||
messageId: "useHostBridge",
|
||||
},
|
||||
"vscode.env.openExternal": {
|
||||
messageId: "useUtils",
|
||||
},
|
||||
// "vscode.env.openExternal": {
|
||||
// messageId: "useUtils",
|
||||
// },
|
||||
// "vscode.window.showWarningMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
"vscode.window.showOpenDialog": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
"vscode.window.showErrorMessage": {
|
||||
messageId: "useHostBridgeShowMessage",
|
||||
},
|
||||
// There are too many warnings for these calls, uncomment the following
|
||||
// when the migration is finished.
|
||||
// "vscode.window.showErrorMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
// "vscode.window.showInformationMessage": {
|
||||
// messageId: "useHostBridgeShowMessage",
|
||||
// },
|
||||
@@ -72,11 +71,11 @@ module.exports = createRule({
|
||||
"This provides consistent path handling across different environments.\n" +
|
||||
"Found: {{code}}",
|
||||
useHostBridgeWorkspace:
|
||||
"Use HostProvider.workspace.getWorkspacePaths({}) instead of vscode.workspace.workspaceFolders.\n" +
|
||||
"Use getHostBridgeProvider().workspaceClient.getWorkspacePaths({}) instead of vscode.workspace.workspaceFolders.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
useHostBridgeShowMessage:
|
||||
"Use HostProvider.window.showMessage instead of the vscode.window.showMessage.\n" +
|
||||
"Use getHostBridgeProvider().windowClient.showMessage instead of the vscode.window.showMessage.\n" +
|
||||
"This provides a consistent abstraction across VSCode and standalone environments.\n" +
|
||||
"Found: {{code}}",
|
||||
useHostBridge:
|
||||
@@ -187,10 +186,6 @@ module.exports = createRule({
|
||||
if (filename.includes("/standalone/runtime-files/")) {
|
||||
return true
|
||||
}
|
||||
// Skip unit tests
|
||||
if (filename.endsWith(".test.ts")) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
const { ESLintUtils } = require("@typescript-eslint/utils")
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
|
||||
|
||||
module.exports = createRule({
|
||||
name: "no-grpc-client-object-literals",
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Enforce using .create() or .fromPartial() for gRPC service client parameters instead of object literals",
|
||||
recommended: "error",
|
||||
},
|
||||
messages: {
|
||||
useProtobufMethod:
|
||||
"Use the appropriate protobuf .create() or .fromPartial() method instead of " +
|
||||
"object literal for gRPC client parameters.\n" +
|
||||
"Found: {{code}}\n" +
|
||||
"gRPC client methods should always receive properly created protobuf objects.",
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
defaultOptions: [],
|
||||
|
||||
create(context) {
|
||||
// Check if a name matches the gRPC service client pattern using regex
|
||||
// Must start with an uppercase letter and end with ServiceClient
|
||||
const isGrpcServiceClient = (name) => {
|
||||
return typeof name === "string" && /^[A-Z].*ServiceClient$/.test(name)
|
||||
}
|
||||
|
||||
const safeObjectExpressions = new Map() // Track object expressions in create/fromPartial calls
|
||||
|
||||
return {
|
||||
// Skip object literals inside create() or fromPartial() method calls
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee &&
|
||||
node.callee.type === "MemberExpression" &&
|
||||
(node.callee.property.name === "create" || node.callee.property.name === "fromPartial") &&
|
||||
node.arguments.length > 0 &&
|
||||
node.arguments[0].type === "ObjectExpression"
|
||||
) {
|
||||
// Track this object expression as being used with create/fromPartial
|
||||
safeObjectExpressions.set(node.arguments[0], { isProblematic: false })
|
||||
}
|
||||
},
|
||||
|
||||
// Track create/fromPartial calls that contain nested object literals
|
||||
"CallExpression[callee.type='MemberExpression'][callee.property.name=/^(create|fromPartial)$/]"(node) {
|
||||
if (node.arguments.length > 0 && node.arguments[0].type === "ObjectExpression") {
|
||||
// Track problematic nested object literals
|
||||
const nestedObjectLiterals = new Map() // Map of object expressions to their containing property paths
|
||||
|
||||
// Search for nested object literals
|
||||
const queue = [
|
||||
...node.arguments[0].properties.map((prop) => ({
|
||||
property: prop,
|
||||
path: prop.key && prop.key.name ? prop.key.name : "unknown",
|
||||
})),
|
||||
]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { property, path } = queue.shift()
|
||||
|
||||
// Skip spread elements
|
||||
if (property.type !== "Property") continue
|
||||
|
||||
// If this is an object literal, mark it as problematic
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
nestedObjectLiterals.set(property.value, path)
|
||||
|
||||
// Add nested properties to queue
|
||||
queue.push(
|
||||
...property.value.properties.map((prop) => ({
|
||||
property: prop,
|
||||
path: `${path}.${prop.key && prop.key.name ? prop.key.name : "unknown"}`,
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// For each problematic nested object, track it with its path
|
||||
nestedObjectLiterals.forEach((path, objectExpr) => {
|
||||
safeObjectExpressions.set(objectExpr, {
|
||||
isProblematic: true,
|
||||
path: path,
|
||||
parentNode: node,
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Check calls to gRPC service clients
|
||||
"CallExpression[callee.type='MemberExpression']"(node) {
|
||||
// Get the object (left side) of the member expression
|
||||
const callee = node.callee
|
||||
if (callee.object && callee.object.type === "Identifier") {
|
||||
const objectName = callee.object.name
|
||||
|
||||
// Check if this is a call to one of our gRPC service clients
|
||||
if (isGrpcServiceClient(objectName)) {
|
||||
// Only check the first argument of gRPC service client calls
|
||||
if (node.arguments.length > 0) {
|
||||
const arg = node.arguments[0] // Only check the first parameter
|
||||
if (arg.type === "ObjectExpression" && !safeObjectExpressions.has(arg)) {
|
||||
// This is an object literal being passed directly to a gRPC client
|
||||
const sourceCode = context.getSourceCode()
|
||||
const callText = sourceCode.getText(node).trim()
|
||||
|
||||
context.report({
|
||||
node: arg,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
code: callText,
|
||||
},
|
||||
})
|
||||
} else if (arg.type === "ObjectExpression") {
|
||||
// Search for nested object literals that aren't protected
|
||||
const queue = [...arg.properties]
|
||||
while (queue.length > 0) {
|
||||
const property = queue.shift()
|
||||
|
||||
// Skip spread elements
|
||||
if (property.type !== "Property") continue
|
||||
|
||||
// Check value
|
||||
if (
|
||||
property.value.type === "ObjectExpression" &&
|
||||
!safeObjectExpressions.has(property.value)
|
||||
) {
|
||||
// Found a nested object literal
|
||||
const sourceCode = context.getSourceCode()
|
||||
const propertyText = sourceCode.getText(property).trim()
|
||||
|
||||
context.report({
|
||||
node: property.value,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
code: `${objectName}.${callee.property.name}(... ${propertyText} ...)`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Add any nested properties to the queue
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
queue.push(...property.value.properties)
|
||||
}
|
||||
}
|
||||
} else if (arg.type === "Identifier") {
|
||||
// This is a variable - check if it references a problematic protobuf object
|
||||
const varName = arg.name
|
||||
const sourceCode = context.getSourceCode()
|
||||
const scope = sourceCode.getScope(node)
|
||||
|
||||
// Find the variable declaration
|
||||
const variable = scope.variables.find((v) => v.name === varName)
|
||||
if (variable && variable.references && variable.references.length > 0) {
|
||||
// Look for definitions
|
||||
const def = variable.defs.find(
|
||||
(d) => d.node && d.node.type === "VariableDeclarator" && d.node.init,
|
||||
)
|
||||
|
||||
if (
|
||||
def &&
|
||||
def.node.init.type === "CallExpression" &&
|
||||
def.node.init.callee.type === "MemberExpression" &&
|
||||
(def.node.init.callee.property.name === "create" ||
|
||||
def.node.init.callee.property.name === "fromPartial")
|
||||
) {
|
||||
// Flag if we find problematic nested object literals in this create/fromPartial call
|
||||
const callText = sourceCode.getText(node).trim()
|
||||
const initCallText = sourceCode.getText(def.node.init).trim()
|
||||
|
||||
// Check for nested object literals in init node
|
||||
let foundNestedLiteral = false
|
||||
if (
|
||||
def.node.init.arguments.length > 0 &&
|
||||
def.node.init.arguments[0].type === "ObjectExpression"
|
||||
) {
|
||||
// Find any nested object literals
|
||||
const queue = [...def.node.init.arguments[0].properties]
|
||||
while (queue.length > 0 && !foundNestedLiteral) {
|
||||
const property = queue.shift()
|
||||
|
||||
// Skip spread elements
|
||||
if (property.type !== "Property") continue
|
||||
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
foundNestedLiteral = true
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
code: `${callText} - using request created with nested object literal at: ${property.key.name}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Add any nested properties to the queue
|
||||
if (property.value.type === "ObjectExpression") {
|
||||
queue.push(...property.value.properties)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,556 @@
|
||||
const { ESLintUtils } = require("@typescript-eslint/utils")
|
||||
|
||||
const createRule = ESLintUtils.RuleCreator((name) => `https://cline.bot/eslint-rules/${name}`)
|
||||
|
||||
module.exports = createRule({
|
||||
name: "no-protobuf-object-literals",
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description: "Enforce using .create() or .fromPartial() for protobuf objects instead of object literals",
|
||||
recommended: "error",
|
||||
},
|
||||
fixable: "code",
|
||||
messages: {
|
||||
useProtobufMethod:
|
||||
"Use {{typeName}}.create() or {{typeName}}.fromPartial() instead of " +
|
||||
"object literal for protobuf type from @shared/proto\n" +
|
||||
"Found: {{code}}\n Suggestion: " +
|
||||
"{{typeName}}.create({{objectContent}})",
|
||||
useProtobufMethodGeneric:
|
||||
"Use .create() or .fromPartial() instead of object literal for protobuf " +
|
||||
"type from @shared/proto\n Found: {{code}}",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
protobufPackages: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
default: ["shared/proto/"],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultOptions: [{ protobufPackages: ["shared/proto/"] }],
|
||||
|
||||
create(context, [options]) {
|
||||
const protobufPackages = options.protobufPackages
|
||||
const protobufImports = new Set() // Set of imported protobuf types
|
||||
const protobufNamespaceImports = new Set() // For namespace imports like "import * as proto"
|
||||
const safeObjectExpressions = new Set() // Track object expressions in create/fromPartial calls
|
||||
|
||||
return {
|
||||
// Skip object literals inside create() or fromPartial() method calls
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee &&
|
||||
node.callee.type === "MemberExpression" &&
|
||||
(node.callee.property.name === "create" || node.callee.property.name === "fromPartial") &&
|
||||
node.arguments.length > 0 &&
|
||||
node.arguments[0].type === "ObjectExpression"
|
||||
) {
|
||||
// Track this object expression as being used with create/fromPartial
|
||||
safeObjectExpressions.add(node.arguments[0])
|
||||
}
|
||||
},
|
||||
|
||||
// Track imports from protobuf packages
|
||||
ImportDeclaration(node) {
|
||||
const packageName = node.source.value
|
||||
|
||||
if (matchesProtobufPackage(packageName, protobufPackages)) {
|
||||
// This is a protobuf package.
|
||||
node.specifiers.forEach((spec) => {
|
||||
if (spec.type === "ImportSpecifier") {
|
||||
// import { MyRequest } from '@shared/proto'
|
||||
protobufImports.add(spec.imported.name)
|
||||
} else if (spec.type === "ImportNamespaceSpecifier") {
|
||||
// import * as proto from '@shared/proto'
|
||||
protobufNamespaceImports.add(spec.local.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// Check variable declarations with type annotations
|
||||
"VariableDeclarator > ObjectExpression"(node) {
|
||||
// Skip if this is inside a create/fromPartial call
|
||||
if (safeObjectExpressions.has(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Found object literal in variable declaration
|
||||
const declarator = node.parent
|
||||
|
||||
if (declarator.id && declarator.id.typeAnnotation) {
|
||||
const typeName = getTypeName(declarator.id.typeAnnotation.typeAnnotation)
|
||||
if (typeName) {
|
||||
// Check if it's a direct protobuf import
|
||||
if (protobufImports.has(typeName)) {
|
||||
//console.log('🚨 VIOLATION: Using object literal for protobuf type:', typeName);
|
||||
const sourceCode = context.getSourceCode()
|
||||
const declaratorText = sourceCode.getText(declarator)
|
||||
const objectText = sourceCode.getText(node)
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
typeName,
|
||||
code: declaratorText,
|
||||
objectContent: objectText,
|
||||
},
|
||||
fix(fixer) {
|
||||
// Replace the object literal with Type.create() call
|
||||
return fixer.replaceText(node, `${typeName}.create(${objectText})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a namespaced protobuf type (e.g., proto.MyRequest)
|
||||
if (isNamespacedProtobufType(protobufNamespaceImports, typeName)) {
|
||||
//console.log('🚨 VIOLATION: Using object literal for namespaced protobuf type:', typeName);
|
||||
const sourceCode = context.getSourceCode()
|
||||
const declaratorText = sourceCode.getText(declarator)
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethodGeneric",
|
||||
data: { code: declaratorText },
|
||||
fix(fixer) {
|
||||
// For namespaced types, use the full type name to call create()
|
||||
return fixer.replaceText(node, `${typeName}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Check assignment expressions
|
||||
"AssignmentExpression > ObjectExpression"(node) {
|
||||
// Skip if this is inside a create/fromPartial call
|
||||
if (safeObjectExpressions.has(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
const assignment = node.parent
|
||||
|
||||
// For assignment to variables without inline type annotation
|
||||
if (assignment.left && assignment.right === node) {
|
||||
let typeName = null
|
||||
|
||||
// Check if there's a typeAnnotation directly on the left
|
||||
if (assignment.left.typeAnnotation) {
|
||||
typeName = getTypeName(assignment.left.typeAnnotation.typeAnnotation)
|
||||
}
|
||||
// Otherwise try to infer from the variable name if it's a simple identifier
|
||||
else if (assignment.left.type === "Identifier") {
|
||||
const varName = assignment.left.name
|
||||
// Check variable declarations in the current scope
|
||||
const sourceCode = context.getSourceCode()
|
||||
const scope = sourceCode.getScope(node)
|
||||
const variable = scope.variables.find((v) => v.name === varName)
|
||||
if (variable && variable.defs.length > 0) {
|
||||
const def = variable.defs[0]
|
||||
if (def.node.id && def.node.id.typeAnnotation) {
|
||||
typeName = getTypeName(def.node.id.typeAnnotation.typeAnnotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeName && protobufImports.has(typeName)) {
|
||||
//console.log('🚨 VIOLATION: Using object literal in assignment for protobuf type:', typeName);
|
||||
const sourceCode = context.getSourceCode()
|
||||
const assignmentText = sourceCode.getText(assignment.left) + " = "
|
||||
const objectText = sourceCode.getText(node)
|
||||
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
typeName,
|
||||
code: assignmentText + "{",
|
||||
objectContent: objectText,
|
||||
},
|
||||
fix(fixer) {
|
||||
// Replace the object literal with Type.create() call in assignments
|
||||
return fixer.replaceText(node, `${typeName}.create(${objectText})`)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Check return statements
|
||||
"ReturnStatement > ObjectExpression"(node) {
|
||||
// Skip if this is inside a create/fromPartial call
|
||||
if (safeObjectExpressions.has(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the parent function to get its return type
|
||||
const functionNode = findParentFunction(node)
|
||||
if (!functionNode) {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to get the return type using our enhanced helper
|
||||
const sourceCode = context.getSourceCode()
|
||||
let returnTypeName = getFunctionReturnType(functionNode, sourceCode)
|
||||
|
||||
// For async functions with Promise<Type> return type, extract the inner type
|
||||
if (returnTypeName && returnTypeName.startsWith("Promise<") && returnTypeName.endsWith(">")) {
|
||||
returnTypeName = returnTypeName.slice(8, -1)
|
||||
}
|
||||
|
||||
// Check if the return type is a protobuf type
|
||||
if (returnTypeName) {
|
||||
if (protobufImports.has(returnTypeName)) {
|
||||
//console.log('🚨 VIOLATION: Return type is a protobuf type:', returnTypeName);
|
||||
const sourceCode = context.getSourceCode()
|
||||
const returnText = sourceCode.getText(node.parent)
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
typeName: returnTypeName,
|
||||
code: returnText,
|
||||
objectContent: sourceCode.getText(node),
|
||||
},
|
||||
fix(fixer) {
|
||||
// Replace the object literal with Type.create() call in return statements
|
||||
return fixer.replaceText(node, `${returnTypeName}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a namespaced protobuf type
|
||||
if (isNamespacedProtobufType(protobufNamespaceImports, returnTypeName)) {
|
||||
const sourceCode = context.getSourceCode()
|
||||
const returnText = sourceCode.getText(node.parent)
|
||||
//console.log('🚨 VIOLATION: Return type is a namespaced protobuf type:', returnTypeName);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethodGeneric",
|
||||
data: { code: returnText },
|
||||
fix(fixer) {
|
||||
// For namespaced types in return statements, we need to extract the full type name
|
||||
const objectCode = sourceCode.getText(node)
|
||||
// Since we may not know the exact type, we'll use the more generic namespaced type
|
||||
return fixer.replaceText(node, `${returnTypeName}.create(${objectCode})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback - if there are any protobuf imports and the function signature
|
||||
// mentions a return type that matches one of the imported types
|
||||
const functionText = functionNode ? sourceCode.getText(functionNode) : ""
|
||||
|
||||
for (const protoType of protobufImports) {
|
||||
// Use more precise regex to match return type patterns specifically
|
||||
// Rather than just checking if the type name appears anywhere in the signature
|
||||
const returnTypeRegex = new RegExp(
|
||||
// Match arrow function return type
|
||||
`=>\\s*:?\\s*${protoType}\\b|` +
|
||||
// Match function declaration return type
|
||||
`\\)\\s*:?\\s*${protoType}\\b|` +
|
||||
// Match Promise return type
|
||||
`\\)\\s*:?\\s*Promise<\\s*${protoType}\\s*>|` +
|
||||
// Match function type in variable declaration
|
||||
`:\\s*\\(.*\\)\\s*=>\\s*${protoType}\\b`,
|
||||
)
|
||||
|
||||
if (returnTypeRegex.test(functionText)) {
|
||||
const returnText = sourceCode.getText(node.parent)
|
||||
//console.log('🚨 VIOLATION: regex matched protobuf type:', functionText);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethod",
|
||||
data: {
|
||||
typeName: protoType,
|
||||
code: returnText,
|
||||
objectContent: sourceCode.getText(node),
|
||||
},
|
||||
fix(fixer) {
|
||||
// Replace the object literal with Type.create() call
|
||||
return fixer.replaceText(node, `${protoType}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
// Check for namespace imports too
|
||||
for (const namespace of protobufNamespaceImports) {
|
||||
// Similar to above, but for namespaced types
|
||||
const namespaceReturnTypeRegex = new RegExp(
|
||||
// Match arrow function return type
|
||||
`=>\\s*:?\\s*${namespace}\\.\\w+\\b|` +
|
||||
// Match function declaration return type
|
||||
`\\)\\s*:?\\s*${namespace}\\.\\w+\\b|` +
|
||||
// Match Promise return type
|
||||
`\\)\\s*:?\\s*Promise<\\s*${namespace}\\.\\w+\\s*>|` +
|
||||
// Match function type in variable declaration
|
||||
`:\\s*\\(.*\\)\\s*=>\\s*${namespace}\\.\\w+\\b`,
|
||||
)
|
||||
|
||||
if (namespaceReturnTypeRegex.test(functionText)) {
|
||||
const returnText = sourceCode.getText(node.parent)
|
||||
//console.log('🚨 VIOLATION: regex matched namespaced protobuf type:', functionText, "namespace:", namespace);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethodGeneric",
|
||||
data: { code: returnText },
|
||||
fix(fixer) {
|
||||
// For namespaced types based on function signature patterns
|
||||
// Extract the namespace and type from the function text using more precise patterns
|
||||
const match = functionText.match(
|
||||
new RegExp(
|
||||
// Match return type patterns more precisely
|
||||
`\\)\\s*:?\\s*(${namespace}\\.[\\w]+)\\b|` + // Function declaration
|
||||
`=>\\s*:?\\s*(${namespace}\\.[\\w]+)\\b|` + // Arrow function
|
||||
`Promise<\\s*(${namespace}\\.[\\w]+)\\s*>`, // Promise wrapped
|
||||
),
|
||||
)
|
||||
if (match) {
|
||||
const fullType = match[1] || match[2]
|
||||
return fixer.replaceText(node, `${fullType}.create(${sourceCode.getText(node)})`)
|
||||
}
|
||||
// Fallback - we can't determine the exact type, but we know it's from the namespace
|
||||
// Use a namespace-based approach
|
||||
return fixer.replaceText(node, `${namespace}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Check function call arguments (more selective approach)
|
||||
"CallExpression > ObjectExpression"(node) {
|
||||
// Skip if this is inside a create/fromPartial call
|
||||
if (safeObjectExpressions.has(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
// We need to be more selective to avoid false positives
|
||||
// Only warn if:
|
||||
// 1. The function is called on a protobuf namespace
|
||||
// 2. The call argument has a type annotation that matches a protobuf type
|
||||
// 3. The call is to a function that we know takes a protobuf type
|
||||
|
||||
// Check if it's a call on a protobuf namespace
|
||||
if (
|
||||
node.parent.callee &&
|
||||
node.parent.callee.type === "MemberExpression" &&
|
||||
node.parent.callee.object.type === "Identifier"
|
||||
) {
|
||||
const namespace = node.parent.callee.object.name
|
||||
if (protobufNamespaceImports.has(namespace)) {
|
||||
const sourceCode = context.getSourceCode()
|
||||
const callText = sourceCode.getText(node.parent)
|
||||
//console.log('🚨 VIOLATION: Check function call arguments object literal:', callText);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethodGeneric",
|
||||
data: { code: callText },
|
||||
fix(fixer) {
|
||||
// For calls on a protobuf namespace
|
||||
const memberExpr = node.parent.callee
|
||||
// Try to determine if this is calling a method that expects a specific type
|
||||
const methodName = memberExpr.property.name
|
||||
|
||||
// If method name looks like 'create' + Type, we can infer the type
|
||||
const possibleTypeName = methodName.replace(/^create/, "")
|
||||
|
||||
// Check if namespace has a type with this name
|
||||
// Since we can't directly check at lint time, we'll use the namespace + inferred type
|
||||
if (possibleTypeName && possibleTypeName !== methodName) {
|
||||
return fixer.replaceText(
|
||||
node,
|
||||
`${namespace}.${possibleTypeName}.create(${sourceCode.getText(node)})`,
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback - use a more generic approach with namespace
|
||||
return fixer.replaceText(node, `${namespace}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// For regular function calls with object literals, check if there are protobuf imports
|
||||
// and if the function might expect a protobuf type
|
||||
if (node.parent.callee) {
|
||||
// This is a more permissive check to catch cases like processContent({ ... })
|
||||
// which might be passing a protobuf type
|
||||
const sourceCode = context.getSourceCode()
|
||||
const scope = sourceCode.getScope(node)
|
||||
|
||||
// Try to find the function definition
|
||||
if (node.parent.callee.type === "Identifier") {
|
||||
const functionName = node.parent.callee.name
|
||||
const variable = scope.variables.find((v) => v.name === functionName)
|
||||
|
||||
// If we found the function and it has parameter type annotations
|
||||
// that match protobuf types, flag it
|
||||
if (variable && variable.defs.length > 0) {
|
||||
const def = variable.defs[0]
|
||||
if (def.node.params && node.parent.arguments.indexOf(node) < def.node.params.length) {
|
||||
const param = def.node.params[node.parent.arguments.indexOf(node)]
|
||||
if (param.typeAnnotation) {
|
||||
const typeName = getTypeName(param.typeAnnotation.typeAnnotation)
|
||||
if (
|
||||
typeName &&
|
||||
(protobufImports.has(typeName) ||
|
||||
isNamespacedProtobufType(protobufNamespaceImports, typeName))
|
||||
) {
|
||||
const callText = sourceCode.getText(node.parent)
|
||||
//console.log('🚨 VIOLATION: Function call arguments object literal:', callText);
|
||||
context.report({
|
||||
node,
|
||||
messageId: "useProtobufMethodGeneric",
|
||||
data: { code: callText },
|
||||
fix(fixer) {
|
||||
// For function calls with protobuf type parameters
|
||||
return fixer.replaceText(node, `${typeName}.create(${sourceCode.getText(node)})`)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Helper functions
|
||||
function getTypeName(typeAnnotation) {
|
||||
if (!typeAnnotation) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeAnnotation.type === "TSTypeReference") {
|
||||
if (typeAnnotation.typeName.type === "Identifier") {
|
||||
return typeAnnotation.typeName.name
|
||||
} else if (typeAnnotation.typeName.type === "TSQualifiedName") {
|
||||
// Handle namespaced types like proto.MyRequest
|
||||
return `${typeAnnotation.typeName.left.name}.${typeAnnotation.typeName.right.name}`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function matchesProtobufPackage(packageName, protobufPackages) {
|
||||
return protobufPackages.some((protobufPackage) => {
|
||||
// Remove leading and trailing @ and / from protobufPackage
|
||||
const cleanedPackage = protobufPackage.replace(/^[@\/]/, "").replace(/[\/]$/, "")
|
||||
const pattern = new RegExp(`(.*[@/]|)${escapeRegex(cleanedPackage)}[/].*`)
|
||||
return pattern.test(packageName)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to escape special regex characters
|
||||
function escapeRegex(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
// Helper to extract function return type more reliably
|
||||
function getFunctionReturnType(functionNode, sourceCode) {
|
||||
// 1. Check explicit return type annotation
|
||||
if (functionNode.returnType) {
|
||||
return getTypeName(functionNode.returnType.typeAnnotation)
|
||||
}
|
||||
|
||||
// 2. For variable declarations like const foo: (arg: Type) => ReturnType = ...
|
||||
if (functionNode.parent && functionNode.parent.type === "VariableDeclarator") {
|
||||
const declarator = functionNode.parent
|
||||
if (declarator.id && declarator.id.typeAnnotation) {
|
||||
const typeAnnotation = declarator.id.typeAnnotation.typeAnnotation
|
||||
|
||||
// Handle function type annotations
|
||||
if (typeAnnotation.type === "TSFunctionType" && typeAnnotation.typeAnnotation) {
|
||||
return getTypeName(typeAnnotation.typeAnnotation)
|
||||
}
|
||||
|
||||
// Handle type references to function types
|
||||
if (typeAnnotation.type === "TSTypeReference") {
|
||||
// This might be a type like Promise<ReturnType>
|
||||
if (
|
||||
typeAnnotation.typeName.name === "Promise" &&
|
||||
typeAnnotation.typeParameters &&
|
||||
typeAnnotation.typeParameters.params.length > 0
|
||||
) {
|
||||
return getTypeName(typeAnnotation.typeParameters.params[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. For class methods, check if it's part of an interface implementation
|
||||
if (
|
||||
functionNode.parent &&
|
||||
functionNode.parent.type === "MethodDefinition" &&
|
||||
functionNode.parent.parent &&
|
||||
functionNode.parent.parent.type === "ClassBody"
|
||||
) {
|
||||
const className = getEnclosingClassName(functionNode)
|
||||
const methodName = functionNode.parent.key.name
|
||||
|
||||
if (className && methodName) {
|
||||
// Look for interface declarations in the scope
|
||||
const scope = sourceCode.getScope(functionNode)
|
||||
// This would require more complex scope analysis which is limited in ESLint
|
||||
// For now, we'll return null and rely on other methods
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Helper to get the class name for a method
|
||||
function getEnclosingClassName(node) {
|
||||
let current = node.parent
|
||||
while (current) {
|
||||
if (current.type === "ClassDeclaration" && current.id) {
|
||||
return current.id.name
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
function isNamespacedProtobufType(protobufNamespaceImports, typeName) {
|
||||
if (!typeName.includes(".")) {
|
||||
return false
|
||||
}
|
||||
|
||||
const namespace = typeName.split(".")[0]
|
||||
return protobufNamespaceImports.has(namespace)
|
||||
}
|
||||
|
||||
function findParentFunction(node) {
|
||||
let current = node.parent
|
||||
while (current) {
|
||||
if (
|
||||
current.type === "FunctionDeclaration" ||
|
||||
current.type === "FunctionExpression" ||
|
||||
current.type === "ArrowFunctionExpression"
|
||||
) {
|
||||
return current
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
Generated
+23
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.20.1",
|
||||
"version": "3.19.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.20.1",
|
||||
"version": "3.19.7",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -7907,9 +7907,10 @@
|
||||
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
@@ -17785,9 +17786,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz",
|
||||
"integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
|
||||
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^3.1.5"
|
||||
@@ -18416,9 +18418,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
||||
"version": "6.21.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
@@ -25160,9 +25163,9 @@
|
||||
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
@@ -31798,9 +31801,9 @@
|
||||
}
|
||||
},
|
||||
"tar-fs": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz",
|
||||
"integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==",
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
|
||||
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
|
||||
"requires": {
|
||||
"bare-fs": "^4.0.1",
|
||||
"bare-path": "^3.0.0",
|
||||
@@ -32223,9 +32226,9 @@
|
||||
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="
|
||||
},
|
||||
"undici": {
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="
|
||||
"version": "6.21.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
|
||||
+9
-25
@@ -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.20.2",
|
||||
"version": "3.19.7",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -186,12 +186,6 @@
|
||||
"category": "Cline",
|
||||
"icon": "$(robot)"
|
||||
},
|
||||
{
|
||||
"command": "cline.abortGitCommitMessage",
|
||||
"title": "Generate Commit Message with Cline - Stop",
|
||||
"category": "Cline",
|
||||
"icon": "$(debug-stop)"
|
||||
},
|
||||
{
|
||||
"command": "cline.explainCode",
|
||||
"title": "Explain with Cline",
|
||||
@@ -219,7 +213,7 @@
|
||||
},
|
||||
{
|
||||
"command": "cline.generateGitCommitMessage",
|
||||
"when": "config.git.enabled && scmProvider == git"
|
||||
"when": "scmProvider == git"
|
||||
},
|
||||
{
|
||||
"command": "cline.focusChatInput",
|
||||
@@ -312,22 +306,13 @@
|
||||
{
|
||||
"command": "cline.generateGitCommitMessage",
|
||||
"group": "navigation",
|
||||
"when": "config.git.enabled && scmProvider == git && !cline.isGeneratingCommit"
|
||||
},
|
||||
{
|
||||
"command": "cline.abortGitCommitMessage",
|
||||
"group": "navigation",
|
||||
"when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit"
|
||||
"when": "scmProvider == git"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "cline.generateGitCommitMessage",
|
||||
"when": "config.git.enabled && scmProvider == git && !cline.isGeneratingCommit"
|
||||
},
|
||||
{
|
||||
"command": "cline.abortGitCommitMessage",
|
||||
"when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit"
|
||||
"when": "scmProvider == git"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -338,14 +323,14 @@
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.mjs",
|
||||
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||
"compile-standalone": "npm run check-types && npm run lint && node esbuild.js --standalone",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.mjs --watch",
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.mjs --production",
|
||||
"protos": "node scripts/build-proto.mjs",
|
||||
"package": "npm run check-types && npm run build:webview && npm run lint && node esbuild.js --production",
|
||||
"protos": "node scripts/build-proto.mjs && node scripts/generate-server-setup.mjs && node scripts/generate-host-bridge-client.mjs",
|
||||
"postprotos": "prettier src/shared/proto src/core/controller src/hosts/ webview-ui/src/services src/generated --write --log-level warn",
|
||||
"clean": "rimraf dist dist-standalone webview-ui/build src/generated out/",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
@@ -362,7 +347,6 @@
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"e2e": "playwright test -c playwright.config.ts",
|
||||
"test:e2e": "playwright install && vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
|
||||
"test:e2e:optimal": "vsce package --no-dependencies --out dist/e2e.vsix && node src/test/e2e/utils/build.js && playwright test",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
"build:webview": "cd webview-ui && npm run build",
|
||||
|
||||
+6
-21
@@ -1,32 +1,17 @@
|
||||
import { defineConfig } from "@playwright/test"
|
||||
|
||||
const isCI = !!process?.env?.CI
|
||||
const isWindow = process?.platform?.startsWith("win")
|
||||
const isGitHubAction = !!process.env.CI
|
||||
|
||||
export default defineConfig({
|
||||
workers: 1,
|
||||
retries: 1,
|
||||
testDir: "src/test/e2e",
|
||||
timeout: isCI || isWindow ? 40000 : 20000,
|
||||
timeout: 20000,
|
||||
expect: {
|
||||
timeout: isCI || isWindow ? 5000 : 2000,
|
||||
timeout: 20000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
reporter: isCI ? [["github"], ["list"]] : [["list"]],
|
||||
projects: [
|
||||
{
|
||||
name: "setup test environment",
|
||||
testMatch: /global\.setup\.ts/,
|
||||
},
|
||||
{
|
||||
name: "e2e tests",
|
||||
testMatch: /.*\.test\.ts/,
|
||||
dependencies: ["setup test environment"],
|
||||
},
|
||||
{
|
||||
name: "cleanup test environment",
|
||||
testMatch: /global\.teardown\.ts/,
|
||||
dependencies: ["e2e tests"],
|
||||
},
|
||||
],
|
||||
reporter: isGitHubAction ? [["github"], ["list"]] : [["list"]],
|
||||
globalSetup: require.resolve("./src/test/e2e/utils/setup"),
|
||||
globalTeardown: require.resolve("./src/test/e2e/utils/teardown"),
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -53,7 +53,6 @@ message UserInfo {
|
||||
optional string display_name = 2;
|
||||
optional string email = 3;
|
||||
optional string photo_url = 4;
|
||||
optional string app_base_url = 5; // Cline app base URL
|
||||
}
|
||||
|
||||
message UserOrganization {
|
||||
@@ -127,4 +126,4 @@ message OrganizationUsageTransaction {
|
||||
int32 prompt_tokens = 10;
|
||||
int32 total_tokens = 11;
|
||||
string user_id = 12;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
+2
-10
@@ -4,7 +4,7 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
|
||||
// Provides methods for diff views.
|
||||
service DiffService {
|
||||
@@ -14,7 +14,6 @@ service DiffService {
|
||||
rpc getDocumentText(GetDocumentTextRequest) returns (GetDocumentTextResponse);
|
||||
// Replace a text selection in the diff.
|
||||
rpc replaceText(ReplaceTextRequest) returns (ReplaceTextResponse);
|
||||
rpc scrollDiff(ScrollDiffRequest) returns (ScrollDiffResponse);
|
||||
// Truncate the diff document.
|
||||
rpc truncateDocument(TruncateDocumentRequest) returns (TruncateDocumentResponse);
|
||||
// Save the diff document.
|
||||
@@ -55,17 +54,10 @@ message ReplaceTextRequest {
|
||||
|
||||
message ReplaceTextResponse {}
|
||||
|
||||
message ScrollDiffRequest {
|
||||
optional string diff_id = 1;
|
||||
optional int32 line = 2;
|
||||
}
|
||||
|
||||
message ScrollDiffResponse {}
|
||||
|
||||
message TruncateDocumentRequest {
|
||||
optional cline.Metadata metadata = 1;
|
||||
optional string diff_id = 2;
|
||||
optional int32 end_line = 3;
|
||||
optional int32 end_line = 5;
|
||||
}
|
||||
|
||||
message TruncateDocumentResponse {}
|
||||
|
||||
@@ -4,7 +4,7 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
|
||||
// Provides methods for working with the user's environment.
|
||||
service EnvService {
|
||||
@@ -13,4 +13,7 @@ service EnvService {
|
||||
|
||||
// Reads text from the system clipboard.
|
||||
rpc clipboardReadText(cline.EmptyRequest) returns (cline.String);
|
||||
|
||||
// Opens a URL in the user's default browser or application.
|
||||
rpc openExternal(cline.StringRequest) returns (cline.Empty);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
|
||||
/**
|
||||
* The watch service is only here as example of a streaming rpc in the host bridge.
|
||||
|
||||
+1
-60
@@ -4,7 +4,7 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
|
||||
// Provides methods for working with IDE windows and editors.
|
||||
service WindowService {
|
||||
@@ -12,11 +12,6 @@ service WindowService {
|
||||
rpc showTextDocument(ShowTextDocumentRequest) returns (TextEditorInfo);
|
||||
rpc showOpenDialogue(ShowOpenDialogueRequest) returns (SelectedResources);
|
||||
rpc showMessage(ShowMessageRequest) returns (SelectedResponse);
|
||||
rpc showInputBox(ShowInputBoxRequest) returns (ShowInputBoxResponse);
|
||||
rpc showSaveDialog(ShowSaveDialogRequest) returns (ShowSaveDialogResponse);
|
||||
rpc openFile(OpenFileRequest) returns (OpenFileResponse);
|
||||
rpc getOpenTabs(GetOpenTabsRequest) returns (GetOpenTabsResponse);
|
||||
rpc getVisibleTabs(GetVisibleTabsRequest) returns (GetVisibleTabsResponse);
|
||||
}
|
||||
|
||||
message ShowTextDocumentRequest {
|
||||
@@ -75,58 +70,4 @@ message ShowMessageRequestOptions {
|
||||
|
||||
message SelectedResponse {
|
||||
optional string selected_option = 1;
|
||||
}
|
||||
|
||||
message ShowSaveDialogRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
optional ShowSaveDialogOptions options = 2;
|
||||
}
|
||||
|
||||
message ShowSaveDialogOptions {
|
||||
optional string default_path = 1;
|
||||
map<string, FileExtensionList> filters = 2;
|
||||
}
|
||||
|
||||
message FileExtensionList {
|
||||
repeated string extensions = 1;
|
||||
}
|
||||
|
||||
message ShowSaveDialogResponse {
|
||||
optional string selected_path = 1;
|
||||
}
|
||||
|
||||
message ShowInputBoxRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
string title = 2;
|
||||
optional string prompt = 3;
|
||||
optional string value = 4;
|
||||
}
|
||||
|
||||
message ShowInputBoxResponse {
|
||||
optional string response = 1;
|
||||
}
|
||||
|
||||
message OpenFileRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
string file_path = 2;
|
||||
}
|
||||
|
||||
message OpenFileResponse {
|
||||
// empty
|
||||
}
|
||||
|
||||
message GetOpenTabsRequest {
|
||||
// empty
|
||||
}
|
||||
|
||||
message GetOpenTabsResponse {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
|
||||
message GetVisibleTabsRequest {
|
||||
// empty
|
||||
}
|
||||
|
||||
message GetVisibleTabsResponse {
|
||||
repeated string paths = 1;
|
||||
}
|
||||
@@ -4,14 +4,10 @@ package host;
|
||||
option java_package = "bot.cline.host.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
// Provides methods for working with workspaces/projects.
|
||||
service WorkspaceService {
|
||||
// Returns a list of the top level directories of the workspace.
|
||||
rpc getWorkspacePaths(GetWorkspacePathsRequest) returns (GetWorkspacePathsResponse);
|
||||
// Saves an open document if it's dirty
|
||||
rpc saveOpenDocumentIfDirty(SaveOpenDocumentIfDirtyRequest) returns (cline.Empty);
|
||||
}
|
||||
|
||||
message GetWorkspacePathsRequest {
|
||||
@@ -26,8 +22,3 @@ message GetWorkspacePathsResponse {
|
||||
optional string id = 1;
|
||||
repeated string paths = 2;
|
||||
}
|
||||
|
||||
message SaveOpenDocumentIfDirtyRequest {
|
||||
cline.Metadata metadata = 1;
|
||||
string file_path = 2;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -129,7 +129,6 @@ enum ApiProvider {
|
||||
CLAUDE_CODE = 26;
|
||||
MOONSHOT = 27;
|
||||
HUGGINGFACE = 28;
|
||||
HUAWEI_CLOUD_MAAS = 29;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -230,11 +229,9 @@ message ModelsApiConfiguration {
|
||||
optional string cline_account_id = 58;
|
||||
optional string groq_api_key = 59;
|
||||
optional string hugging_face_api_key = 60;
|
||||
optional string huawei_cloud_maas_api_key = 61;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
optional string plan_mode_api_model_id = 101;
|
||||
optional int32 plan_mode_thinking_budget_tokens = 102;
|
||||
optional string plan_mode_reasoning_effort = 103;
|
||||
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 104;
|
||||
@@ -257,12 +254,26 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 121;
|
||||
optional string plan_mode_hugging_face_model_id = 122;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 123;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 124;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 125;
|
||||
optional string plan_mode_anthropic_model_id = 124;
|
||||
optional string plan_mode_claude_code_model_id = 125;
|
||||
optional string plan_mode_vertex_model_id = 126;
|
||||
optional string plan_mode_gemini_model_id = 127;
|
||||
optional string plan_mode_openai_native_model_id = 128;
|
||||
optional string plan_mode_deepseek_model_id = 129;
|
||||
optional string plan_mode_qwen_model_id = 130;
|
||||
optional string plan_mode_doubao_model_id = 131;
|
||||
optional string plan_mode_mistral_model_id = 132;
|
||||
optional string plan_mode_asksage_model_id = 133;
|
||||
optional string plan_mode_xai_model_id = 134;
|
||||
optional string plan_mode_moonshot_model_id = 135;
|
||||
optional string plan_mode_nebius_model_id = 136;
|
||||
optional string plan_mode_sambanova_model_id = 137;
|
||||
optional string plan_mode_cerebras_model_id = 138;
|
||||
optional string plan_mode_aws_bedrock_model_id = 139;
|
||||
optional string plan_mode_sapaicore_model_id = 140;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
optional string act_mode_api_model_id = 201;
|
||||
optional int32 act_mode_thinking_budget_tokens = 202;
|
||||
optional string act_mode_reasoning_effort = 203;
|
||||
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 204;
|
||||
@@ -285,8 +296,23 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 221;
|
||||
optional string act_mode_hugging_face_model_id = 222;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 223;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 224;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 225;
|
||||
optional string act_mode_anthropic_model_id = 224;
|
||||
optional string act_mode_claude_code_model_id = 225;
|
||||
optional string act_mode_vertex_model_id = 226;
|
||||
optional string act_mode_gemini_model_id = 227;
|
||||
optional string act_mode_openai_native_model_id = 228;
|
||||
optional string act_mode_deepseek_model_id = 229;
|
||||
optional string act_mode_qwen_model_id = 230;
|
||||
optional string act_mode_doubao_model_id = 231;
|
||||
optional string act_mode_mistral_model_id = 232;
|
||||
optional string act_mode_asksage_model_id = 233;
|
||||
optional string act_mode_xai_model_id = 234;
|
||||
optional string act_mode_moonshot_model_id = 235;
|
||||
optional string act_mode_nebius_model_id = 236;
|
||||
optional string act_mode_sambanova_model_id = 237;
|
||||
optional string act_mode_cerebras_model_id = 238;
|
||||
optional string act_mode_aws_bedrock_model_id = 239;
|
||||
optional string act_mode_sapaicore_model_id = 240;
|
||||
|
||||
repeated string favorited_model_ids = 300;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
syntax = "proto3";
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
service StateService {
|
||||
rpc getLatestState(EmptyRequest) returns (State);
|
||||
rpc updateTerminalConnectionTimeout(UpdateTerminalConnectionTimeoutRequest) returns (UpdateTerminalConnectionTimeoutResponse);
|
||||
rpc updateTerminalConnectionTimeout(Int64Request) returns (Int64);
|
||||
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
|
||||
rpc updateDefaultTerminalProfile(StringRequest) returns (TerminalProfileUpdateResponse);
|
||||
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
|
||||
rpc subscribeToState(EmptyRequest) returns (stream State);
|
||||
rpc toggleFavoriteModel(StringRequest) returns (Empty);
|
||||
rpc resetState(ResetStateRequest) returns (Empty);
|
||||
rpc togglePlanActModeProto(TogglePlanActModeRequest) returns (Boolean);
|
||||
rpc togglePlanActMode(TogglePlanActModeRequest) returns (Boolean);
|
||||
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
|
||||
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
|
||||
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
|
||||
@@ -43,7 +43,7 @@ message TerminalProfileUpdateResponse {
|
||||
|
||||
message TogglePlanActModeRequest {
|
||||
Metadata metadata = 1;
|
||||
PlanActMode mode = 2;
|
||||
ChatSettings chat_settings = 2;
|
||||
optional ChatContent chat_content = 3;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,12 @@ enum PlanActMode {
|
||||
ACT = 1;
|
||||
}
|
||||
|
||||
message ChatSettings {
|
||||
PlanActMode mode = 1;
|
||||
optional string preferred_language = 2;
|
||||
optional string open_ai_reasoning_effort = 3;
|
||||
}
|
||||
|
||||
message ChatContent {
|
||||
optional string message = 1;
|
||||
repeated string images = 2;
|
||||
@@ -102,14 +108,12 @@ message UpdateSettingsRequest {
|
||||
optional bool plan_act_separate_models_setting = 4;
|
||||
optional bool enable_checkpoints_setting = 5;
|
||||
optional bool mcp_marketplace_enabled = 6;
|
||||
optional int32 shell_integration_timeout = 8;
|
||||
optional ChatSettings chat_settings = 7;
|
||||
optional int64 shell_integration_timeout = 8;
|
||||
optional bool terminal_reuse_enabled = 9;
|
||||
optional bool mcp_responses_collapsed = 10;
|
||||
optional string mcp_display_mode = 11;
|
||||
optional int32 terminal_output_line_limit = 12;
|
||||
optional PlanActMode mode = 13;
|
||||
optional string preferred_language = 14;
|
||||
optional string openai_reasoning_effort = 15;
|
||||
optional int64 terminal_output_line_limit = 12;
|
||||
}
|
||||
|
||||
// Complete API Configuration message
|
||||
@@ -149,8 +153,8 @@ message ApiConfiguration {
|
||||
optional string requesty_api_key = 32;
|
||||
optional string together_api_key = 33;
|
||||
optional string fireworks_api_key = 34;
|
||||
optional int32 fireworks_model_max_completion_tokens = 35;
|
||||
optional int32 fireworks_model_max_tokens = 36;
|
||||
optional int64 fireworks_model_max_completion_tokens = 35;
|
||||
optional int64 fireworks_model_max_tokens = 36;
|
||||
optional string qwen_api_key = 37;
|
||||
optional string doubao_api_key = 38;
|
||||
optional string mistral_api_key = 39;
|
||||
@@ -162,7 +166,7 @@ message ApiConfiguration {
|
||||
optional string xai_api_key = 45;
|
||||
optional string sambanova_api_key = 46;
|
||||
optional string cerebras_api_key = 47;
|
||||
optional int32 request_timeout_ms = 48;
|
||||
optional int64 request_timeout_ms = 48;
|
||||
optional string sap_ai_core_client_id = 49;
|
||||
optional string sap_ai_core_client_secret = 50;
|
||||
optional string sap_ai_resource_group = 51;
|
||||
@@ -170,12 +174,12 @@ message ApiConfiguration {
|
||||
optional string sap_ai_core_base_url = 53;
|
||||
optional string moonshot_api_key = 54;
|
||||
optional string moonshot_api_line = 55;
|
||||
optional string huawei_cloud_maas_api_key = 56;
|
||||
optional string groq_api_key = 56;
|
||||
optional string hugging_face_api_key = 57;
|
||||
|
||||
// Plan mode configurations
|
||||
optional string plan_mode_api_provider = 100;
|
||||
optional string plan_mode_api_model_id = 101;
|
||||
optional int32 plan_mode_thinking_budget_tokens = 102;
|
||||
optional int64 plan_mode_thinking_budget_tokens = 102;
|
||||
optional string plan_mode_reasoning_effort = 103;
|
||||
optional string plan_mode_vscode_lm_model_selector = 104; // JSON string
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 105;
|
||||
@@ -193,13 +197,31 @@ message ApiConfiguration {
|
||||
optional string plan_mode_together_model_id = 117;
|
||||
optional string plan_mode_fireworks_model_id = 118;
|
||||
optional string plan_mode_sap_ai_core_model_id = 119;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 120;
|
||||
optional string plan_mode_huawei_cloud_maas_model_info = 121;
|
||||
optional string plan_mode_anthropic_model_id = 120;
|
||||
optional string plan_mode_claude_code_model_id = 121;
|
||||
optional string plan_mode_vertex_model_id = 122;
|
||||
optional string plan_mode_gemini_model_id = 123;
|
||||
optional string plan_mode_openai_native_model_id = 124;
|
||||
optional string plan_mode_deep_seek_model_id = 125;
|
||||
optional string plan_mode_qwen_model_id = 126;
|
||||
optional string plan_mode_doubao_model_id = 127;
|
||||
optional string plan_mode_mistral_model_id = 128;
|
||||
optional string plan_mode_asksage_model_id = 129;
|
||||
optional string plan_mode_xai_model_id = 130;
|
||||
optional string plan_mode_moonshot_model_id = 131;
|
||||
optional string plan_mode_nebius_model_id = 132;
|
||||
optional string plan_mode_sambanova_model_id = 133;
|
||||
optional string plan_mode_cerebras_model_id = 134;
|
||||
optional string plan_mode_aws_bedrock_model_id = 135;
|
||||
optional string plan_mode_groq_model_id = 136;
|
||||
optional string plan_mode_groq_model_info = 137; // JSON string
|
||||
optional string plan_mode_hugging_face_model_id = 138;
|
||||
optional string plan_mode_hugging_face_model_info = 139; // JSON string
|
||||
optional string plan_mode_sapaicore_model_id = 140;
|
||||
|
||||
// Act mode configurations
|
||||
optional string act_mode_api_provider = 200;
|
||||
optional string act_mode_api_model_id = 201;
|
||||
optional int32 act_mode_thinking_budget_tokens = 202;
|
||||
optional int64 act_mode_thinking_budget_tokens = 202;
|
||||
optional string act_mode_reasoning_effort = 203;
|
||||
optional string act_mode_vscode_lm_model_selector = 204; // JSON string
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 205;
|
||||
@@ -217,8 +239,27 @@ message ApiConfiguration {
|
||||
optional string act_mode_together_model_id = 217;
|
||||
optional string act_mode_fireworks_model_id = 218;
|
||||
optional string act_mode_sap_ai_core_model_id = 219;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 220;
|
||||
optional string act_mode_huawei_cloud_maas_model_info = 221;
|
||||
optional string act_mode_anthropic_model_id = 220;
|
||||
optional string act_mode_claude_code_model_id = 221;
|
||||
optional string act_mode_vertex_model_id = 222;
|
||||
optional string act_mode_gemini_model_id = 223;
|
||||
optional string act_mode_openai_native_model_id = 224;
|
||||
optional string act_mode_deep_seek_model_id = 225;
|
||||
optional string act_mode_qwen_model_id = 226;
|
||||
optional string act_mode_doubao_model_id = 227;
|
||||
optional string act_mode_mistral_model_id = 228;
|
||||
optional string act_mode_asksage_model_id = 229;
|
||||
optional string act_mode_xai_model_id = 230;
|
||||
optional string act_mode_moonshot_model_id = 231;
|
||||
optional string act_mode_nebius_model_id = 232;
|
||||
optional string act_mode_sambanova_model_id = 233;
|
||||
optional string act_mode_cerebras_model_id = 234;
|
||||
optional string act_mode_aws_bedrock_model_id = 235;
|
||||
optional string act_mode_groq_model_id = 236;
|
||||
optional string act_mode_groq_model_info = 237; // JSON string
|
||||
optional string act_mode_hugging_face_model_id = 238;
|
||||
optional string act_mode_hugging_face_model_info = 239; // JSON string
|
||||
optional string act_mode_sapaicore_model_id = 240;
|
||||
|
||||
// Favorited model IDs
|
||||
repeated string favorited_model_ids = 300;
|
||||
@@ -229,11 +270,3 @@ message ApiConfiguration {
|
||||
|
||||
optional string cline_account_id = 303;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutResponse {
|
||||
optional int32 timeout_ms = 1;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
import "cline/common.proto";
|
||||
import "common.proto";
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Configuration file for protocol buffer build scripts
|
||||
// Contains service name mappings used by both build-proto.js and build-go-proto.js
|
||||
|
||||
// List of gRPC services
|
||||
// To add a new service, simply add it to this map and run the build scripts
|
||||
// The service handler will be automatically discovered and used by grpc-handler.ts
|
||||
export const serviceNameMap = {
|
||||
account: "cline.AccountService",
|
||||
browser: "cline.BrowserService",
|
||||
checkpoints: "cline.CheckpointsService",
|
||||
file: "cline.FileService",
|
||||
mcp: "cline.McpService",
|
||||
state: "cline.StateService",
|
||||
task: "cline.TaskService",
|
||||
web: "cline.WebService",
|
||||
models: "cline.ModelsService",
|
||||
slash: "cline.SlashService",
|
||||
ui: "cline.UiService",
|
||||
// Add new services here - no other code changes needed!
|
||||
}
|
||||
|
||||
// List of host gRPC services (IDE API bridge)
|
||||
// These services are implemented in the IDE extension and called by the standalone Cline Core
|
||||
export const hostServiceNameMap = {
|
||||
uri: "host.UriService",
|
||||
watch: "host.WatchService",
|
||||
workspace: "host.WorkspaceService",
|
||||
env: "host.EnvService",
|
||||
window: "host.WindowService",
|
||||
diff: "host.DiffService",
|
||||
// Add new host services here
|
||||
}
|
||||
+357
-87
@@ -1,20 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import chalk from "chalk"
|
||||
import { execSync } from "child_process"
|
||||
import * as fs from "fs/promises"
|
||||
import { globby } from "globby"
|
||||
import { createRequire } from "module"
|
||||
import os from "os"
|
||||
import * as path from "path"
|
||||
import { rmrf } from "./file-utils.mjs"
|
||||
import { main as generateProtoBusSetup } from "./generate-protobus-setup.mjs"
|
||||
import { main as generateHostBridgeClient } from "./generate-host-bridge-client.mjs"
|
||||
import { loadProtoDescriptorSet } from "./proto-utils.mjs"
|
||||
import { fileURLToPath } from "url"
|
||||
import { execSync } from "child_process"
|
||||
import { globby } from "globby"
|
||||
import chalk from "chalk"
|
||||
import os from "os"
|
||||
|
||||
import { createRequire } from "module"
|
||||
import { serviceNameMap } from "./build-proto-config.mjs"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
|
||||
const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url))
|
||||
|
||||
const PROTO_DIR = path.resolve("proto")
|
||||
const TS_OUT_DIR = path.resolve("src/shared/proto")
|
||||
const GRPC_JS_OUT_DIR = path.resolve("src/generated/grpc-js")
|
||||
@@ -35,15 +36,13 @@ const TS_PROTO_OPTIONS = [
|
||||
"useDate=false", // Timestamp fields will not be automatically converted to Date.
|
||||
]
|
||||
|
||||
// Service directories derived from imported serviceNameMap
|
||||
const serviceDirs = Object.keys(serviceNameMap).map((serviceKey) => path.join("src/core/controller", serviceKey))
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.bold.blue("Starting Protocol Buffer code generation..."))
|
||||
|
||||
await cleanup()
|
||||
await compileProtos()
|
||||
await checkProtos()
|
||||
await generateProtoBusSetup()
|
||||
await generateHostBridgeClient()
|
||||
}
|
||||
async function compileProtos() {
|
||||
console.log(chalk.bold.blue("Compiling Protocol Buffers..."))
|
||||
|
||||
// Check for Apple Silicon compatibility before proceeding
|
||||
checkAppleSiliconCompatibility()
|
||||
@@ -53,6 +52,9 @@ async function compileProtos() {
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
}
|
||||
|
||||
// Check for missing proto files for services in serviceNameMap
|
||||
await ensureProtoFilesExist()
|
||||
|
||||
// Process all proto files
|
||||
const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR, realpath: true })
|
||||
console.log(chalk.cyan(`Processing ${protoFiles.length} proto files from`), PROTO_DIR)
|
||||
@@ -81,6 +83,12 @@ async function compileProtos() {
|
||||
|
||||
log_verbose(chalk.green("Protocol Buffer code generation completed successfully."))
|
||||
log_verbose(chalk.green(`TypeScript files generated in: ${TS_OUT_DIR}`))
|
||||
|
||||
await generateProtoBusServiceConfig()
|
||||
await generateProtoBusMethodRegistrations()
|
||||
await generateProtoBusGrpcClientConfig()
|
||||
|
||||
console.log(chalk.bold.blue("Finished Protocol Buffer code generation."))
|
||||
}
|
||||
|
||||
async function tsProtoc(outDir, protoFiles, protoOptions) {
|
||||
@@ -103,6 +111,312 @@ async function tsProtoc(outDir, protoFiles, protoOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a gRPC client configuration file for the webview
|
||||
* This eliminates the need for manual imports and client creation in grpc-client.ts
|
||||
*/
|
||||
async function generateProtoBusGrpcClientConfig() {
|
||||
log_verbose(chalk.cyan("Generating gRPC client configuration..."))
|
||||
|
||||
const serviceImports = []
|
||||
const serviceClientCreations = []
|
||||
const serviceExports = []
|
||||
|
||||
// Process each service in the serviceNameMap
|
||||
for (const [dirName, _fullServiceName] of Object.entries(serviceNameMap)) {
|
||||
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
|
||||
|
||||
// Add import statement
|
||||
serviceImports.push(`import { ${capitalizedName}ServiceDefinition } from "@shared/proto/${dirName}"`)
|
||||
|
||||
// Add client creation
|
||||
serviceClientCreations.push(
|
||||
`const ${capitalizedName}ServiceClient = createGrpcClient(${capitalizedName}ServiceDefinition)`,
|
||||
)
|
||||
|
||||
// Add to exports
|
||||
serviceExports.push(`${capitalizedName}ServiceClient`)
|
||||
}
|
||||
|
||||
// Generate the file content
|
||||
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
|
||||
import { createGrpcClient } from "./grpc-client-base"
|
||||
${serviceImports.join("\n")}
|
||||
|
||||
${serviceClientCreations.join("\n")}
|
||||
|
||||
export {
|
||||
${serviceExports.join(",\n\t")}
|
||||
}`
|
||||
|
||||
const filePath = path.resolve("webview-ui/src/services/grpc-client.ts")
|
||||
await writeFileWithMkdirs(filePath, content)
|
||||
log_verbose(chalk.green(`Generated gRPC client at ${filePath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse proto files to extract streaming method information
|
||||
* @param protoFiles Array of proto file names
|
||||
* @param scriptDir Directory containing proto files
|
||||
* @returns Map of service names to their streaming methods
|
||||
*/
|
||||
async function parseProtoForStreamingMethods(protoFiles, scriptDir) {
|
||||
log_verbose(chalk.cyan("Parsing proto files for streaming methods..."))
|
||||
|
||||
// Map of service name to array of streaming method names
|
||||
const streamingMethodsMap = new Map()
|
||||
|
||||
for (const protoFile of protoFiles) {
|
||||
const content = await fs.readFile(path.join(scriptDir, protoFile), "utf8")
|
||||
|
||||
// Extract package name
|
||||
const packageMatch = content.match(/package\s+([^;]+);/)
|
||||
const packageName = packageMatch ? packageMatch[1].trim() : "unknown"
|
||||
|
||||
// Extract service definitions
|
||||
const serviceMatches = Array.from(content.matchAll(/service\s+(\w+)\s*\{([^}]+)\}/g))
|
||||
for (const serviceMatch of serviceMatches) {
|
||||
const serviceName = serviceMatch[1]
|
||||
const serviceBody = serviceMatch[2]
|
||||
const fullServiceName = `${packageName}.${serviceName}`
|
||||
|
||||
// Extract method definitions with streaming
|
||||
const methodMatches = Array.from(
|
||||
serviceBody.matchAll(/rpc\s+(\w+)\s*\(\s*(stream\s+)?(\w+)\s*\)\s*returns\s*\(\s*(stream\s+)?(\w+)\s*\)/g),
|
||||
)
|
||||
|
||||
const streamingMethods = []
|
||||
for (const methodMatch of methodMatches) {
|
||||
const methodName = methodMatch[1]
|
||||
const isRequestStreaming = !!methodMatch[2]
|
||||
const requestType = methodMatch[3]
|
||||
const isResponseStreaming = !!methodMatch[4]
|
||||
const responseType = methodMatch[5]
|
||||
|
||||
if (isResponseStreaming) {
|
||||
streamingMethods.push({
|
||||
name: methodName,
|
||||
requestType,
|
||||
responseType,
|
||||
isRequestStreaming,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (streamingMethods.length > 0) {
|
||||
streamingMethodsMap.set(fullServiceName, streamingMethods)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return streamingMethodsMap
|
||||
}
|
||||
|
||||
async function generateProtoBusMethodRegistrations() {
|
||||
log_verbose(chalk.cyan("Generating method registration files..."))
|
||||
|
||||
// Parse proto files for streaming methods
|
||||
const protoFiles = await globby("*.proto", { cwd: PROTO_DIR })
|
||||
const streamingMethodsMap = await parseProtoForStreamingMethods(protoFiles, PROTO_DIR)
|
||||
|
||||
for (const serviceDir of serviceDirs) {
|
||||
const serviceName = path.basename(serviceDir)
|
||||
const fullServiceName = serviceNameMap[serviceName]
|
||||
const streamingMethods = streamingMethodsMap.get(fullServiceName) || []
|
||||
|
||||
log_verbose(chalk.cyan(`Generating method registrations for ${serviceName}...`))
|
||||
|
||||
// Get all TypeScript files in the service directory
|
||||
const files = await globby("*.ts", { cwd: serviceDir })
|
||||
|
||||
// Filter out index.ts and methods.ts
|
||||
const implementationFiles = files.filter((file) => file !== "index.ts" && file !== "methods.ts")
|
||||
|
||||
// Create the methods.ts file with header
|
||||
let methodsContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
|
||||
// Import all method implementations
|
||||
import { registerMethod } from "./index"\n`
|
||||
|
||||
// Import implementations directly
|
||||
for (const file of implementationFiles) {
|
||||
const baseName = path.basename(file, ".ts")
|
||||
methodsContent += `import { ${baseName} } from "./${baseName}"\n`
|
||||
}
|
||||
|
||||
// Add streaming methods information
|
||||
if (streamingMethods.length > 0) {
|
||||
methodsContent += `\n// Streaming methods for this service
|
||||
export const streamingMethods = ${JSON.stringify(
|
||||
streamingMethods.map((m) => m.name),
|
||||
null,
|
||||
2,
|
||||
)}\n`
|
||||
}
|
||||
|
||||
// Add registration function
|
||||
methodsContent += `\n// Register all ${serviceName} service methods
|
||||
export function registerAllMethods(): void {
|
||||
\t// Register each method with the registry\n`
|
||||
|
||||
// Add registration statements
|
||||
for (const file of implementationFiles) {
|
||||
const baseName = path.basename(file, ".ts")
|
||||
const isStreaming = streamingMethods.some((m) => m.name === baseName)
|
||||
|
||||
if (isStreaming) {
|
||||
methodsContent += `\tregisterMethod("${baseName}", ${baseName}, { isStreaming: true })\n`
|
||||
} else {
|
||||
methodsContent += `\tregisterMethod("${baseName}", ${baseName})\n`
|
||||
}
|
||||
}
|
||||
|
||||
// Close the function
|
||||
methodsContent += `}`
|
||||
|
||||
// Write the methods.ts file
|
||||
const registryFile = path.join(serviceDir, "methods.ts")
|
||||
await writeFileWithMkdirs(registryFile, methodsContent)
|
||||
log_verbose(chalk.green(`Generated ${registryFile}`))
|
||||
|
||||
// Generate index.ts file
|
||||
const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
|
||||
const indexContent = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
|
||||
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../grpc-service"
|
||||
import { StreamingResponseHandler } from "../grpc-handler"
|
||||
import { registerAllMethods } from "./methods"
|
||||
|
||||
// Create ${serviceName} service registry
|
||||
const ${serviceName}Service = createServiceRegistry("${serviceName}")
|
||||
|
||||
// Export the method handler types and registration function
|
||||
export type ${capitalizedServiceName}MethodHandler = ServiceMethodHandler
|
||||
export type ${capitalizedServiceName}StreamingMethodHandler = StreamingMethodHandler
|
||||
export const registerMethod = ${serviceName}Service.registerMethod
|
||||
|
||||
// Export the request handlers
|
||||
export const handle${capitalizedServiceName}ServiceRequest = ${serviceName}Service.handleRequest
|
||||
export const handle${capitalizedServiceName}ServiceStreamingRequest = ${serviceName}Service.handleStreamingRequest
|
||||
export const isStreamingMethod = ${serviceName}Service.isStreamingMethod
|
||||
|
||||
// Register all ${serviceName} methods
|
||||
registerAllMethods()`
|
||||
|
||||
// Write the index.ts file
|
||||
const indexFile = path.join(serviceDir, "index.ts")
|
||||
await writeFileWithMkdirs(indexFile, indexContent)
|
||||
log_verbose(chalk.green(`Generated ${indexFile}`))
|
||||
}
|
||||
|
||||
log_verbose(chalk.green("Method registration files generated successfully."))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a service configuration file that maps service names to their handlers
|
||||
* This eliminates the need for manual switch/case statements in grpc-handler.ts
|
||||
*/
|
||||
async function generateProtoBusServiceConfig() {
|
||||
log_verbose(chalk.cyan("Generating service configuration file..."))
|
||||
|
||||
const serviceImports = []
|
||||
const serviceConfigs = []
|
||||
|
||||
// Add all services from the serviceNameMap
|
||||
for (const [dirName, fullServiceName] of Object.entries(serviceNameMap)) {
|
||||
const capitalizedName = dirName.charAt(0).toUpperCase() + dirName.slice(1)
|
||||
serviceImports.push(
|
||||
`import { handle${capitalizedName}ServiceRequest, handle${capitalizedName}ServiceStreamingRequest } from "./${dirName}/index"`,
|
||||
)
|
||||
serviceConfigs.push(`
|
||||
"${fullServiceName}": {
|
||||
requestHandler: handle${capitalizedName}ServiceRequest,
|
||||
streamingHandler: handle${capitalizedName}ServiceStreamingRequest
|
||||
}`)
|
||||
}
|
||||
|
||||
const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
|
||||
import { Controller } from "./index"
|
||||
import { StreamingResponseHandler } from "./grpc-handler"
|
||||
${serviceImports.join("\n")}
|
||||
|
||||
/**
|
||||
* Configuration for a service handler
|
||||
*/
|
||||
export interface ServiceHandlerConfig {
|
||||
requestHandler: (controller: Controller, method: string, message: any) => Promise<any>;
|
||||
streamingHandler: (controller: Controller, method: string, message: any, responseStream: StreamingResponseHandler, requestId?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of service names to their handler configurations
|
||||
*/
|
||||
export const serviceHandlers: Record<string, ServiceHandlerConfig> = {${serviceConfigs.join(",")}
|
||||
};`
|
||||
|
||||
const configPath = path.resolve("src/core/controller/grpc-service-config.ts")
|
||||
await writeFileWithMkdirs(configPath, content)
|
||||
log_verbose(chalk.green(`Generated service configuration at ${configPath}`))
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a .proto file exists for each service in the serviceNameMap
|
||||
* If a .proto file doesn't exist, create a template file
|
||||
*/
|
||||
async function ensureProtoFilesExist() {
|
||||
log_verbose(chalk.cyan("Checking for missing proto files..."))
|
||||
|
||||
// Get existing proto files
|
||||
const existingProtoFiles = await globby("*.proto", { cwd: PROTO_DIR })
|
||||
const existingProtoServices = existingProtoFiles.map((file) => path.basename(file, ".proto"))
|
||||
|
||||
// Check each service in serviceNameMap
|
||||
for (const [serviceName, fullServiceName] of Object.entries(serviceNameMap)) {
|
||||
if (!existingProtoServices.includes(serviceName)) {
|
||||
log_verbose(chalk.yellow(`Creating template proto file for ${serviceName}...`))
|
||||
|
||||
// Extract service class name from full name (e.g., "cline.ModelsService" -> "ModelsService")
|
||||
const serviceClassName = fullServiceName.split(".").pop()
|
||||
|
||||
// Create template proto file
|
||||
const protoContent = `syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
option java_package = "bot.cline.proto";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
// ${serviceClassName} provides methods for managing ${serviceName}
|
||||
service ${serviceClassName} {
|
||||
// Add your RPC methods here
|
||||
// Example (String is from common.proto, responses should be generic types):
|
||||
// rpc YourMethod(YourRequest) returns (String);
|
||||
}
|
||||
|
||||
// Add your message definitions here
|
||||
// Example (Requests must always start with Metadata):
|
||||
// message YourRequest {
|
||||
// Metadata metadata = 1;
|
||||
// string stringField = 2;
|
||||
// int32 int32Field = 3;
|
||||
// }
|
||||
`
|
||||
|
||||
// Write the template proto file
|
||||
const protoFilePath = path.join(PROTO_DIR, `${serviceName}.proto`)
|
||||
await fs.writeFile(protoFilePath, protoContent)
|
||||
log_verbose(chalk.green(`Created template proto file at ${protoFilePath}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
// Clean up existing generated files
|
||||
log_verbose(chalk.cyan("Cleaning up existing generated TypeScript files..."))
|
||||
@@ -113,7 +427,6 @@ async function cleanup() {
|
||||
await rmrf("src/standalone/services/host-grpc-client.ts")
|
||||
await rmrf("src/standalone/server-setup.ts")
|
||||
await rmrf("src/hosts/vscode/host-grpc-service-config.ts")
|
||||
await rmrf("src/core/controller/grpc-service-config.ts")
|
||||
const oldhostbridgefiles = [
|
||||
"src/hosts/vscode/workspace/methods.ts",
|
||||
"src/hosts/vscode/workspace/index.ts",
|
||||
@@ -128,35 +441,37 @@ async function cleanup() {
|
||||
"src/hosts/vscode/uri/methods.ts",
|
||||
"src/hosts/vscode/uri/index.ts",
|
||||
]
|
||||
const oldprotobusfiles = [
|
||||
"src/core/controller/account/index.ts",
|
||||
"src/core/controller/account/methods.ts",
|
||||
"src/core/controller/browser/index.ts",
|
||||
"src/core/controller/browser/methods.ts",
|
||||
"src/core/controller/checkpoints/index.ts",
|
||||
"src/core/controller/checkpoints/methods.ts",
|
||||
"src/core/controller/file/index.ts",
|
||||
"src/core/controller/file/methods.ts",
|
||||
"src/core/controller/mcp/index.ts",
|
||||
"src/core/controller/mcp/methods.ts",
|
||||
"src/core/controller/models/index.ts",
|
||||
"src/core/controller/models/methods.ts",
|
||||
"src/core/controller/slash/index.ts",
|
||||
"src/core/controller/slash/methods.ts",
|
||||
"src/core/controller/state/index.ts",
|
||||
"src/core/controller/state/methods.ts",
|
||||
"src/core/controller/task/index.ts",
|
||||
"src/core/controller/task/methods.ts",
|
||||
"src/core/controller/ui/index.ts",
|
||||
"src/core/controller/ui/methods.ts",
|
||||
"src/core/controller/web/index.ts",
|
||||
"src/core/controller/web/methods.ts",
|
||||
]
|
||||
for (const file of [...oldhostbridgefiles, ...oldprotobusfiles]) {
|
||||
for (const file of oldhostbridgefiles) {
|
||||
await rmrf(file)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write `contents` to `filePath`, creating any necessary directories in `filePath`.
|
||||
*/
|
||||
async function writeFileWithMkdirs(filePath, content) {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
await fs.writeFile(filePath, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an empty dir, do nothing if the directory doesn't exist or is not empty.
|
||||
*/
|
||||
async function rmdir(path) {
|
||||
try {
|
||||
await fs.rmdir(path)
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOTEMPTY" && error.code !== "ENOENT") {
|
||||
// Only re-throw if it's not "not empty" or "doesn't exist"
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function rmrf(path) {
|
||||
await fs.rm(path, { force: true, recursive: true })
|
||||
}
|
||||
|
||||
// Check for Apple Silicon compatibility
|
||||
function checkAppleSiliconCompatibility() {
|
||||
// Only run check on macOS
|
||||
@@ -187,51 +502,6 @@ function checkAppleSiliconCompatibility() {
|
||||
}
|
||||
}
|
||||
|
||||
const int64TypeNames = ["TYPE_INT64", "TYPE_UINT64", "TYPE_SINT64", "TYPE_FIXED64", "TYPE_SFIXED64"]
|
||||
|
||||
async function checkProtos() {
|
||||
const proto = await loadProtoDescriptorSet()
|
||||
const int64Fields = []
|
||||
|
||||
for (const [packageName, packageDef] of Object.entries(proto)) {
|
||||
for (const [messageName, def] of Object.entries(packageDef)) {
|
||||
// Skip service definitions
|
||||
if (def && typeof def === "object" && "service" in def) {
|
||||
continue
|
||||
}
|
||||
// Check message fields
|
||||
if (def && def.type && def.type.field) {
|
||||
for (const field of def.type.field) {
|
||||
if (int64TypeNames.includes(field.type)) {
|
||||
const name = `${packageName}.${messageName}.${field.name}`
|
||||
int64Fields.push({
|
||||
name: name,
|
||||
type: field.type,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (int64Fields.length > 0) {
|
||||
console.log(chalk.yellow(`\nWarning: Found ${int64Fields.length} fields using 64-bit integer types`))
|
||||
for (const field of int64Fields) {
|
||||
const typeNames = {
|
||||
TYPE_INT64: "int64",
|
||||
TYPE_UINT64: "uint64",
|
||||
TYPE_SINT64: "sint64",
|
||||
TYPE_FIXED64: "fixed64",
|
||||
TYPE_SFIXED64: "sfixed64",
|
||||
}
|
||||
log_verbose(chalk.yellow(` - ${field.name} (${typeNames[field.type]})`))
|
||||
}
|
||||
log_verbose(chalk.yellow("\nWARNING: 64-bit integer fields detected in proto definitions"))
|
||||
log_verbose(chalk.yellow("JavaScript cannot safely represent integers larger than 2^53-1 (Number.MAX_SAFE_INTEGER)."))
|
||||
log_verbose(chalk.yellow("Consider using string representation for large numbers or implementing BigInt support.\n"))
|
||||
}
|
||||
}
|
||||
|
||||
function log_verbose(s) {
|
||||
if (process.argv.includes("-v") || process.argv.includes("--verbose")) {
|
||||
console.log(s)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
/**
|
||||
* Write `contents` to `filePath`, creating any necessary directories in `filePath`.
|
||||
*/
|
||||
export async function writeFileWithMkdirs(filePath, content) {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
await fs.writeFile(filePath, content)
|
||||
}
|
||||
|
||||
export async function rmrf(path) {
|
||||
await fs.rm(path, { force: true, recursive: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an empty dir, do nothing if the directory doesn't exist or is not empty.
|
||||
*/
|
||||
export async function rmdir(path) {
|
||||
try {
|
||||
await fs.rmdir(path)
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOTEMPTY" && error.code !== "ENOENT") {
|
||||
// Only re-throw if it's not "not empty" or "doesn't exist"
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { writeFileWithMkdirs } from "./file-utils.mjs"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import chalk from "chalk"
|
||||
import { loadServicesFromProtoDescriptor, getFqn } from "./proto-utils.mjs"
|
||||
|
||||
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
|
||||
// Contains the interface definitions for the host bridge clients.
|
||||
const TYPES_FILE = path.resolve("src/generated/hosts/host-bridge-client-types.ts")
|
||||
@@ -12,17 +15,49 @@ const EXTERNAL_CLIENT_FILE = path.resolve("src/generated/hosts/standalone/host-b
|
||||
// Contains the handler map for the external host bridge clients (using the custom service registry).
|
||||
const VSCODE_CLIENT_FILE = path.resolve("src/generated/hosts/vscode/hostbridge-grpc-service-config.ts")
|
||||
|
||||
const typeNameToFQN = new Map()
|
||||
|
||||
function addTypeNameToFqn(name, fqn) {
|
||||
if (typeNameToFQN.has(name)) {
|
||||
throw new Error(`Proto type ${name} redefined (${fqn}).`)
|
||||
}
|
||||
typeNameToFQN.set(name, fqn)
|
||||
}
|
||||
function getFqn(name) {
|
||||
if (!typeNameToFQN.has(name)) {
|
||||
throw Error(`No FQN for ${name}`)
|
||||
}
|
||||
return typeNameToFQN.get(name)
|
||||
}
|
||||
/**
|
||||
* Main function to generate the host bridge client
|
||||
*/
|
||||
export async function main() {
|
||||
const { hostServices } = await loadServicesFromProtoDescriptor()
|
||||
async function main() {
|
||||
// Load service definitions from descriptor set
|
||||
const descriptorBuffer = await fs.readFile(DESCRIPTOR_SET)
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer)
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition)
|
||||
|
||||
// Extract host services and proto messages from the proto definition
|
||||
const hostServices = {}
|
||||
for (const [name, def] of Object.entries(proto.host)) {
|
||||
if (def && "service" in def) {
|
||||
hostServices[name] = def
|
||||
} else {
|
||||
addTypeNameToFqn(name, `proto.host.${name}`)
|
||||
}
|
||||
}
|
||||
for (const [name, def] of Object.entries(proto.cline)) {
|
||||
if (def && !("service" in def)) {
|
||||
addTypeNameToFqn(name, `proto.cline.${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
await generateTypesFile(hostServices)
|
||||
await generateExternalClientFile(hostServices)
|
||||
await generateVscodeClientFile(hostServices)
|
||||
|
||||
console.log(`Generated Host Bridge client files at:`)
|
||||
console.log(`Generated host bridge client files at:`)
|
||||
console.log(`- ${TYPES_FILE}`)
|
||||
console.log(`- ${EXTERNAL_CLIENT_FILE}`)
|
||||
console.log(`- ${VSCODE_CLIENT_FILE}`)
|
||||
@@ -45,7 +80,8 @@ import { StreamingCallbacks } from "@hosts/host-provider-types"
|
||||
${clientInterfaces.join("\n\n")}
|
||||
`
|
||||
// Write output file
|
||||
await writeFileWithMkdirs(TYPES_FILE, content)
|
||||
await fs.mkdir(path.dirname(TYPES_FILE), { recursive: true })
|
||||
await fs.writeFile(TYPES_FILE, content)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,14 +135,14 @@ import * as niceGrpc from "@generated/nice-grpc/index"
|
||||
import { StreamingCallbacks } from "@hosts/host-provider-types"
|
||||
import * as proto from "@shared/proto/index"
|
||||
import { Channel, createClient } from "nice-grpc"
|
||||
import { BaseGrpcClient } from "@/hosts/external/grpc-types"
|
||||
|
||||
${imports.join("\n")}
|
||||
|
||||
${clientImplementations.join("\n\n")}
|
||||
`
|
||||
// Write output file
|
||||
await writeFileWithMkdirs(EXTERNAL_CLIENT_FILE, content)
|
||||
await fs.mkdir(path.dirname(EXTERNAL_CLIENT_FILE), { recursive: true })
|
||||
await fs.writeFile(EXTERNAL_CLIENT_FILE, content)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,49 +158,31 @@ function generateExternalClientSetup(serviceName, serviceDefinition) {
|
||||
const isStreamingResponse = methodDef.responseStream
|
||||
|
||||
if (!isStreamingResponse) {
|
||||
return ` ${methodName}(request: ${requestType}): Promise<${responseType}> {
|
||||
return this.makeRequest((client) => client.${methodName}(request))
|
||||
}`
|
||||
return ` ${methodName}(request: ${requestType}): Promise<${responseType}> {
|
||||
return this.client.${methodName}(request)
|
||||
}`
|
||||
} else {
|
||||
// Generate streaming method
|
||||
return ` ${methodName}(
|
||||
request: ${requestType},
|
||||
callbacks: StreamingCallbacks<${responseType}>,
|
||||
): () => void {
|
||||
const client = this.getClient()
|
||||
const abortController = new AbortController()
|
||||
const stream: AsyncIterable<${responseType}> = client.${methodName}(request, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
const wrappedCallbacks: StreamingCallbacks<${responseType}> = {
|
||||
...callbacks,
|
||||
onError: (error: any) => {
|
||||
if (error?.code === "UNAVAILABLE") {
|
||||
this.destroyClient()
|
||||
}
|
||||
callbacks.onError?.(error)
|
||||
},
|
||||
}
|
||||
asyncIteratorToCallbacks(stream, wrappedCallbacks)
|
||||
return () => {
|
||||
abortController.abort()
|
||||
}
|
||||
}\n`
|
||||
return ` ${methodName}(request: ${requestType}, callbacks: StreamingCallbacks<${responseType}>): () => void {
|
||||
const abortController = new AbortController()
|
||||
const stream: AsyncIterable<${responseType}> = this.client.${methodName}(request, {signal: abortController.signal})
|
||||
asyncIteratorToCallbacks(stream, callbacks)
|
||||
return () => {abortController.abort()}
|
||||
}`
|
||||
}
|
||||
})
|
||||
.join("\n")
|
||||
.join("\n\n")
|
||||
|
||||
// Generate the class
|
||||
return `/**
|
||||
* Type-safe client implementation for ${serviceName}.
|
||||
*/
|
||||
export class ${serviceName}ClientImpl
|
||||
extends BaseGrpcClient<niceGrpc.host.${serviceName}Client>
|
||||
implements ${serviceName}ClientInterface {
|
||||
export class ${serviceName}ClientImpl implements ${serviceName}ClientInterface {
|
||||
private client: niceGrpc.host.${serviceName}Client
|
||||
|
||||
protected createClient(channel: Channel): niceGrpc.host.${serviceName}Client {
|
||||
return createClient(niceGrpc.host.${serviceName}Definition, channel)
|
||||
}
|
||||
constructor(channel: Channel) {
|
||||
this.client = createClient(niceGrpc.host.${serviceName}Definition, channel)
|
||||
}
|
||||
|
||||
${methods}
|
||||
}`
|
||||
@@ -209,7 +227,8 @@ ${handlerMap.join("\n")}
|
||||
`
|
||||
|
||||
// Write output file
|
||||
await writeFileWithMkdirs(VSCODE_CLIENT_FILE, content)
|
||||
await fs.mkdir(path.dirname(VSCODE_CLIENT_FILE), { recursive: true })
|
||||
await fs.writeFile(VSCODE_CLIENT_FILE, content)
|
||||
}
|
||||
|
||||
function generateVscodeClientImplementation(serviceName, serviceDefinition) {
|
||||
@@ -234,10 +253,8 @@ const ${name}ServiceRegistry = createServiceRegistry("${name}")
|
||||
${methods}`
|
||||
}
|
||||
|
||||
// Only run main if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
// Run the main function
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { writeFileWithMkdirs } from "./file-utils.mjs"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { loadServicesFromProtoDescriptor, getFqn } from "./proto-utils.mjs"
|
||||
|
||||
const WEBVIEW_CLIENTS_FILE = path.resolve("webview-ui/src/services/grpc-client.ts")
|
||||
const VSCODE_SERVICES_FILE = path.resolve("src/generated/hosts/vscode/protobus-services.ts")
|
||||
const VSCODE_SERVICE_TYPES_FILE = path.resolve("src/generated/hosts/vscode/protobus-service-types.ts")
|
||||
const STANDALONE_SERVER_SETUP_FILE = path.resolve("src/generated/hosts/standalone/protobus-server-setup.ts")
|
||||
|
||||
const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url))
|
||||
|
||||
export async function main() {
|
||||
const { protobusServices } = await loadServicesFromProtoDescriptor()
|
||||
await generateWebviewProtobusClients(protobusServices)
|
||||
await generateVscodeServiceTypes(protobusServices)
|
||||
await generateVscodeProtobusServers(protobusServices)
|
||||
await generateStandaloneProtobusServiceSetup(protobusServices)
|
||||
|
||||
console.log(`Generated ProtoBus files at:`)
|
||||
console.log(`- ${WEBVIEW_CLIENTS_FILE}`)
|
||||
console.log(`- ${VSCODE_SERVICE_TYPES_FILE}`)
|
||||
console.log(`- ${VSCODE_SERVICES_FILE}`)
|
||||
console.log(`- ${STANDALONE_SERVER_SETUP_FILE}`)
|
||||
}
|
||||
|
||||
async function generateWebviewProtobusClients(protobusServices) {
|
||||
const clients = []
|
||||
|
||||
for (const [serviceName, def] of Object.entries(protobusServices)) {
|
||||
const rpcs = []
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
const requestType = getFqn(rpc.requestType.type.name)
|
||||
const responseType = getFqn(rpc.responseType.type.name)
|
||||
|
||||
if (rpc.requestStream) {
|
||||
throw new Error("Request streaming is not supported")
|
||||
}
|
||||
if (!rpc.responseStream) {
|
||||
rpcs.push(` static async ${rpcName}(request: ${requestType}): Promise<${responseType}> {
|
||||
return this.makeRequest("${rpcName}", request)
|
||||
}`)
|
||||
} else {
|
||||
rpcs.push(` static ${rpcName}(request: ${requestType}, callbacks: Callbacks<${responseType}>): ()=>void {
|
||||
return this.makeStreamingRequest("${rpcName}", request, callbacks)
|
||||
}`)
|
||||
}
|
||||
}
|
||||
clients.push(`export class ${serviceName}Client extends ProtoBusClient {
|
||||
static override serviceName: string = "cline.${serviceName}"
|
||||
${rpcs.join("\n")}
|
||||
}`)
|
||||
}
|
||||
|
||||
// Create output file
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
import * as proto from "@shared/proto/index"
|
||||
import { ProtoBusClient, Callbacks } from "./grpc-client-base"
|
||||
|
||||
${clients.join("\n")}
|
||||
`
|
||||
// Write output file
|
||||
await writeFileWithMkdirs(WEBVIEW_CLIENTS_FILE, output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate imports and function to add all the handlers to the server for all services defined in the proto files.
|
||||
*/
|
||||
async function generateVscodeServiceTypes(protobusServices) {
|
||||
const servers = []
|
||||
|
||||
for (const [serviceName, def] of Object.entries(protobusServices)) {
|
||||
const domain = getDomainName(serviceName)
|
||||
servers.push(`// ${domain} Service Handler Types`)
|
||||
servers.push(`export type ${serviceName}Handlers = {`)
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
const requestType = getFqn(rpc.requestType.type.name)
|
||||
const responseType = getFqn(rpc.responseType.type.name)
|
||||
if (rpc.requestStream) {
|
||||
throw new Error("Request streaming is not supported")
|
||||
}
|
||||
if (!rpc.responseStream) {
|
||||
servers.push(` ${rpcName}:(controller: Controller, request: ${requestType}) => Promise<${responseType}>`)
|
||||
} else {
|
||||
servers.push(
|
||||
` ${rpcName}:(controller: Controller, request: ${requestType}, responseStream: StreamingResponseHandler<${responseType}>, requestId?: string) => Promise<void>`,
|
||||
)
|
||||
}
|
||||
}
|
||||
servers.push(`}\n`)
|
||||
}
|
||||
|
||||
// Create output file
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
import * as proto from "@shared/proto/index"
|
||||
import { Controller } from "@core/controller"
|
||||
import { StreamingResponseHandler } from "@/core/controller/grpc-handler"
|
||||
|
||||
${servers.join("\n")}
|
||||
`
|
||||
// Write output file
|
||||
await writeFileWithMkdirs(VSCODE_SERVICE_TYPES_FILE, output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate imports and function to add all the handlers to the server for all services defined in the proto files.
|
||||
*/
|
||||
async function generateVscodeProtobusServers(protobusServices) {
|
||||
const imports = []
|
||||
const servers = []
|
||||
const serviceMap = []
|
||||
for (const [serviceName, def] of Object.entries(protobusServices)) {
|
||||
const domain = getDomainName(serviceName)
|
||||
const dir = getDirName(serviceName)
|
||||
imports.push(`// ${domain} Service`)
|
||||
servers.push(`const ${serviceName}Handlers: serviceTypes.${serviceName}Handlers = {`)
|
||||
for (const [rpcName, _rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`)
|
||||
servers.push(` ${rpcName}: ${rpcName},`)
|
||||
}
|
||||
servers.push(`} \n`)
|
||||
serviceMap.push(` "cline.${serviceName}": ${serviceName}Handlers,`)
|
||||
imports.push("")
|
||||
}
|
||||
|
||||
// Create output file
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
import * as serviceTypes from "src/generated/hosts/vscode/protobus-service-types"
|
||||
|
||||
${imports.join("\n")}
|
||||
${servers.join("\n")}
|
||||
export const serviceHandlers: Record<string, any> = {
|
||||
${serviceMap.join("\n")}
|
||||
}
|
||||
`
|
||||
// Write output file
|
||||
await writeFileWithMkdirs(VSCODE_SERVICES_FILE, output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate imports and function to add all the handlers to the server for all services defined in the proto files.
|
||||
*/
|
||||
async function generateStandaloneProtobusServiceSetup(protobusServices) {
|
||||
const imports = []
|
||||
const handlerSetup = []
|
||||
|
||||
for (const [name, def] of Object.entries(protobusServices)) {
|
||||
const domain = getDomainName(name)
|
||||
const dir = getDirName(name)
|
||||
imports.push(`// ${domain} Service`)
|
||||
handlerSetup.push(` // ${domain} Service`)
|
||||
handlerSetup.push(` server.addService(cline.${name}Service, {`)
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`)
|
||||
const requestType = "cline." + rpc.requestType.type.name
|
||||
const responseType = "cline." + rpc.responseType.type.name
|
||||
if (rpc.requestStream) {
|
||||
throw new Error("Request streaming is not supported")
|
||||
}
|
||||
if (rpc.responseStream) {
|
||||
handlerSetup.push(
|
||||
` ${rpcName}: wrapStreamingResponse<${requestType},${responseType}>(${rpcName}, controller),`,
|
||||
)
|
||||
} else {
|
||||
handlerSetup.push(` ${rpcName}: wrapper<${requestType},${responseType}>(${rpcName}, controller),`)
|
||||
}
|
||||
}
|
||||
handlerSetup.push(` });`)
|
||||
imports.push("")
|
||||
handlerSetup.push("")
|
||||
}
|
||||
|
||||
// Create output file
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${SCRIPT_NAME}
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import { cline } from "@generated/grpc-js"
|
||||
import { Controller } from "@core/controller"
|
||||
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@hosts/external/grpc-types"
|
||||
|
||||
${imports.join("\n")}
|
||||
export function addProtobusServices(
|
||||
server: grpc.Server,
|
||||
controller: Controller,
|
||||
wrapper: GrpcHandlerWrapper,
|
||||
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
|
||||
): void {
|
||||
${handlerSetup.join("\n")}
|
||||
}
|
||||
`
|
||||
// Write output file
|
||||
await writeFileWithMkdirs(STANDALONE_SERVER_SETUP_FILE, output)
|
||||
}
|
||||
|
||||
function getDomainName(serviceName) {
|
||||
return serviceName.replace(/Service$/, "")
|
||||
}
|
||||
function getDirName(serviceName) {
|
||||
const domain = getDomainName(serviceName)
|
||||
return domain.charAt(0).toLowerCase() + domain.slice(1)
|
||||
}
|
||||
|
||||
// Only run main if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch((error) => {
|
||||
console.error(chalk.red("Error:"), error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
import * as fs from "fs"
|
||||
import * as health from "grpc-health-check"
|
||||
import path, { basename, dirname } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const OUT_FILE = path.resolve("src/generated/standalone/server-setup.ts")
|
||||
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
|
||||
// Load service definitions.
|
||||
const clineDef = protoLoader.loadFileDescriptorSetFromBuffer(fs.readFileSync(DESCRIPTOR_SET))
|
||||
const healthDef = protoLoader.loadSync(health.protoPath)
|
||||
const packageDefinition = { ...clineDef, ...healthDef }
|
||||
const proto = grpc.loadPackageDefinition(packageDefinition)
|
||||
|
||||
/**
|
||||
* Generate imports and function to add all the handlers to the server for all services defined in the proto files.
|
||||
*/
|
||||
function generateHandlersAndExports() {
|
||||
let imports = []
|
||||
let handlerSetup = []
|
||||
|
||||
for (const [name, def] of Object.entries(proto.cline)) {
|
||||
if (!def || !("service" in def)) {
|
||||
continue
|
||||
}
|
||||
const domain = name.replace(/Service$/, "")
|
||||
const dir = domain.charAt(0).toLowerCase() + domain.slice(1)
|
||||
imports.push(`// ${domain} Service`)
|
||||
handlerSetup.push(` // ${domain} Service`)
|
||||
handlerSetup.push(` server.addService(cline.${name}Service, {`)
|
||||
for (const [rpcName, rpc] of Object.entries(def.service)) {
|
||||
imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`)
|
||||
const requestType = "cline." + rpc.requestType.type.name
|
||||
if (rpc.requestStream) {
|
||||
throw new Error("Request streaming is not supported")
|
||||
}
|
||||
if (rpc.responseStream) {
|
||||
handlerSetup.push(` ${rpcName}: wrapStreamingResponse<${requestType},void>(${rpcName}, controller),`)
|
||||
} else {
|
||||
const responseType = "cline." + rpc.responseType.type.name
|
||||
handlerSetup.push(` ${rpcName}: wrapper<${requestType},${responseType}>(${rpcName}, controller),`)
|
||||
}
|
||||
}
|
||||
handlerSetup.push(` });`)
|
||||
imports.push("")
|
||||
handlerSetup.push("")
|
||||
}
|
||||
return {
|
||||
imports: imports.join("\n"),
|
||||
handlerSetup: handlerSetup.join("\n"),
|
||||
}
|
||||
}
|
||||
|
||||
const { imports, handlerSetup } = generateHandlersAndExports()
|
||||
const scriptName = path.basename(fileURLToPath(import.meta.url))
|
||||
|
||||
// Create output file
|
||||
let output = `// GENERATED CODE -- DO NOT EDIT!
|
||||
// Generated by ${scriptName}
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import { cline } from "@generated/grpc-js"
|
||||
import { Controller } from "@core/controller"
|
||||
import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@hosts/external/grpc-types"
|
||||
|
||||
${imports}
|
||||
export function addProtobusServices(
|
||||
server: grpc.Server,
|
||||
controller: Controller,
|
||||
wrapper: GrpcHandlerWrapper,
|
||||
wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper,
|
||||
): void {
|
||||
${handlerSetup}
|
||||
}
|
||||
`
|
||||
// Write output file
|
||||
fs.mkdirSync(dirname(OUT_FILE), { recursive: true })
|
||||
fs.writeFileSync(OUT_FILE, output)
|
||||
|
||||
console.log(`Generated service handlers in ${OUT_FILE}.`)
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as grpc from "@grpc/grpc-js"
|
||||
import * as protoLoader from "@grpc/proto-loader"
|
||||
|
||||
const DESCRIPTOR_SET = path.resolve("dist-standalone/proto/descriptor_set.pb")
|
||||
|
||||
const typeNameToFQN = new Map()
|
||||
|
||||
function addTypeNameToFqn(name, fqn) {
|
||||
if (typeNameToFQN.has(name) && typeNameToFQN.get(name) !== fqn) {
|
||||
throw new Error(`Proto type ${name} redefined (${fqn}).`)
|
||||
}
|
||||
typeNameToFQN.set(name, fqn)
|
||||
}
|
||||
// Get the fully qualified name for a proto type, e.g. getFqn('StringRequest') returns 'cline.StringRequest'
|
||||
export function getFqn(name) {
|
||||
if (!typeNameToFQN.has(name)) {
|
||||
throw Error(`No FQN for ${name}`)
|
||||
}
|
||||
return typeNameToFQN.get(name)
|
||||
}
|
||||
|
||||
export async function loadProtoDescriptorSet() {
|
||||
const descriptorBuffer = await fs.readFile(DESCRIPTOR_SET)
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(descriptorBuffer)
|
||||
return grpc.loadPackageDefinition(packageDefinition)
|
||||
}
|
||||
|
||||
export async function loadServicesFromProtoDescriptor() {
|
||||
// Load service definitions from descriptor set
|
||||
const proto = await loadProtoDescriptorSet()
|
||||
|
||||
// Extract host services and proto messages from the proto definition
|
||||
const hostServices = {}
|
||||
for (const [name, def] of Object.entries(proto.host)) {
|
||||
if (def && "service" in def) {
|
||||
hostServices[name] = def
|
||||
} else {
|
||||
addTypeNameToFqn(name, `proto.host.${name}`)
|
||||
}
|
||||
}
|
||||
const protobusServices = {}
|
||||
for (const [name, def] of Object.entries(proto.cline)) {
|
||||
if (def && "service" in def) {
|
||||
protobusServices[name] = def
|
||||
} else {
|
||||
addTypeNameToFqn(name, `proto.cline.${name}`)
|
||||
}
|
||||
}
|
||||
return { protobusServices, hostServices }
|
||||
}
|
||||
+22
-32
@@ -1,5 +1,5 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "../shared/api"
|
||||
import { ApiConfiguration, ModelInfo } from "../shared/api"
|
||||
import { AnthropicHandler } from "./providers/anthropic"
|
||||
import { AwsBedrockHandler } from "./providers/bedrock"
|
||||
import { OpenRouterHandler } from "./providers/openrouter"
|
||||
@@ -29,9 +29,8 @@ import { SapAiCoreHandler } from "./providers/sapaicore"
|
||||
import { ClaudeCodeHandler } from "./providers/claude-code"
|
||||
import { MoonshotHandler } from "./providers/moonshot"
|
||||
import { GroqHandler } from "./providers/groq"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { Mode } from "../shared/ChatSettings"
|
||||
import { HuggingFaceHandler } from "./providers/huggingface"
|
||||
import { HuaweiCloudMaaSHandler } from "./providers/huawei-cloud-maas"
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
@@ -53,7 +52,7 @@ function createHandlerForProvider(
|
||||
return new AnthropicHandler({
|
||||
apiKey: options.apiKey,
|
||||
anthropicBaseUrl: options.anthropicBaseUrl,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
anthropicModelId: mode === "plan" ? options.planModeAnthropicModelId : options.actModeAnthropicModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
@@ -69,7 +68,7 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "bedrock":
|
||||
return new AwsBedrockHandler({
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
awsBedrockModelId: mode === "plan" ? options.planModeAwsBedrockModelId : options.actModeAwsBedrockModelId,
|
||||
awsAccessKey: options.awsAccessKey,
|
||||
awsSecretKey: options.awsSecretKey,
|
||||
awsSessionToken: options.awsSessionToken,
|
||||
@@ -92,7 +91,7 @@ function createHandlerForProvider(
|
||||
return new VertexHandler({
|
||||
vertexProjectId: options.vertexProjectId,
|
||||
vertexRegion: options.vertexRegion,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
vertexModelId: mode === "plan" ? options.planModeVertexModelId : options.actModeVertexModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
geminiApiKey: options.geminiApiKey,
|
||||
@@ -129,19 +128,19 @@ function createHandlerForProvider(
|
||||
geminiBaseUrl: options.geminiBaseUrl,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
geminiModelId: mode === "plan" ? options.planModeGeminiModelId : options.actModeGeminiModelId,
|
||||
taskId: options.taskId,
|
||||
})
|
||||
case "openai-native":
|
||||
return new OpenAiNativeHandler({
|
||||
openAiNativeApiKey: options.openAiNativeApiKey,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
openaiNativeModelId: mode === "plan" ? options.planModeOpenAiNativeModelId : options.actModeOpenAiNativeModelId,
|
||||
})
|
||||
case "deepseek":
|
||||
return new DeepSeekHandler({
|
||||
deepSeekApiKey: options.deepSeekApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
deepSeekModelId: mode === "plan" ? options.planModeDeepSeekModelId : options.actModeDeepSeekModelId,
|
||||
})
|
||||
case "requesty":
|
||||
return new RequestyHandler({
|
||||
@@ -167,21 +166,20 @@ function createHandlerForProvider(
|
||||
case "qwen":
|
||||
return new QwenHandler({
|
||||
qwenApiKey: options.qwenApiKey,
|
||||
qwenApiLine:
|
||||
options.qwenApiLine === QwenApiRegions.INTERNATIONAL ? QwenApiRegions.INTERNATIONAL : QwenApiRegions.CHINA,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
qwenApiLine: options.qwenApiLine,
|
||||
qwenModelId: mode === "plan" ? options.planModeQwenModelId : options.actModeQwenModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "doubao":
|
||||
return new DoubaoHandler({
|
||||
doubaoApiKey: options.doubaoApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
doubaoModelId: mode === "plan" ? options.planModeDoubaoModelId : options.actModeDoubaoModelId,
|
||||
})
|
||||
case "mistral":
|
||||
return new MistralHandler({
|
||||
mistralApiKey: options.mistralApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
mistralModelId: mode === "plan" ? options.planModeMistralModelId : options.actModeMistralModelId,
|
||||
})
|
||||
case "vscode-lm":
|
||||
return new VsCodeLmHandler({
|
||||
@@ -211,10 +209,11 @@ function createHandlerForProvider(
|
||||
taskId: options.taskId,
|
||||
})
|
||||
case "moonshot":
|
||||
console.log("moonshot", mode === "plan" ? options.planModeMoonshotModelId : options.actModeMoonshotModelId)
|
||||
return new MoonshotHandler({
|
||||
moonshotApiKey: options.moonshotApiKey,
|
||||
moonshotApiLine: options.moonshotApiLine,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
moonshotModelId: mode === "plan" ? options.planModeMoonshotModelId : options.actModeMoonshotModelId,
|
||||
})
|
||||
case "huggingface":
|
||||
return new HuggingFaceHandler({
|
||||
@@ -226,36 +225,35 @@ function createHandlerForProvider(
|
||||
case "nebius":
|
||||
return new NebiusHandler({
|
||||
nebiusApiKey: options.nebiusApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
nebiusModelId: mode === "plan" ? options.planModeNebiusModelId : options.actModeNebiusModelId,
|
||||
})
|
||||
case "asksage":
|
||||
return new AskSageHandler({
|
||||
asksageApiKey: options.asksageApiKey,
|
||||
asksageApiUrl: options.asksageApiUrl,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
asksageModelId: mode === "plan" ? options.planModeAskSageModelId : options.actModeAskSageModelId,
|
||||
})
|
||||
case "xai":
|
||||
return new XAIHandler({
|
||||
xaiApiKey: options.xaiApiKey,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
xaiModelId: mode === "plan" ? options.planModeXaiModelId : options.actModeXaiModelId,
|
||||
})
|
||||
case "sambanova":
|
||||
return new SambanovaHandler({
|
||||
sambanovaApiKey: options.sambanovaApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
sambanovaModelId: mode === "plan" ? options.planModeSambanovaModelId : options.actModeSambanovaModelId,
|
||||
})
|
||||
case "cerebras":
|
||||
return new CerebrasHandler({
|
||||
cerebrasApiKey: options.cerebrasApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
cerebrasModelId: mode === "plan" ? options.planModeCerebrasModelId : options.actModeCerebrasModelId,
|
||||
})
|
||||
case "groq":
|
||||
return new GroqHandler({
|
||||
groqApiKey: options.groqApiKey,
|
||||
groqModelId: mode === "plan" ? options.planModeGroqModelId : options.actModeGroqModelId,
|
||||
groqModelInfo: mode === "plan" ? options.planModeGroqModelInfo : options.actModeGroqModelInfo,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "sapaicore":
|
||||
return new SapAiCoreHandler({
|
||||
@@ -264,28 +262,20 @@ function createHandlerForProvider(
|
||||
sapAiCoreTokenUrl: options.sapAiCoreTokenUrl,
|
||||
sapAiResourceGroup: options.sapAiResourceGroup,
|
||||
sapAiCoreBaseUrl: options.sapAiCoreBaseUrl,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
sapAiCoreModelId: mode === "plan" ? options.planModeSapAiCoreModelId : options.actModeSapAiCoreModelId,
|
||||
})
|
||||
case "claude-code":
|
||||
return new ClaudeCodeHandler({
|
||||
claudeCodePath: options.claudeCodePath,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
claudeCodeModelId: mode === "plan" ? options.planModeClaudeCodeModelId : options.actModeClaudeCodeModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "huawei-cloud-maas":
|
||||
return new HuaweiCloudMaaSHandler({
|
||||
huaweiCloudMaasApiKey: options.huaweiCloudMaasApiKey,
|
||||
huaweiCloudMaasModelId:
|
||||
mode === "plan" ? options.planModeHuaweiCloudMaasModelId : options.actModeHuaweiCloudMaasModelId,
|
||||
huaweiCloudMaasModelInfo:
|
||||
mode === "plan" ? options.planModeHuaweiCloudMaasModelInfo : options.actModeHuaweiCloudMaasModelInfo,
|
||||
})
|
||||
default:
|
||||
return new AnthropicHandler({
|
||||
apiKey: options.apiKey,
|
||||
anthropicBaseUrl: options.anthropicBaseUrl,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
anthropicModelId: mode === "plan" ? options.planModeAnthropicModelId : options.actModeAnthropicModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
|
||||
@@ -203,7 +203,7 @@ describe("AwsBedrockHandler", () => {
|
||||
})
|
||||
|
||||
const mockOptions: ApiHandlerOptions = {
|
||||
actModeApiModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
actModeAwsBedrockModelId: "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
awsRegion: "us-east-1",
|
||||
awsAccessKey: "test-key",
|
||||
awsSecretKey: "test-secret",
|
||||
@@ -617,7 +617,7 @@ describe("AwsBedrockHandler", () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId:
|
||||
actModeAwsBedrockModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
@@ -632,7 +632,7 @@ describe("AwsBedrockHandler", () => {
|
||||
const customOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "my-namespace/my-custom-model",
|
||||
actModeAwsBedrockModelId: "my-namespace/my-custom-model",
|
||||
}
|
||||
const customHandler = new AwsBedrockHandler(customOptions)
|
||||
|
||||
@@ -681,7 +681,7 @@ describe("AwsBedrockHandler", () => {
|
||||
const customCrossRegionOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
actModeAwsBedrockModelId: "arn:aws:bedrock:us-west-2:123456789012:custom-model/my-model",
|
||||
awsUseCrossRegionInference: true,
|
||||
}
|
||||
const customCrossRegionHandler = new AwsBedrockHandler(customCrossRegionOptions)
|
||||
@@ -694,7 +694,7 @@ describe("AwsBedrockHandler", () => {
|
||||
const ultraThinkOptions: ApiHandlerOptions = {
|
||||
...mockOptions,
|
||||
actModeAwsBedrockCustomSelected: true,
|
||||
actModeApiModelId:
|
||||
actModeAwsBedrockModelId:
|
||||
"arn:aws:bedrock:us-west-2:123456789012:custom-model/anthropic.claude-3-5-sonnet-20241022-v2:0/Qk8MMyLmRd",
|
||||
actModeAwsBedrockCustomModelBaseId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ApiStream } from "../transform/stream"
|
||||
interface AnthropicHandlerOptions {
|
||||
apiKey?: string
|
||||
anthropicBaseUrl?: string
|
||||
apiModelId?: string
|
||||
anthropicModelId?: string
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: AnthropicModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.anthropicModelId
|
||||
if (modelId && modelId in anthropicModels) {
|
||||
const id = modelId as AnthropicModelId
|
||||
return { id, info: anthropicModels[id] }
|
||||
|
||||
@@ -7,7 +7,7 @@ import { withRetry } from "../retry"
|
||||
interface AskSageHandlerOptions {
|
||||
asksageApiKey?: string
|
||||
asksageApiUrl?: string
|
||||
apiModelId?: string
|
||||
asksageModelId?: string
|
||||
}
|
||||
|
||||
type AskSageRequest = {
|
||||
@@ -103,7 +103,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.asksageModelId
|
||||
if (modelId && modelId in askSageModels) {
|
||||
const id = modelId as AskSageModelId
|
||||
return { id, info: askSageModels[id] }
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import type { Message, ContentBlock } from "@aws-sdk/client-bedrock-runtime"
|
||||
|
||||
interface AwsBedrockHandlerOptions {
|
||||
apiModelId?: string
|
||||
awsBedrockModelId?: string
|
||||
awsAccessKey?: string
|
||||
awsSecretKey?: string
|
||||
awsSessionToken?: string
|
||||
@@ -143,7 +143,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.awsBedrockModelId
|
||||
if (modelId && modelId in bedrockModels) {
|
||||
const id = modelId as BedrockModelId
|
||||
return { id, info: bedrockModels[id] }
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ApiStream } from "@api/transform/stream"
|
||||
|
||||
interface CerebrasHandlerOptions {
|
||||
cerebrasApiKey?: string
|
||||
apiModelId?: string
|
||||
cerebrasModelId?: string
|
||||
}
|
||||
|
||||
export class CerebrasHandler implements ApiHandler {
|
||||
@@ -56,7 +56,7 @@ export class CerebrasHandler implements ApiHandler {
|
||||
|
||||
// Check if this is a reasoning model that uses thinking tags
|
||||
const modelId = this.getModel().id
|
||||
const isReasoningModel = modelId.includes("qwen")
|
||||
const isReasoningModel = modelId.includes("qwen") || modelId.includes("deepseek-r1-distill")
|
||||
|
||||
// Convert Anthropic messages to Cerebras format
|
||||
for (const message of messages) {
|
||||
@@ -175,7 +175,7 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.cerebrasModelId
|
||||
if (modelId && modelId in cerebrasModels) {
|
||||
const id = modelId as CerebrasModelId
|
||||
return { id, info: cerebrasModels[id] }
|
||||
|
||||
@@ -8,7 +8,7 @@ import { filterMessagesForClaudeCode } from "@/integrations/claude-code/message-
|
||||
|
||||
interface ClaudeCodeHandlerOptions {
|
||||
claudeCodePath?: string
|
||||
apiModelId?: string
|
||||
claudeCodeModelId?: string
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export class ClaudeCodeHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel() {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.claudeCodeModelId
|
||||
if (modelId && modelId in claudeCodeModels) {
|
||||
const id = modelId as ClaudeCodeModelId
|
||||
return { id, info: claudeCodeModels[id] }
|
||||
|
||||
@@ -10,7 +10,7 @@ import { convertToR1Format } from "../transform/r1-format"
|
||||
|
||||
interface DeepSeekHandlerOptions {
|
||||
deepSeekApiKey?: string
|
||||
apiModelId?: string
|
||||
deepSeekModelId?: string
|
||||
}
|
||||
|
||||
export class DeepSeekHandler implements ApiHandler {
|
||||
@@ -119,7 +119,7 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: DeepSeekModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.deepSeekModelId
|
||||
if (modelId && modelId in deepSeekModels) {
|
||||
const id = modelId as DeepSeekModelId
|
||||
return { id, info: deepSeekModels[id] }
|
||||
|
||||
@@ -8,7 +8,7 @@ import { withRetry } from "../retry"
|
||||
|
||||
interface DoubaoHandlerOptions {
|
||||
doubaoApiKey?: string
|
||||
apiModelId?: string
|
||||
doubaoModelId?: string
|
||||
}
|
||||
|
||||
export class DoubaoHandler implements ApiHandler {
|
||||
@@ -36,7 +36,7 @@ export class DoubaoHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: DoubaoModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.doubaoModelId
|
||||
if (modelId && modelId in doubaoModels) {
|
||||
const id = modelId as DoubaoModelId
|
||||
return { id, info: doubaoModels[id] }
|
||||
|
||||
@@ -19,7 +19,7 @@ interface GeminiHandlerOptions {
|
||||
geminiApiKey?: string
|
||||
geminiBaseUrl?: string
|
||||
thinkingBudgetTokens?: number
|
||||
apiModelId?: string
|
||||
geminiModelId?: string
|
||||
taskId?: string
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
* Get the model ID and info for the current configuration
|
||||
*/
|
||||
getModel(): { id: GeminiModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.geminiModelId
|
||||
if (modelId && modelId in geminiModels) {
|
||||
const id = modelId as GeminiModelId
|
||||
return { id, info: geminiModels[id] }
|
||||
|
||||
@@ -11,7 +11,6 @@ interface GroqHandlerOptions {
|
||||
groqApiKey?: string
|
||||
groqModelId?: string
|
||||
groqModelInfo?: ModelInfo
|
||||
apiModelId?: string // For backward compatibility
|
||||
}
|
||||
|
||||
// Model family definitions for enhanced behavior
|
||||
@@ -267,7 +266,7 @@ export class GroqHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Third priority: apiModelId (for backward compatibility)
|
||||
const apiModelId = this.options.apiModelId
|
||||
const apiModelId = this.options.groqModelId
|
||||
if (apiModelId && apiModelId in groqModels) {
|
||||
const id = apiModelId as GroqModelId
|
||||
return { id, info: groqModels[id] }
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import { ApiHandler } from ".."
|
||||
import { huaweiCloudMaasDefaultModelId, HuaweiCloudMaasModelId, huaweiCloudMaasModels, ModelInfo } from "@shared/api"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
interface HuaweiCloudMaaSHandlerOptions {
|
||||
huaweiCloudMaasApiKey?: string
|
||||
huaweiCloudMaasModelId?: string
|
||||
huaweiCloudMaasModelInfo?: ModelInfo
|
||||
}
|
||||
|
||||
export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
private options: HuaweiCloudMaaSHandlerOptions
|
||||
private client: OpenAI | undefined
|
||||
constructor(options: HuaweiCloudMaaSHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.huaweiCloudMaasApiKey) {
|
||||
throw new Error("Huawei Cloud MaaS API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.modelarts-maas.com/v1/",
|
||||
apiKey: this.options.huaweiCloudMaasApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Huawei Cloud MaaS client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
getModel(): { id: HuaweiCloudMaasModelId; info: ModelInfo } {
|
||||
// First priority: huaweiCloudMaasModelId and huaweiCloudMaasModelInfo (like Groq does)
|
||||
const huaweiCloudMaasModelId = this.options.huaweiCloudMaasModelId
|
||||
const huaweiCloudMaasModelInfo = this.options.huaweiCloudMaasModelInfo
|
||||
if (huaweiCloudMaasModelId && huaweiCloudMaasModelInfo) {
|
||||
return { id: huaweiCloudMaasModelId as HuaweiCloudMaasModelId, info: huaweiCloudMaasModelInfo }
|
||||
}
|
||||
|
||||
// Second priority: huaweiCloudMaasModelId with static model info
|
||||
if (huaweiCloudMaasModelId && huaweiCloudMaasModelId in huaweiCloudMaasModels) {
|
||||
const id = huaweiCloudMaasModelId as HuaweiCloudMaasModelId
|
||||
return { id, info: huaweiCloudMaasModels[id] }
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
id: huaweiCloudMaasDefaultModelId,
|
||||
info: huaweiCloudMaasModels[huaweiCloudMaasDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
let reasoning: string | null = null
|
||||
let didOutputUsage: boolean = false
|
||||
let finalUsage: any = null
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
// Handle reasoning content detection
|
||||
if (delta?.content) {
|
||||
if (reasoning || delta.content.includes("<think>")) {
|
||||
reasoning = (reasoning || "") + delta.content
|
||||
} else if (!reasoning) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reasoning output
|
||||
if (reasoning || (delta && "reasoning_content" in delta && delta.reasoning_content)) {
|
||||
const reasoningContent = delta?.content || ((delta as any)?.reasoning_content as string | undefined) || ""
|
||||
if (reasoningContent.trim()) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: reasoningContent,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if reasoning is complete
|
||||
if (reasoning?.includes("</think>")) {
|
||||
reasoning = null
|
||||
}
|
||||
}
|
||||
|
||||
// Store usage information for later output
|
||||
if (chunk.usage) {
|
||||
finalUsage = chunk.usage
|
||||
}
|
||||
|
||||
// Output usage when stream is finished
|
||||
if (!didOutputUsage && chunk.choices?.[0]?.finish_reason) {
|
||||
if (finalUsage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: finalUsage.prompt_tokens || 0,
|
||||
outputTokens: finalUsage.completion_tokens || 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
}
|
||||
}
|
||||
didOutputUsage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface MistralHandlerOptions {
|
||||
mistralApiKey?: string
|
||||
apiModelId?: string
|
||||
mistralModelId?: string
|
||||
}
|
||||
|
||||
export class MistralHandler implements ApiHandler {
|
||||
@@ -82,7 +82,7 @@ export class MistralHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: MistralModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.mistralModelId
|
||||
if (modelId && modelId in mistralModels) {
|
||||
const id = modelId as MistralModelId
|
||||
return { id, info: mistralModels[id] }
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ModelInfo, MoonshotModelId, moonshotModels, moonshotDefaultModelId } fr
|
||||
interface MoonshotHandlerOptions {
|
||||
moonshotApiKey?: string
|
||||
moonshotApiLine?: string
|
||||
apiModelId?: string
|
||||
moonshotModelId?: string
|
||||
}
|
||||
|
||||
export class MoonshotHandler implements ApiHandler {
|
||||
@@ -79,7 +79,7 @@ export class MoonshotHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: MoonshotModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.moonshotModelId
|
||||
|
||||
if (modelId && modelId in moonshotModels) {
|
||||
const id = modelId as MoonshotModelId
|
||||
|
||||
@@ -9,7 +9,7 @@ import { nebiusDefaultModelId, nebiusModels, type ModelInfo, type NebiusModelId
|
||||
|
||||
interface NebiusHandlerOptions {
|
||||
nebiusApiKey?: string
|
||||
apiModelId?: string
|
||||
nebiusModelId?: string
|
||||
}
|
||||
|
||||
export class NebiusHandler implements ApiHandler {
|
||||
@@ -77,7 +77,7 @@ export class NebiusHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.nebiusModelId
|
||||
|
||||
if (modelId !== undefined && modelId in nebiusModels) {
|
||||
return { id: modelId, info: nebiusModels[modelId as NebiusModelId] }
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ChatCompletionReasoningEffort } from "openai/resources/chat/comple
|
||||
interface OpenAiNativeHandlerOptions {
|
||||
openAiNativeApiKey?: string
|
||||
reasoningEffort?: string
|
||||
apiModelId?: string
|
||||
openaiNativeModelId?: string
|
||||
}
|
||||
|
||||
export class OpenAiNativeHandler implements ApiHandler {
|
||||
@@ -132,7 +132,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: OpenAiNativeModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.openaiNativeModelId
|
||||
if (modelId && modelId in openAiNativeModels) {
|
||||
const id = modelId as OpenAiNativeModelId
|
||||
return { id, info: openAiNativeModels[id] }
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
internationalQwenDefaultModelId,
|
||||
MainlandQwenModelId,
|
||||
InternationalQwenModelId,
|
||||
QwenApiRegions,
|
||||
} from "@shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -18,8 +17,8 @@ import { withRetry } from "../retry"
|
||||
|
||||
interface QwenHandlerOptions {
|
||||
qwenApiKey?: string
|
||||
qwenApiLine?: QwenApiRegions
|
||||
apiModelId?: string
|
||||
qwenApiLine?: string
|
||||
qwenModelId?: string
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
@@ -28,15 +27,7 @@ export class QwenHandler implements ApiHandler {
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: QwenHandlerOptions) {
|
||||
// Ensure options start with defaults but allow overrides
|
||||
this.options = {
|
||||
qwenApiLine: QwenApiRegions.CHINA,
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
private useChinaApi(): boolean {
|
||||
return this.options.qwenApiLine === QwenApiRegions.CHINA
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
@@ -46,9 +37,10 @@ export class QwenHandler implements ApiHandler {
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.useChinaApi()
|
||||
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
baseURL:
|
||||
this.options.qwenApiLine === "china"
|
||||
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
apiKey: this.options.qwenApiKey,
|
||||
})
|
||||
} catch (error: any) {
|
||||
@@ -59,9 +51,9 @@ export class QwenHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: MainlandQwenModelId | InternationalQwenModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.qwenModelId
|
||||
// Branch based on API line to let poor typescript know what to do
|
||||
if (this.useChinaApi()) {
|
||||
if (this.options.qwenApiLine === "china") {
|
||||
return {
|
||||
id: (modelId as MainlandQwenModelId) ?? mainlandQwenDefaultModelId,
|
||||
info: mainlandQwenModels[modelId as MainlandQwenModelId] ?? mainlandQwenModels[mainlandQwenDefaultModelId],
|
||||
|
||||
@@ -9,7 +9,7 @@ import { convertToR1Format } from "@api/transform/r1-format"
|
||||
|
||||
interface SambanovaHandlerOptions {
|
||||
sambanovaApiKey?: string
|
||||
apiModelId?: string
|
||||
sambanovaModelId?: string
|
||||
}
|
||||
|
||||
export class SambanovaHandler implements ApiHandler {
|
||||
@@ -81,7 +81,7 @@ export class SambanovaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.sambanovaModelId
|
||||
if (modelId && modelId in sambanovaModels) {
|
||||
const id = modelId as SambanovaModelId
|
||||
return { id, info: sambanovaModels[id] }
|
||||
|
||||
@@ -12,7 +12,7 @@ interface SapAiCoreHandlerOptions {
|
||||
sapAiCoreTokenUrl?: string
|
||||
sapAiResourceGroup?: string
|
||||
sapAiCoreBaseUrl?: string
|
||||
apiModelId?: string
|
||||
sapAiCoreModelId?: string
|
||||
}
|
||||
|
||||
interface Deployment {
|
||||
@@ -574,7 +574,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: SapAiCoreModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.sapAiCoreModelId
|
||||
if (modelId && modelId in sapAiCoreModels) {
|
||||
const id = modelId as SapAiCoreModelId
|
||||
return { id, info: sapAiCoreModels[id] }
|
||||
|
||||
@@ -9,7 +9,7 @@ import { GeminiHandler } from "./gemini"
|
||||
interface VertexHandlerOptions {
|
||||
vertexProjectId?: string
|
||||
vertexRegion?: string
|
||||
apiModelId?: string
|
||||
vertexModelId?: string
|
||||
thinkingBudgetTokens?: number
|
||||
geminiApiKey?: string
|
||||
geminiBaseUrl?: string
|
||||
@@ -264,7 +264,7 @@ export class VertexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: VertexModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.vertexModelId
|
||||
if (modelId && modelId in vertexModels) {
|
||||
const id = modelId as VertexModelId
|
||||
return { id, info: vertexModels[id] }
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
import { convertToVsCodeLmMessages } from "@api/transform/vscode-lm-format"
|
||||
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
|
||||
import { calculateApiCostAnthropic } from "@utils/cost"
|
||||
import * as vscode from "vscode"
|
||||
import { ApiHandler, SingleCompletionHandler } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { calculateApiCostAnthropic } from "@utils/cost"
|
||||
import { ApiStream } from "@api/transform/stream"
|
||||
import { convertToVsCodeLmMessages } from "@api/transform/vscode-lm-format"
|
||||
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCodeSelectorUtils"
|
||||
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
interface VsCodeLmHandlerOptions {
|
||||
vsCodeLmModelSelector?: any
|
||||
@@ -237,28 +237,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private extractTextFromMessage(message: vscode.LanguageModelChatMessage): string {
|
||||
if (Array.isArray(message.content)) {
|
||||
return message.content
|
||||
.filter((part) => part instanceof vscode.LanguageModelTextPart)
|
||||
.map((part) => (part as vscode.LanguageModelTextPart).value)
|
||||
.join("")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private isClaudeModel(): boolean {
|
||||
return this.client?.family?.startsWith("claude") || false
|
||||
}
|
||||
|
||||
private async countTokens(text: string | vscode.LanguageModelChatMessage): Promise<number> {
|
||||
// For Claude models, use character-to-token ratio instead of VSCode LM's inaccurate counting
|
||||
if (this.isClaudeModel()) {
|
||||
const textContent = typeof text === "string" ? text : this.extractTextFromMessage(text)
|
||||
// Use 4 character-to-token ratio for Claude models
|
||||
return Math.ceil(textContent.length / 4)
|
||||
}
|
||||
|
||||
// Check for required dependencies
|
||||
if (!this.client) {
|
||||
console.warn("Cline <Language Model API>: No client available for token counting")
|
||||
@@ -325,10 +304,15 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async calculateTotalInputTokens(vsCodeLmMessages: vscode.LanguageModelChatMessage[]): Promise<number> {
|
||||
private async calculateTotalInputTokens(
|
||||
systemPrompt: string,
|
||||
vsCodeLmMessages: vscode.LanguageModelChatMessage[],
|
||||
): Promise<number> {
|
||||
const systemTokens: number = await this.countTokens(systemPrompt)
|
||||
|
||||
const messageTokens: number[] = await Promise.all(vsCodeLmMessages.map((msg) => this.countTokens(msg)))
|
||||
|
||||
return messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)
|
||||
return systemTokens + messageTokens.reduce((sum: number, tokens: number): number => sum + tokens, 0)
|
||||
}
|
||||
|
||||
private ensureCleanState(): void {
|
||||
@@ -450,7 +434,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
this.currentRequestCancellation = new vscode.CancellationTokenSource()
|
||||
|
||||
// Calculate input tokens before starting the stream
|
||||
const totalInputTokens: number = await this.calculateTotalInputTokens(vsCodeLmMessages)
|
||||
const totalInputTokens: number = await this.calculateTotalInputTokens(systemPrompt, vsCodeLmMessages)
|
||||
|
||||
// Accumulate the text and count at the end of the stream to reduce token counting overhead.
|
||||
let accumulatedText: string = ""
|
||||
|
||||
@@ -11,13 +11,7 @@ import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
interface XAIHandlerOptions {
|
||||
xaiApiKey?: string
|
||||
reasoningEffort?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
interface XAIHandlerOptions {
|
||||
xaiApiKey?: string
|
||||
reasoningEffort?: string
|
||||
apiModelId?: string
|
||||
xaiModelId?: string
|
||||
}
|
||||
|
||||
export class XAIHandler implements ApiHandler {
|
||||
@@ -102,7 +96,7 @@ export class XAIHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: XAIModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
const modelId = this.options.xaiModelId
|
||||
if (modelId && modelId in xaiModels) {
|
||||
const id = modelId as XAIModelId
|
||||
return { id, info: xaiModels[id] }
|
||||
|
||||
+41
-63
@@ -1,8 +1,6 @@
|
||||
export enum Environment {
|
||||
production = "production",
|
||||
staging = "staging",
|
||||
local = "local",
|
||||
}
|
||||
export type Environment = "production" | "staging" | "local"
|
||||
|
||||
const CLINE_ENVIRONMENT: Environment = (process.env.CLINE_ENVIRONMENT as Environment) || "production"
|
||||
|
||||
interface EnvironmentConfig {
|
||||
appBaseUrl: string
|
||||
@@ -18,63 +16,43 @@ interface EnvironmentConfig {
|
||||
}
|
||||
}
|
||||
|
||||
function getClineEnv(): Environment {
|
||||
const _env = process?.env?.CLINE_ENVIRONMENT
|
||||
if (_env && Object.values(Environment).includes(_env as Environment)) {
|
||||
return _env as Environment
|
||||
}
|
||||
return Environment.production
|
||||
const configs: Record<Environment, EnvironmentConfig> = {
|
||||
production: {
|
||||
appBaseUrl: "https://app.cline.bot",
|
||||
apiBaseUrl: "https://api.cline.bot",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyC5rx59Xt8UgwdU3PCfzUF7vCwmp9-K2vk",
|
||||
authDomain: "cline-prod.firebaseapp.com",
|
||||
projectId: "cline-prod",
|
||||
storageBucket: "cline-prod.firebasestorage.app",
|
||||
messagingSenderId: "941048379330",
|
||||
appId: "1:941048379330:web:45058eedeefc5cdfcc485b",
|
||||
},
|
||||
},
|
||||
staging: {
|
||||
appBaseUrl: "https://staging-app.cline.bot",
|
||||
apiBaseUrl: "https://core-api.staging.int.cline.bot",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
|
||||
authDomain: "cline-staging.firebaseapp.com",
|
||||
projectId: "cline-staging",
|
||||
storageBucket: "cline-staging.firebasestorage.app",
|
||||
messagingSenderId: "853479478430",
|
||||
appId: "1:853479478430:web:2de0dba1c63c3262d4578f",
|
||||
},
|
||||
},
|
||||
local: {
|
||||
appBaseUrl: "http://localhost:3000",
|
||||
apiBaseUrl: "http://localhost:7777",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyD8wtkd1I-EICuAg6xgAQpRdwYTvwxZG2w",
|
||||
authDomain: "cline-preview.firebaseapp.com",
|
||||
projectId: "cline-preview",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config getter function to avoid storing all configs in memory
|
||||
function getEnvironmentConfig(env: Environment): EnvironmentConfig {
|
||||
switch (env) {
|
||||
case Environment.staging:
|
||||
return {
|
||||
appBaseUrl: "https://staging-app.cline.bot",
|
||||
apiBaseUrl: "https://core-api.staging.int.cline.bot",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
|
||||
authDomain: "cline-staging.firebaseapp.com",
|
||||
projectId: "cline-staging",
|
||||
storageBucket: "cline-staging.firebasestorage.app",
|
||||
messagingSenderId: "853479478430",
|
||||
appId: "1:853479478430:web:2de0dba1c63c3262d4578f",
|
||||
},
|
||||
}
|
||||
case Environment.local:
|
||||
return {
|
||||
appBaseUrl: "http://localhost:3000",
|
||||
apiBaseUrl: "http://localhost:7777",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyD8wtkd1I-EICuAg6xgAQpRdwYTvwxZG2w",
|
||||
authDomain: "cline-preview.firebaseapp.com",
|
||||
projectId: "cline-preview",
|
||||
},
|
||||
}
|
||||
default:
|
||||
return {
|
||||
appBaseUrl: "https://app.cline.bot",
|
||||
apiBaseUrl: "https://api.cline.bot",
|
||||
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
|
||||
firebase: {
|
||||
apiKey: "AIzaSyC5rx59Xt8UgwdU3PCfzUF7vCwmp9-K2vk",
|
||||
authDomain: "cline-prod.firebaseapp.com",
|
||||
projectId: "cline-prod",
|
||||
storageBucket: "cline-prod.firebasestorage.app",
|
||||
messagingSenderId: "941048379330",
|
||||
appId: "1:941048379330:web:45058eedeefc5cdfcc485b",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get environment once at module load
|
||||
const CLINE_ENVIRONMENT = getClineEnv()
|
||||
const _configCache = getEnvironmentConfig(CLINE_ENVIRONMENT)
|
||||
|
||||
console.info("Cline environment:", CLINE_ENVIRONMENT)
|
||||
|
||||
export const clineEnvConfig = _configCache
|
||||
export const clineEnvConfig = configs[CLINE_ENVIRONMENT]
|
||||
|
||||
@@ -6,8 +6,8 @@ import * as path from "path"
|
||||
import { FileContextTracker } from "./FileContextTracker"
|
||||
import * as diskModule from "@core/storage/disk"
|
||||
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-provider"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import type { DiffViewProviderCreator, WebviewProviderCreator } from "@/hosts/host-providers"
|
||||
import * as hostProviders from "@hosts/host-providers"
|
||||
import { vscodeHostBridgeClient } from "@/hosts/vscode/hostbridge/client/host-grpc-client"
|
||||
|
||||
describe("FileContextTracker", () => {
|
||||
@@ -53,14 +53,10 @@ describe("FileContextTracker", () => {
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
|
||||
// Reset HostProvider before initializing to avoid "already initialized" errors
|
||||
HostProvider.reset()
|
||||
HostProvider.initialize(
|
||||
hostProviders.initializeHostProviders(
|
||||
((_) => {}) as WebviewProviderCreator,
|
||||
(() => {}) as DiffViewProviderCreator,
|
||||
vscodeHostBridgeClient,
|
||||
(_) => {},
|
||||
)
|
||||
|
||||
// Create tracker instance
|
||||
@@ -70,8 +66,6 @@ describe("FileContextTracker", () => {
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
// Reset HostProvider after each test to ensure clean state
|
||||
HostProvider.reset()
|
||||
})
|
||||
|
||||
it("should add a record when a file is read by a tool", async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getWorkspaceState, updateWorkspaceState } from "@core/storage/state"
|
||||
import { getGlobalState } from "@core/storage/state"
|
||||
import type { FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
import type { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { getHostBridgeProvider } from "@/hosts/host-providers"
|
||||
import { getCwd } from "@/utils/path"
|
||||
|
||||
// This class is responsible for tracking file operations that may result in stale context.
|
||||
|
||||
@@ -240,7 +240,7 @@ export async function deleteRuleFile(
|
||||
}
|
||||
|
||||
// Delete the file from disk
|
||||
await fs.rm(rulePath, { force: true })
|
||||
await fs.unlink(rulePath)
|
||||
|
||||
// Get the filename for messages
|
||||
const fileName = path.basename(rulePath)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Controller } from "../index"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { EmptyRequest, String } from "@shared/proto/cline/common"
|
||||
import { EmptyRequest, String } from "../../../shared/proto/common"
|
||||
import { openExternal } from "@utils/env"
|
||||
|
||||
const authService = AuthService.getInstance()
|
||||
|
||||
@@ -12,6 +13,6 @@ const authService = AuthService.getInstance()
|
||||
* @param controller The controller instance.
|
||||
* @returns The login URL as a string.
|
||||
*/
|
||||
export async function accountLoginClicked(_controller: Controller, _: EmptyRequest): Promise<String> {
|
||||
export async function accountLoginClicked(controller: Controller, _: EmptyRequest): Promise<String> {
|
||||
return await authService.createAuthRequest()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import type { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { Empty } from "../../../shared/proto/common"
|
||||
import type { EmptyRequest } from "../../../shared/proto/common"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
const authService = AuthService.getInstance()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AuthStateChangedRequest, AuthState } from "@shared/proto/cline/account"
|
||||
import { AuthStateChangedRequest, AuthState } from "@shared/proto/account"
|
||||
import type { Controller } from "../index"
|
||||
import { updateGlobalState } from "../../storage/state"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Controller } from "../index"
|
||||
import { GetOrganizationCreditsRequest, OrganizationCreditsData, OrganizationUsageTransaction } from "@shared/proto/cline/account"
|
||||
import { GetOrganizationCreditsRequest, OrganizationCreditsData, OrganizationUsageTransaction } from "@shared/proto/account"
|
||||
|
||||
/**
|
||||
* Handles fetching all organization credits data (balance, usage, payments)
|
||||
@@ -22,11 +22,6 @@ export async function getOrganizationCredits(
|
||||
controller.accountService.fetchOrganizationUsageTransactionsRPC(request.organizationId),
|
||||
])
|
||||
|
||||
// If balance call fails (returns undefined), throw an error
|
||||
if (!balanceData) {
|
||||
throw new Error("Failed to fetch organization credits data")
|
||||
}
|
||||
|
||||
return OrganizationCreditsData.create({
|
||||
balance: balanceData ? { currentBalance: balanceData.balance / 100 } : { currentBalance: 0 },
|
||||
organizationId: balanceData?.organizationId || "",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Controller } from "../index"
|
||||
import type { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { UserCreditsData } from "@shared/proto/cline/account"
|
||||
import type { EmptyRequest } from "@shared/proto/common"
|
||||
import { UserCreditsData } from "@shared/proto/account"
|
||||
|
||||
/**
|
||||
* Handles fetching all user credits data (balance, usage, payments)
|
||||
@@ -21,11 +21,6 @@ export async function getUserCredits(controller: Controller, request: EmptyReque
|
||||
controller.accountService.fetchPaymentTransactionsRPC(),
|
||||
])
|
||||
|
||||
// If either call fails (returns undefined), throw an error
|
||||
if (balance === undefined) {
|
||||
throw new Error("Failed to fetch user credits data")
|
||||
}
|
||||
|
||||
return UserCreditsData.create({
|
||||
balance: balance ? { currentBalance: balance.balance / 100 } : { currentBalance: 0 },
|
||||
usageTransactions: usageTransactions,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Controller } from "../index"
|
||||
import type { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { UserOrganization, UserOrganizationsResponse } from "@shared/proto/cline/account"
|
||||
import type { EmptyRequest } from "@shared/proto/common"
|
||||
import { UserOrganization, UserOrganizationsResponse } from "@shared/proto/account"
|
||||
|
||||
/**
|
||||
* Handles fetching all user credits data (balance, usage, payments)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Controller } from "../index"
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { UserOrganizationUpdateRequest } from "@shared/proto/cline/account"
|
||||
import { Empty } from "@shared/proto/common"
|
||||
import { UserOrganizationUpdateRequest } from "@shared/proto/account"
|
||||
|
||||
/**
|
||||
* Handles setting the user's active organization
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BrowserConnection } from "@shared/proto/cline/browser"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { BrowserConnection } from "@shared/proto/browser"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { getAllExtensionState } from "@core/storage/state"
|
||||
import { BrowserSession } from "@services/browser/BrowserSession"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BrowserConnectionInfo } from "@shared/proto/cline/browser"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { BrowserConnectionInfo } from "@shared/proto/browser"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { getAllExtensionState } from "@core/storage/state"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChromePath } from "@shared/proto/cline/browser"
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { ChromePath } from "../../../shared/proto/browser"
|
||||
import { EmptyRequest } from "../../../shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { getAllExtensionState } from "../../storage/state"
|
||||
import { BrowserSession } from "../../../services/browser/BrowserSession"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EmptyRequest, String as StringMessage } from "@shared/proto/cline/common"
|
||||
import { EmptyRequest, String as StringMessage } from "../../../shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { BrowserSession } from "../../../services/browser/BrowserSession"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BrowserConnection } from "@shared/proto/cline/browser"
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { BrowserConnection } from "@shared/proto/browser"
|
||||
import { StringRequest } from "@shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { getAllExtensionState } from "@core/storage/state"
|
||||
import { BrowserSession } from "@services/browser/BrowserSession"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { UpdateBrowserSettingsRequest } from "@shared/proto/cline/browser"
|
||||
import { Boolean } from "@shared/proto/cline/common"
|
||||
import { UpdateBrowserSettingsRequest } from "../../../shared/proto/browser"
|
||||
import { Boolean } from "../../../shared/proto/common"
|
||||
import { Controller } from "../index"
|
||||
import { updateGlobalState, getGlobalState } from "../../storage/state"
|
||||
import { BrowserSettings as SharedBrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../../shared/BrowserSettings"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user