mirror of
https://github.com/cline/cline.git
synced 2026-09-02 07:42:19 +08:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cca4226d1 | |||
| 7a9bb4278f | |||
| d2d171e0c2 | |||
| df283a0d49 | |||
| 3ba6c3bf31 | |||
| 80f581e750 | |||
| b25262e7df | |||
| 73a8543df7 | |||
| 0ac6777ce3 | |||
| 3701b4df6c | |||
| d9548271af | |||
| 89980e5192 | |||
| a9a3812fd9 | |||
| d2aa530edc | |||
| 9b639a0203 | |||
| bc0a45ee91 | |||
| 2a5a67f9ef | |||
| 293d9396d5 | |||
| 608bdba50e | |||
| 2f55e00acc | |||
| fef7e2d3c6 | |||
| 8be4255f2f | |||
| e690832569 | |||
| 93ef27557b | |||
| 5e889ccf4f | |||
| 99d84246e4 | |||
| a70e2ec31a | |||
| c8aae13268 | |||
| 5d6a367181 | |||
| 67607b3f8e | |||
| 328b9cbf5b | |||
| 3bddcff52d | |||
| df02404b4e | |||
| 9033c65214 | |||
| a69f9679ff | |||
| 115b122225 | |||
| f4a97d67d8 | |||
| 3fd42632c8 | |||
| 8f7b865e23 | |||
| 62c5980eb3 | |||
| 61f4143620 | |||
| 8e9dde7903 | |||
| 3b9b386099 | |||
| c649c371de | |||
| c6fe0854d3 | |||
| a3f43d2095 | |||
| 14615d44c4 | |||
| d8c9776328 | |||
| 623c0833e6 | |||
| f47d1670c5 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Reasoning budget is now configurable for all supported models from OpenRouter.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix Ollama connection issue to default endpoint at port 11434
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Optimized Cline for GPT-5 model family with an aligned system prompt
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add \*.go files to deep-planning prompt
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add "Use custom prompt" option to Ollama provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix AutoApproveModal overflowing issue
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Dify.ai api integration
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Prompt changes for deep-planning in windows/powershell
|
||||
@@ -1,6 +0,0 @@
|
||||
[codespell]
|
||||
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
||||
skip = .git*,*.svg,package-lock.json,*.css,.codespellrc,locales
|
||||
check-hidden = true
|
||||
ignore-regex = (\b(optIn|isTaller)\b|https://\S+)
|
||||
# ignore-words-list =
|
||||
@@ -1,28 +0,0 @@
|
||||
# Codespell configuration is within .codespellrc
|
||||
---
|
||||
name: Codespell
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
if: false
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Annotate locations with typos
|
||||
uses: codespell-project/codespell-problem-matcher@v1
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
only_warn: 1
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
CLINE_ENVIRONMENT: production
|
||||
run: |
|
||||
# Required to generate the .vsix
|
||||
vsce package --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
vsce package --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix"
|
||||
|
||||
if [ "${{ github.event.inputs.release-type }}" = "pre-release" ]; then
|
||||
npm run publish:marketplace:prerelease
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
name: Scenario Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
matrix_prep:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- id: set-matrix
|
||||
run: |
|
||||
echo 'matrix=[{"runner":"ubuntu"},{"runner":"windows"},{"runner":"macos"}]' >> $GITHUB_OUTPUT
|
||||
|
||||
validate-scenario:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Validate PR-specific scenario metadata
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" != "pull_request" ]; then
|
||||
echo "Not a pull_request event; skipping scenario validation."
|
||||
exit 0
|
||||
fi
|
||||
bash scripts/validate-scenario.sh "${{ github.event.number }}"
|
||||
|
||||
scenarios:
|
||||
needs: [matrix_prep, validate-scenario]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.matrix_prep.outputs.matrix) }}
|
||||
runs-on: ${{ matrix.runner }}-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
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
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Install xvfb on Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
|
||||
# Run optimized Scenario tests (reuses build steps like e2e)
|
||||
- name: Run Scenario tests - Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: xvfb-run -a npm run test:scenarios:optimal
|
||||
|
||||
- name: Run Scenario tests - Non-Linux
|
||||
if: matrix.runner != 'ubuntu'
|
||||
run: npm run test:scenarios:optimal
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: playwright-recordings-${{ matrix.runner }}
|
||||
path: |
|
||||
test-results/playwright/
|
||||
|
||||
scenario-summary:
|
||||
needs: scenarios
|
||||
permissions: {}
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check all scenarios passed
|
||||
run: |
|
||||
if [ "${{ needs.scenarios.result }}" != "success" ]; then
|
||||
echo "Some scenario tests failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "All scenario tests passed successfully"
|
||||
+1
-2
@@ -3,8 +3,7 @@
|
||||
"ts"
|
||||
],
|
||||
"spec": [
|
||||
"src/**/__tests__/*.ts",
|
||||
"eslint-rules/__tests__/**/*.test.ts"
|
||||
"src/**/__tests__/*.ts"
|
||||
],
|
||||
"require": [
|
||||
"ts-node/register",
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
## [3.26.6]
|
||||
|
||||
- Add free Grok Coder model to Cline provider for users looking for a fast, free coding model option
|
||||
- Fix GPT-5 models not respecting auto-compact setting when enabled, improving context window management
|
||||
- Fix provider retry attempts not showing proper user feedback during rate limiting scenarios
|
||||
- Improve markdown and code block styling to automatically adapt when switching VS Code themes
|
||||
|
||||
## [3.26.5]
|
||||
|
||||
- fix (provider/vercel-ai-gateway): reduce model list load frequency in settings view
|
||||
- Fix OVSX publish command to resolve deployment failure
|
||||
|
||||
## [3.26.4]
|
||||
|
||||
- Update nebius ai studio models
|
||||
- Update sap provider - support reasoning effort for open ai models
|
||||
- Fix Claude 4 image input in SAP AI Core Provider
|
||||
|
||||
## [3.26.3]
|
||||
|
||||
- Add compact system prompt option for LM Studio and Ollama models, optimized for smaller context windows (8k or less)
|
||||
- Add token usage tracking for LM Studio models to better monitor API consumption
|
||||
- Add "Use compact prompt" checkbox in LM Studio provider settings
|
||||
- Fix "Unexpected API Response" bug with gpt-5
|
||||
|
||||
## [3.26.2]
|
||||
|
||||
- Improve OpenRouter model parsing to show reasoning budget sliders for all models that support thinking, not just Claude models
|
||||
- Fix OpenRouter context window error handling to properly extract error codes from error messages, resolving "Unexpected API Response" errors with GPT-5 on Cline provider
|
||||
- Fix GPT-5 context window configuration for OpenAI/OpenRouter/Cline providers to use correct 272K limit
|
||||
- Remove max tokens configuration from Sonic Alpha model
|
||||
- Add Go language support to deep-planning feature (Thanks @yuvalman!)
|
||||
- Fix typo in Focus Chain settings page (Thanks @joyceerhl!)
|
||||
|
||||
## [3.26.1]
|
||||
|
||||
- Add Vercel AI Gateway as a new API provider option (Thanks @joshualipman123!)
|
||||
|
||||
@@ -40,3 +40,20 @@ Refer to the [Generative AI Hub Supported Models page](https://me.sap.com/notes/
|
||||
- **Deployed Models:** These models are already deployed in your specified resource group and are ready to use immediately.
|
||||
- **Not Deployed Models:** These models don't have active deployments in your specified resource group. You won't be able to use these models until you create deployments for them in SAP AI Core.
|
||||
- **Creating Deployments:** To use a not deployed model, you'll need to create a deployment in your resource group in sap ai core service instance. See [Create a Deployment for a Generative AI Model](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/create-deployment-for-generative-ai-model-in-sap-ai-core) for instructions.
|
||||
|
||||
#### Configuring Reasoning Effort for OpenAI Models
|
||||
|
||||
When using OpenAI reasoning models (such as o1, o3, o3-mini, o4-mini) through SAP AI Core, you can control the reasoning effort to balance performance and cost:
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
2. **Navigate to Features:** Go to the "Features" section in the settings.
|
||||
3. **Find OpenAI Reasoning Effort:** Locate the "OpenAI Reasoning Effort" setting.
|
||||
4. **Choose Effort Level:** Select between:
|
||||
- **Low:** Faster responses with lower token usage, suitable for simpler tasks
|
||||
- **Medium:** Balanced performance and token usage for most tasks
|
||||
- **High:** More thorough analysis with higher token usage, better for complex reasoning tasks
|
||||
|
||||
> 💡 **Note**
|
||||
>
|
||||
> This setting only applies when using OpenAI reasoning models (o1, o3, o3-mini, o4-mini, gpt-5, etc.) deployed through SAP AI Core. Other models will ignore this setting.
|
||||
|
||||
|
||||
@@ -1,34 +1,98 @@
|
||||
---
|
||||
title: "Vercel AI Gateway"
|
||||
description: "Learn how to use Vercel AI Gateway with Cline to access a wide variety of AI models through a single unified API."
|
||||
description: "Use Vercel AI Gateway in Cline to reach 100+ models from one endpoint with routing, retries, and spend observability."
|
||||
---
|
||||
|
||||
The AI Gateway offers a unified API to multiple model providers and gives you the ability to set budgets, monitor usage, load-balance requests, and manage fallbacks.
|
||||
Vercel AI Gateway gives you a single API to access models from many providers. You switch by model id without swapping SDKs or juggling multiple keys. Cline integrates directly so you can pick a Gateway model in the dropdown, use it like any other provider, and see token and cache usage in the stream.
|
||||
|
||||
Useful Links:
|
||||
- [AI Gateway Team Dashboard](https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai&title=AI+Gateway+Overview+Tab)
|
||||
- [AI Gateway Landing Page](https://vercel.com/ai-gateway)
|
||||
Useful links:
|
||||
- Team dashboard: https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai
|
||||
- Models catalog: https://vercel.com/ai-gateway/models
|
||||
- Docs: https://vercel.com/docs/ai-gateway
|
||||
|
||||
### Getting an API Key
|
||||
## What you get
|
||||
|
||||
1. **Sign Up/Sign In:** Go to [vercel.com](https://vercel.com/) and create an account or sign in.
|
||||
2. **Get an API Key:** From the Vercel dashboard, click the AI Gateway tab. Then click **API keys** on the left side bar and hit **Create key**.
|
||||
3. **Copy the Key:** Copy the API key.
|
||||
- One endpoint for 100+ models with a single key
|
||||
- Automatic retries and fallbacks that you configure on the dashboard
|
||||
- Spend monitoring with requests by model, token counts, cache usage, latency percentiles, and cost
|
||||
- OpenAI-compatible surface so existing clients work
|
||||
|
||||
For more on Vercel AI Gateway authentication, see [Authentication](https://vercel.com/docs/ai-gateway/authentication)
|
||||
## Getting an API Key
|
||||
|
||||
### Supported Models
|
||||
1. Sign in at https://vercel.com
|
||||
2. Dashboard → AI Gateway → API Keys → Create key
|
||||
3. Copy the key
|
||||
|
||||
Vercel AI Gateway supports a large and growing number of models from various providers. Cline automatically fetches the list of available models from the Gateway API. Refer to the [Vercel AI Gateway Models page](https://vercel.com/ai-gateway/models) for the complete list.
|
||||
For more on authentication and OIDC options, see https://vercel.com/docs/ai-gateway/authentication
|
||||
|
||||
### Configuration in Cline
|
||||
## Configuration in Cline
|
||||
|
||||
1. **Open Cline Settings:** Click the settings icon (⚙️) in the Cline panel.
|
||||
2. **Select Provider:** Choose "Vercel AI Gateway" from the "API Provider" dropdown.
|
||||
3. **Enter API Key:** Paste your Vercel AI Gateway API key into the "Vercel AI Gateway API Key" field.
|
||||
4. **Select Model:** Choose your desired model from the "Model" dropdown, or enter a custom model ID if the model list doesn't load automatically.
|
||||
1. Open Cline settings
|
||||
2. Select **Vercel AI Gateway** as the API Provider
|
||||
3. Paste your Gateway API Key
|
||||
4. Pick a model from the list. Cline fetches the catalog automatically. You can also paste an exact id
|
||||
|
||||
### Tips and Notes
|
||||
Notes:
|
||||
- Model ids often follow `provider/model`. Copy the exact id from the catalog
|
||||
Examples:
|
||||
- `openai/gpt-5`
|
||||
- `anthropic/claude-sonnet-4`
|
||||
- `google/gemini-2.5-pro`
|
||||
- `groq/llama-3.1-70b`
|
||||
- `deepseek/deepseek-v3`
|
||||
|
||||
- **Model Selection:** The AI Gateway offers a wide range of models. Experiment to find the best one for your needs.
|
||||
- **Pricing:** Vercel charges based on the underlying model's pricing. See the [AI Gateway Pricing page](https://vercel.com/docs/ai-gateway/pricing) for details.
|
||||
## Observability you can act on
|
||||
|
||||
<Frame>
|
||||
<img src="https://assets.vercel.com/image/upload/v1753121283/gateway-overhead-dark_zhqwwj.svg" alt="Vercel AI Gateway observability with requests by model, tokens, cache, latency, and cost." />
|
||||
</Frame>
|
||||
|
||||
What to watch:
|
||||
- Requests by model - confirm routing and adoption
|
||||
- Tokens - input vs output, including reasoning if exposed
|
||||
- Cache - cached input and cache creation tokens
|
||||
- Latency - p75 duration and p75 time to first token
|
||||
- Cost - per project and per model
|
||||
|
||||
Use it to:
|
||||
- Compare output tokens per request before and after a model change
|
||||
- Validate cache strategy by tracking cache reads and write creation
|
||||
- Catch TTFT regressions during experiments
|
||||
- Align budgets with real usage
|
||||
|
||||
## Supported models
|
||||
|
||||
The gateway supports a large and changing set of models. Cline pulls the list from the Gateway API and caches it locally. For the current catalog, see https://vercel.com/ai-gateway/models
|
||||
|
||||
## Tips
|
||||
|
||||
<Tip>
|
||||
Use separate gateway keys per environment (dev, staging, prod). It keeps dashboards clean and budgets isolated.
|
||||
</Tip>
|
||||
|
||||
<Note>
|
||||
Pricing is pass-through at provider list price. Bring-your-own key has 0% markup. You still pay provider and processing fees.
|
||||
</Note>
|
||||
|
||||
<Info>
|
||||
Vercel does not add rate limits. Upstream providers may. New accounts receive $5 credits every 30 days until the first payment.
|
||||
</Info>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- 401 - send the Gateway key to the Gateway endpoint, not an upstream URL
|
||||
- 404 model - copy the exact id from the Vercel catalog
|
||||
- Slow first token - check p75 TTFT in the dashboard and try a model optimized for streaming
|
||||
- Cost spikes - break down by model in the dashboard and cap or route traffic
|
||||
|
||||
## Inspiration
|
||||
|
||||
- Multi-model evals - swap only the model id in Cline and compare latency and output tokens
|
||||
- Progressive rollout - route a small percent to a new model in the dashboard and ramp with metrics
|
||||
- Budget enforcement - set per-project limits without code changes
|
||||
|
||||
## Crosslinks
|
||||
|
||||
- OpenAI-Compatible setup: /provider-config/openai-compatible
|
||||
- Model Selection Guide: /getting-started/model-selection-guide
|
||||
- Understanding Context Management: /getting-started/understanding-context-management
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
See [https://cline.bot/privacy](https://cline.bot/privacy) for our privacy policy.
|
||||
@@ -1,39 +0,0 @@
|
||||
# Cline Documentation
|
||||
|
||||
Welcome to the Cline documentation - your comprehensive guide to using and extending Cline's capabilities. Here you'll find resources to help you get started, improve your skills, and contribute to the project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
- **New to coding?** We've prepared a gentle introduction:
|
||||
- [Getting Started for New Coders](getting-started-new-coders/README.md)
|
||||
|
||||
## Improving Your Prompting Skills
|
||||
|
||||
- **Want to communicate more effectively with Cline?** Explore:
|
||||
- [Prompt Engineering Guide](prompting/README.md)
|
||||
- [Cline Memory Bank](prompting/custom%20instructions%20library/cline-memory-bank.md)
|
||||
|
||||
## Exploring Cline's Tools
|
||||
|
||||
- **Understand Cline's capabilities:**
|
||||
|
||||
- [Cline Tools Guide](tools/cline-tools-guide.md)
|
||||
- [Mentions Feature Guide](tools/mentions-guide.md)
|
||||
|
||||
- **Extend Cline with MCP Servers:**
|
||||
- [MCP Overview](mcp/README.md)
|
||||
- [Building MCP Servers from GitHub](mcp/mcp-server-from-github.md)
|
||||
- [Building Custom MCP Servers](mcp/mcp-server-from-scratch.md)
|
||||
|
||||
## Contributing to Cline
|
||||
|
||||
- **Interested in contributing?** We welcome your input:
|
||||
- Feel free to submit a pull request
|
||||
- [Contribution Guidelines](../CONTRIBUTING.md)
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Cline GitHub Repository:** [https://github.com/cline/cline](https://github.com/cline/cline)
|
||||
- **MCP Documentation:** [https://modelcontextprotocol.org/docs](https://modelcontextprotocol.org/docs)
|
||||
|
||||
We're always looking to improve this documentation. If you have suggestions or find areas that could be enhanced, please let us know. Your feedback helps make Cline better for everyone.
|
||||
@@ -1,43 +0,0 @@
|
||||
# Cline Extension Architecture
|
||||
|
||||
This directory contains architectural documentation for the Cline VSCode extension.
|
||||
|
||||
## Extension Architecture Diagram
|
||||
|
||||
The [extension-architecture.mmd](./extension-architecture.mmd) file contains a Mermaid diagram showing the high-level architecture of the Cline extension. The diagram illustrates:
|
||||
|
||||
1. **Core Extension**
|
||||
- Extension entry point and main classes
|
||||
- State management through VSCode's global state and secrets storage
|
||||
- Core business logic in the Cline class
|
||||
|
||||
2. **Webview UI**
|
||||
- React-based user interface
|
||||
- State management through ExtensionStateContext
|
||||
- Component hierarchy
|
||||
|
||||
3. **Storage**
|
||||
- Task-specific storage for history and state
|
||||
- Git-based checkpoint system for file changes
|
||||
|
||||
4. **Data Flow**
|
||||
- Core extension data flow between components
|
||||
- Webview UI data flow
|
||||
- Bidirectional communication between core and webview
|
||||
|
||||
## Viewing the Diagram
|
||||
|
||||
To view the diagram:
|
||||
1. Install a Mermaid diagram viewer extension in VSCode
|
||||
2. Open extension-architecture.mmd
|
||||
3. Use the extension's preview feature to render the diagram
|
||||
|
||||
You can also view the diagram on GitHub, which has built-in Mermaid rendering support.
|
||||
|
||||
## Color Scheme
|
||||
|
||||
The diagram uses a high-contrast color scheme for better visibility:
|
||||
- Pink (#ff0066): Global state and secrets storage components
|
||||
- Blue (#0066ff): Extension state context
|
||||
- Green (#00cc66): Cline provider
|
||||
- All components use white text for maximum readability
|
||||
@@ -1,67 +0,0 @@
|
||||
flowchart TB
|
||||
subgraph "VSCode Extension Host"
|
||||
subgraph "Core Extension"
|
||||
ExtensionEntry["Extension Entry<br/>src/extension.ts"]
|
||||
WebviewProvider["WebviewProvider<br/>src/core/webview/index.ts"]
|
||||
Controller["Controller<br/>src/core/controller/index.ts"]
|
||||
Task["Task<br/>src/core/task/index.ts"]
|
||||
GlobalState["VSCode Global State"]
|
||||
SecretsStorage["VSCode Secrets Storage"]
|
||||
McpHub["McpHub<br/>src/services/mcp/McpHub.ts"]
|
||||
end
|
||||
|
||||
subgraph "Webview UI"
|
||||
WebviewApp["React App<br/>webview-ui/src/App.tsx"]
|
||||
ExtStateContext["ExtensionStateContext<br/>webview-ui/src/context/ExtensionStateContext.tsx"]
|
||||
ReactComponents["React Components"]
|
||||
end
|
||||
|
||||
subgraph "Storage"
|
||||
TaskStorage["Task Storage<br/>Per-Task Files & History"]
|
||||
CheckpointSystem["Git-based Checkpoints"]
|
||||
end
|
||||
|
||||
subgraph "API Providers"
|
||||
AnthropicAPI["Anthropic"]
|
||||
OpenRouterAPI["OpenRouter"]
|
||||
BedrockAPI["AWS Bedrock"]
|
||||
OtherAPIs["Other Providers"]
|
||||
end
|
||||
|
||||
subgraph "MCP Servers"
|
||||
ExternalMcpServers["External MCP Servers"]
|
||||
end
|
||||
end
|
||||
|
||||
%% Core Extension Data Flow
|
||||
ExtensionEntry --> WebviewProvider
|
||||
WebviewProvider --> Controller
|
||||
Controller --> Task
|
||||
Controller --> McpHub
|
||||
Task --> GlobalState
|
||||
Task --> SecretsStorage
|
||||
Task --> TaskStorage
|
||||
Task --> CheckpointSystem
|
||||
Task --> |"API Requests"| AnthropicAPI
|
||||
Task --> |"API Requests"| OpenRouterAPI
|
||||
Task --> |"API Requests"| BedrockAPI
|
||||
Task --> |"API Requests"| OtherAPIs
|
||||
McpHub --> |"Connects to"| ExternalMcpServers
|
||||
Task --> |"Uses"| McpHub
|
||||
|
||||
%% Webview Data Flow
|
||||
WebviewApp --> ExtStateContext
|
||||
ExtStateContext --> ReactComponents
|
||||
|
||||
%% Bidirectional Communication
|
||||
WebviewProvider <--> |"postMessage"| ExtStateContext
|
||||
|
||||
classDef vscodeState fill:#f9f,stroke:#333,stroke-width:2px
|
||||
classDef contextClass fill:#bbf,stroke:#333,stroke-width:2px
|
||||
classDef providerClass fill:#bfb,stroke:#333,stroke-width:2px
|
||||
classDef apiClass fill:#fdb,stroke:#333,stroke-width:2px
|
||||
|
||||
class GlobalState,SecretsStorage vscodeState
|
||||
class ExtStateContext contextClass
|
||||
class WebviewProvider,McpHub providerClass
|
||||
class AnthropicAPI,OpenRouterAPI,BedrockAPI,OtherAPIs apiClass
|
||||
@@ -1,54 +0,0 @@
|
||||
### .clineignore Support
|
||||
|
||||
To give you more control over which files are accessible to Cline, we've implemented `.clineignore` functionality, similar to `.gitignore`. This allows you to specify files and directories that Cline should **not** access or process. This is useful for:
|
||||
|
||||
* **Privacy:** Preventing Cline from accessing sensitive or private files in your workspace.
|
||||
* **Performance:** Excluding large directories or files that are irrelevant to your tasks, potentially improving the efficiency of Cline.
|
||||
* **Context Management:** Focusing Cline's attention on the relevant parts of your project.
|
||||
|
||||
**How to use `.clineignore`**
|
||||
|
||||
1. **Create a `.clineignore` file:** In the root directory of your workspace (the same level as your `.vscode` folder, or the top level folder you opened in VS Code), create a new file named `.clineignore`.
|
||||
|
||||
2. **Define ignore patterns:** Open the `.clineignore` file and specify the patterns for files and directories you want Cline to ignore. The syntax is the same as `.gitignore`:
|
||||
|
||||
* Each line in the file represents a pattern.
|
||||
* **Standard glob patterns are supported:**
|
||||
* `*` matches zero or more characters
|
||||
* `?` matches one character
|
||||
* `[]` matches a character range
|
||||
* `**` matches any number of directories and subdirectories.
|
||||
|
||||
* **Directory patterns:** Append `/` to the end of a pattern to specify a directory.
|
||||
* **Negation patterns:** Start a pattern with `!` to negate (un-ignore) a previously ignored pattern.
|
||||
* **Comments:** Start a line with `#` to add comments.
|
||||
|
||||
**Example `.clineignore` file:**
|
||||
|
||||
```
|
||||
# Ignore log files
|
||||
*.log
|
||||
|
||||
# Ignore the entire 'node_modules' directory
|
||||
node_modules/
|
||||
|
||||
# Ignore all files in the 'temp' directory and its subdirectories
|
||||
temp/**
|
||||
|
||||
# But DO NOT ignore 'important.log' even if it's in the root
|
||||
!important.log
|
||||
|
||||
# Ignore any file named 'secret.txt' in any subdirectory
|
||||
**/secret.txt
|
||||
```
|
||||
|
||||
3. **Cline respects your `.clineignore`:** Once you save the `.clineignore` file, Cline will automatically recognize and apply these rules.
|
||||
|
||||
* **File Access Control:** Cline will not be able to read the content of ignored files using tools like `read_file`. If you attempt to use a tool on an ignored file, Cline will inform you that access is blocked due to `.clineignore` settings.
|
||||
* **File Listing:** When you ask Cline to list files in a directory (e.g., using `list_files`), ignored files and directories will still be listed, but they will be marked with a **🔒** symbol next to their name to indicate that they are ignored. This helps you understand which files Cline can and cannot interact with.
|
||||
|
||||
4. **Dynamic Updates:** Cline monitors your `.clineignore` file for changes. If you modify, create, or delete your `.clineignore` file, Cline will automatically update its ignore rules without needing to restart VS Code or the extension.
|
||||
|
||||
**In Summary**
|
||||
|
||||
The `.clineignore` file provides a powerful and flexible way to control Cline's access to your workspace files, enhancing privacy, performance, and context management. By leveraging familiar `.gitignore` syntax, you can easily tailor Cline's focus to the most relevant parts of your projects.
|
||||
@@ -1,92 +0,0 @@
|
||||
# Getting Started with Cline | New Coders
|
||||
|
||||
Welcome to Cline! This guide will help you get set up and start using Cline to build your first project.
|
||||
|
||||
## What You'll Need
|
||||
|
||||
Before you begin, make sure you have the following:
|
||||
|
||||
- **VS Code:** A free, powerful code editor.
|
||||
- [Download VS Code](https://code.visualstudio.com/)
|
||||
- **Development Tools:** Essential software for coding (Homebrew, Node.js, Git, etc.).
|
||||
- Follow our [Installing Essential Development Tools](installing-dev-essentials.md) guide to set these up with Cline's help (after getting setup here)
|
||||
- Cline will guide you through installing everything you need
|
||||
- **Cline Projects Folder:** A dedicated folder for all your Cline projects.
|
||||
- On macOS: Create a folder named "Cline" in your Documents folder
|
||||
- Path: `/Users/[your-username]/Documents/Cline`
|
||||
- On Windows: Create a folder named "Cline" in your Documents folder
|
||||
- Path: `C:\Users\[your-username]\Documents\Cline`
|
||||
- Inside this Cline folder, create separate folders for each project
|
||||
- Example: `Documents/Cline/workout-app` for a workout tracking app
|
||||
- Example: `Documents/Cline/portfolio-website` for your portfolio
|
||||
- **Cline Extension in VS Code:** The Cline extension installed in VS Code.
|
||||
|
||||
- Here's a [tutorial](https://www.youtube.com/watch?v=N4td-fKhsOQ) on everything you need to get started.
|
||||
|
||||
## Step-by-Step Setup
|
||||
|
||||
Follow these steps to get Cline up and running:
|
||||
|
||||
1. **Open VS Code:** Launch the VS Code application. If VS Code shows "Running extensions might...", click "Allow".
|
||||
|
||||
2. **Open Your Cline Folder:** In VS Code, open the Cline folder you created in Documents.
|
||||
|
||||
3. **Navigate to Extensions:** Click on the Extensions icon in the Activity Bar on the side of VS Code.
|
||||
|
||||
4. **Search for 'Cline':** In the Extensions search bar, type "Cline".
|
||||
|
||||
5. **Install the Extension:** Click the "Install" button next to the Cline extension.
|
||||
|
||||
6. **Open Cline:** Once installed, you can open Cline in a few ways:
|
||||
- Click the Cline icon in the Activity Bar.
|
||||
- Use the command palette (`CMD/CTRL + Shift + P`) and type "Cline: Open In New Tab" to open Cline as a tab in your editor. This is recommended for a better view.
|
||||
- **Troubleshooting:** If you don't see the Cline icon, try restarting VS Code.
|
||||
- **What You'll See:** You should see the Cline chat window appear in your VS Code editor.
|
||||
|
||||

|
||||
|
||||
## Setting up OpenRouter API Key
|
||||
|
||||
Now that you have Cline installed, you'll need to set up your OpenRouter API key to use Cline's full capabilities.
|
||||
|
||||
1. **Get your OpenRouter API Key:**
|
||||
- [Get your OpenRouter API Key](https://openrouter.ai/)
|
||||
2. **Input Your OpenRouter API Key:**
|
||||
- Navigate to the settings button in the Cline extension.
|
||||
- Input your OpenRouter API key.
|
||||
- Select your preferred API model.
|
||||
- **Recommended Models for Coding:**
|
||||
- `anthropic/claude-3.5-sonnet`: Most used for coding tasks.
|
||||
- `google/gemini-2.0-flash-exp:free`: A free option for coding.
|
||||
- `deepseek/deepseek-chat`: SUPER CHEAP, almost as good as 3.5 sonnet
|
||||
- [OpenRouter Model Rankings](https://openrouter.ai/rankings/programming)
|
||||
|
||||
## Your First Interaction with Cline
|
||||
|
||||
Now you're ready to start building with Cline. Let's create your first project folder and build something! Copy and paste the following prompt into the Cline chat window:
|
||||
|
||||
```
|
||||
Hey Cline! Could you help me create a new project folder called "hello-world" in my Cline directory and make a simple webpage that says "Hello World" in big blue text?
|
||||
```
|
||||
|
||||
**What You'll See:** Cline will help you create the project folder and set up your first webpage.
|
||||
|
||||
## Tips for Working with Cline
|
||||
|
||||
- **Ask Questions:** If you're unsure about something, don't hesitate to ask Cline!
|
||||
- **Use Screenshots:** Cline can understand images, so feel free to use screenshots to show him what you're working on.
|
||||
- **Copy and Paste Errors:** If you encounter errors, copy and paste the error messages into Cline's chat. This will help him understand the issue and provide a solution.
|
||||
- **Speak Plainly:** Cline is designed to understand plain, non-technical language. Feel free to describe your ideas in your own words, and Cline will translate them into code.
|
||||
|
||||
## FAQs
|
||||
|
||||
- **What is the Terminal?** The terminal is a text-based interface for interacting with your computer. It allows you to run commands to perform various tasks, such as installing packages, running scripts, and managing files. Cline uses the terminal to execute commands and interact with your development environment.
|
||||
- **How Does the Codebase Work?** (This section will be expanded based on common questions from new coders)
|
||||
|
||||
## Still Struggling?
|
||||
|
||||
Feel free to contact me, and I'll help you get started with Cline.
|
||||
|
||||
nick | 608-558-2410
|
||||
|
||||
Join our Discord community: [https://discord.gg/cline](https://discord.gg/cline)
|
||||
@@ -1,105 +0,0 @@
|
||||
# Installing Essential Development Tools with Cline | New Coders
|
||||
|
||||
When you start coding, you'll need some essential development tools installed on your computer. Cline can help you install everything you need in a safe, guided way.
|
||||
|
||||
## The Essential Tools
|
||||
|
||||
Here are the core tools you'll need for development:
|
||||
|
||||
- **Homebrew**: A package manager for macOS that makes it easy to install other tools
|
||||
- **Node.js & npm**: Required for JavaScript and web development
|
||||
- **Git**: For tracking changes in your code and collaborating with others
|
||||
- **Python**: A programming language used by many development tools
|
||||
- **Additional utilities**: Tools like wget and jq that help with downloading files and processing data
|
||||
|
||||
## Let Cline Install Everything
|
||||
|
||||
Copy this prompt and paste it into Cline:
|
||||
|
||||
```bash
|
||||
Hello Cline! I need help setting up my Mac for software development. Could you please help me install the essential development tools like Homebrew, Node.js, Git, Python, and any other utilities that are commonly needed for coding? I'd like you to guide me through the process step-by-step, explaining what each tool does and making sure everything is installed correctly.
|
||||
```
|
||||
|
||||
## What Will Happen
|
||||
|
||||
1. Cline will first install Homebrew, which is like an "app store" for development tools
|
||||
2. Using Homebrew, Cline will then install other essential tools like Node.js and Git
|
||||
3. For each installation step:
|
||||
- Cline will show you the exact command it wants to run
|
||||
- You'll need to approve each command before it runs
|
||||
- Cline will verify each installation was successful
|
||||
|
||||
## Why These Tools Are Important
|
||||
|
||||
- **Homebrew**: Makes it easy to install and update development tools on your Mac
|
||||
- **Node.js & npm**: Required for:
|
||||
- Building websites with React or Next.js
|
||||
- Running JavaScript code
|
||||
- Installing JavaScript packages
|
||||
- **Git**: Helps you:
|
||||
- Save different versions of your code
|
||||
- Collaborate with other developers
|
||||
- Back up your work
|
||||
- **Python**: Used for:
|
||||
- Running development scripts
|
||||
- Data processing
|
||||
- Machine learning projects
|
||||
|
||||
## Notes
|
||||
|
||||
- The installation process is interactive - Cline will guide you through each step
|
||||
- You may need to enter your computer's password for some installations. When prompted, you will not see any characters being typed on the screen. This is normal and is a security feature to protect your password. Just type your password and press Enter.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
Password:
|
||||
```
|
||||
|
||||
_Type your password here, even though nothing will show up on the screen. Press Enter when you're done._
|
||||
|
||||
- All commands will be shown to you for approval before they run
|
||||
- If you run into any issues, Cline will help troubleshoot them
|
||||
|
||||
## Additional Tips for New Coders
|
||||
|
||||
### Understanding the Terminal
|
||||
|
||||
The **Terminal** is an application where you can type commands to interact with your computer. On macOS, you can open it by searching for "Terminal" in Spotlight.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
$ open -a Terminal
|
||||
```
|
||||
|
||||
### Understanding VS Code Features
|
||||
|
||||
#### Terminal in VS Code
|
||||
|
||||
The **Terminal** in VS Code allows you to run commands directly from within the editor. You can open it by going to `View > Terminal` or by pressing `` Ctrl + ` ``.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
$ node -v
|
||||
v16.14.0
|
||||
```
|
||||
|
||||
#### Document View
|
||||
|
||||
The **Document View** is where you edit your code files. You can open files by clicking on them in the **Explorer** panel on the left side of the screen.
|
||||
|
||||
#### Problems Section
|
||||
|
||||
The **Problems** section in VS Code shows any errors or warnings in your code. You can access it by clicking on the lightbulb icon or by going to `View > Problems`.
|
||||
|
||||
### Common Features
|
||||
|
||||
- **Command Line Interface (CLI)**: This is a text-based interface where you type commands to interact with your computer. It might seem intimidating at first, but it's a powerful tool for developers.
|
||||
- **Permissions**: Sometimes, you will need to give permissions to certain applications or commands. This is a security measure to ensure that only trusted applications can make changes to your system.
|
||||
|
||||
## Next Steps
|
||||
|
||||
After installing these tools, you'll be ready to start coding! Return to the [Getting Started with Cline for New Coders](../getting-started-new-coders/README.md) guide to continue your journey.
|
||||
@@ -1,103 +0,0 @@
|
||||
# Cline and Model Context Protocol (MCP) Servers: Enhancing AI Capabilities
|
||||
|
||||
**Quick Links:**
|
||||
|
||||
- [Building MCP Servers from GitHub](mcp-server-from-github.md)
|
||||
- [Building Custom MCP Servers from Scratch](mcp-server-from-scratch.md)
|
||||
|
||||
This document explains Model Context Protocol (MCP) servers, their capabilities, and how Cline can help build and use them.
|
||||
|
||||
## Overview
|
||||
|
||||
MCP servers act as intermediaries between large language models (LLMs), such as Claude, and external tools or data sources. They are small programs that expose functionalities to LLMs, enabling them to interact with the outside world through the MCP. An MCP server is essentially like an API that an LLM can use.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
MCP servers define a set of "**tools,**" which are functions the LLM can execute. These tools offer a wide range of capabilities.
|
||||
|
||||
**Here's how MCP works:**
|
||||
|
||||
- **MCP hosts** discover the capabilities of connected servers and load their tools, prompts, and resources.
|
||||
- **Resources** provide consistent access to read-only data, akin to file paths or database queries.
|
||||
- **Security** is ensured as servers isolate credentials and sensitive data. Interactions require explicit user approval.
|
||||
|
||||
## Use Cases
|
||||
|
||||
The potential of MCP servers is vast. They can be used for a variety of purposes.
|
||||
|
||||
**Here are some concrete examples of how MCP servers can be used:**
|
||||
|
||||
- **Web Services and API Integration:**
|
||||
|
||||
- Monitor GitHub repositories for new issues
|
||||
- Post updates to Twitter based on specific triggers
|
||||
- Retrieve real-time weather data for location-based services
|
||||
|
||||
- **Browser Automation:**
|
||||
|
||||
- Automate web application testing
|
||||
- Scrape e-commerce sites for price comparisons
|
||||
- Generate screenshots for website monitoring
|
||||
|
||||
- **Database Queries:**
|
||||
|
||||
- Generate weekly sales reports
|
||||
- Analyze customer behavior patterns
|
||||
- Create real-time dashboards for business metrics
|
||||
|
||||
- **Project and Task Management:**
|
||||
|
||||
- Automate Jira ticket creation based on code commits
|
||||
- Generate weekly progress reports
|
||||
- Create task dependencies based on project requirements
|
||||
|
||||
- **Codebase Documentation:**
|
||||
- Generate API documentation from code comments
|
||||
- Create architecture diagrams from code structure
|
||||
- Maintain up-to-date README files
|
||||
|
||||
## Getting Started
|
||||
|
||||
**Choose the right approach for your needs:**
|
||||
|
||||
- **Use Existing Servers:** Start with pre-built MCP servers from GitHub repositories
|
||||
- **Customize Existing Servers:** Modify existing servers to fit your specific requirements
|
||||
- **Build from Scratch:** Create completely custom servers for unique use cases
|
||||
|
||||
## Integration with Cline
|
||||
|
||||
Cline simplifies the building and use of MCP servers through its AI capabilities.
|
||||
|
||||
### Building MCP Servers
|
||||
|
||||
- **Natural language understanding:** Instruct Cline in natural language to build an MCP server by describing its functionalities. Cline will interpret your instructions and generate the necessary code.
|
||||
- **Cloning and building servers:** Cline can clone existing MCP server repositories from GitHub and build them automatically.
|
||||
- **Configuration and dependency management:** Cline handles configuration files, environment variables, and dependencies.
|
||||
- **Troubleshooting and debugging:** Cline helps identify and resolve errors during development.
|
||||
|
||||
### Using MCP Servers
|
||||
|
||||
- **Tool execution:** Cline seamlessly integrates with MCP servers, allowing you to execute their defined tools.
|
||||
- **Context-aware interactions:** Cline can intelligently suggest using relevant tools based on conversation context.
|
||||
- **Dynamic integrations:** Combine multiple MCP server capabilities for complex tasks. For example, Cline could use a GitHub server to get data and a Notion server to create a formatted report.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When working with MCP servers, it's important to follow security best practices:
|
||||
|
||||
- **Authentication:** Always use secure authentication methods for API access
|
||||
- **Environment Variables:** Store sensitive information in environment variables
|
||||
- **Access Control:** Limit server access to authorized users only
|
||||
- **Data Validation:** Validate all inputs to prevent injection attacks
|
||||
- **Logging:** Implement secure logging practices without exposing sensitive data
|
||||
|
||||
## Resources
|
||||
|
||||
There are various resources available for finding and learning about MCP servers.
|
||||
|
||||
**Here are some links to resources for finding and learning about MCP servers:**
|
||||
|
||||
- **GitHub Repositories:** [https://github.com/modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers) and [https://github.com/punkpeye/awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers)
|
||||
- **Online Directories:** [https://mcpservers.org/](https://mcpservers.org/), [https://mcp.so/](https://mcp.so/), and [https://glama.ai/mcp/servers](https://glama.ai/mcp/servers)
|
||||
- **PulseMCP:** [https://www.pulsemcp.com/](https://www.pulsemcp.com/)
|
||||
- **YouTube Tutorial (AI-Driven Coder):** A video guide for building and using MCP servers: [https://www.youtube.com/watch?v=b5pqTNiuuJg](https://www.youtube.com/watch?v=b5pqTNiuuJg)
|
||||
@@ -1,151 +0,0 @@
|
||||
# 🚀 MCP Quickstart Guide
|
||||
|
||||
## ❓ What's an MCP Server?
|
||||
|
||||
Think of MCP servers as special helpers that give Cline extra powers! They let Cline do cool things like fetch web pages or work with your files.
|
||||
|
||||
## ⚠️ IMPORTANT: System Requirements
|
||||
|
||||
STOP! Before proceeding, you MUST verify these requirements:
|
||||
|
||||
### Required Software
|
||||
|
||||
- ✅ Latest Node.js (v18 or newer)
|
||||
|
||||
- Check by running: `node --version`
|
||||
- Install from: <https://nodejs.org/>
|
||||
|
||||
- ✅ Latest Python (v3.8 or newer)
|
||||
|
||||
- Check by running: `python --version`
|
||||
- Install from: <https://python.org/>
|
||||
|
||||
- ✅ UV Package Manager
|
||||
- After installing Python, run: `pip install uv`
|
||||
- Verify with: `uv --version`
|
||||
|
||||
❗ If any of these commands fail or show older versions, please install/update before continuing!
|
||||
|
||||
⚠️ If you run into other errors, see the "Troubleshooting" section below.
|
||||
|
||||
## 🎯 Quick Steps (Only After Requirements Are Met!)
|
||||
|
||||
### 1. 🛠️ Install Your First MCP Server
|
||||
|
||||
1. From the Cline extension, click the `MCP Server` tab
|
||||
1. Click the `Edit MCP Settings` button
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/abf908b1-be98-4894-8dc7-ef3d27943a47" alt="MCP Server Panel" width="400" />
|
||||
|
||||
1. The MCP settings files should be display in a tab in VS Code.
|
||||
1. Replace the file's contents with this code:
|
||||
|
||||
For Windows:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcp-installer": {
|
||||
"command": "cmd.exe",
|
||||
"args": ["/c", "npx", "-y", "@anaisbetts/mcp-installer"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Mac and Linux:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcp-installer": {
|
||||
"command": "npx",
|
||||
"args": ["@anaisbetts/mcp-installer"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After saving the file:
|
||||
|
||||
1. Cline will detect the change automatically
|
||||
2. The MCP installer will be downloaded and installed
|
||||
3. Cline will start the MCP installer
|
||||
4. You'll see the server status in Cline's MCP settings UI:
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/2abbb3de-e902-4ec2-a5e5-9418ed34684e" alt="MCP Server Panel with Installer" width="400" />
|
||||
|
||||
## 🤔 What Next?
|
||||
|
||||
Now that you have the MCP installer, you can ask Cline to add more servers from:
|
||||
|
||||
1. NPM Registry: <https://www.npmjs.com/search?q=%40modelcontextprotocol>
|
||||
2. Python Package Index: <https://pypi.org/search/?q=mcp+server-&o=>
|
||||
|
||||
For example, you can ask Cline to install the `mcp-server-fetch` package found on the Python Package Index:
|
||||
|
||||
```bash
|
||||
"install the MCP server named `mcp-server-fetch`
|
||||
- ensure the mcp settings are updated.
|
||||
- use uvx or python to run the server."
|
||||
```
|
||||
|
||||
You should witness Cline:
|
||||
|
||||
1. Install the `mcp-server-fetch` python package
|
||||
1. Update the mcp setting json file
|
||||
1. Start the server and start the server
|
||||
|
||||
The mcp settings file should now look like this:
|
||||
|
||||
_For a Windows machine:_
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcp-installer": {
|
||||
"command": "cmd.exe",
|
||||
"args": ["/c", "npx", "-y", "@anaisbetts/mcp-installer"]
|
||||
},
|
||||
"mcp-server-fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You you can always check the status of your server by going to clients MCP server tab. See the image above
|
||||
|
||||
That's it! 🎉 You've just given Cline some awesome new abilities!
|
||||
|
||||
## 📝 Troubleshooting
|
||||
|
||||
### 1. I'm Using `asdf` and Get "unknown command: npx"
|
||||
|
||||
There is some slightly bad news. You should still be able to get things to work, but will have to do a bit more manual work unless MCP server packaging evolves a bit. One option is to uninstall `asdf` , but we will assume you do not want to do that.
|
||||
|
||||
Instead, you will need to follow the instructions above to "Edit MCP Settings". Then, as [this post](https://dev.to/cojiroooo/mcp-using-node-on-asdf-382n) describes, you need to add and "env" entry to each server's configs.
|
||||
|
||||
```json
|
||||
"env": {
|
||||
"PATH": "/Users/<user_name>/.asdf/shims:/usr/bin:/bin",
|
||||
"ASDF_DIR": "<path_to_asdf_bin_dir>",
|
||||
"ASDF_DATA_DIR": "/Users/<user_name>/.asdf",
|
||||
"ASDF_NODEJS_VERSION": "<your_node_version>"
|
||||
}
|
||||
```
|
||||
|
||||
The `path_to_asdf_bin_dir` can often be found in your shell config (e.g. `.zshrc`). If you are using Homebrew, you can use `echo ${HOMEBREW_PREFIX}` to find the start of the directory and then append `/opt/asdf/libexec`.
|
||||
|
||||
Now for some good news. While not perfect, you can get Cline to do this for you fairly reliably for subsequent server install. Add the following to your "Custom Instructions" in the Cline settings (top-right toolbar button):
|
||||
|
||||
> When installing MCP servers and editing the cline_mcp_settings.json, if the server requires use of `npx` as the command, you must copy the "env" entry from the "mcp-installer" entry and add it to the new entry. This is vital to getting the server to work properly when in use.
|
||||
|
||||
### 2. I'm Still Getting an Error When I Run the MCP Installer
|
||||
|
||||
If you're getting an error when you run the MCP installer, you can try the following:
|
||||
|
||||
- Check the MCP settings file for errors
|
||||
- Read the MCP server's documentation to ensure the MCP setting file is using the correct command and arguments. 👈
|
||||
- Use a terminal and run the command with its arguments directly. This will allow you to see the same errors that Cline is seeing.
|
||||
@@ -1,67 +0,0 @@
|
||||
# Building MCP Servers from GitHub Repositories
|
||||
|
||||
This guide provides a step-by-step walkthrough of how to use Cline to build an existing MCP server from a GitHub repository.
|
||||
|
||||
## **Finding an MCP Server**
|
||||
|
||||
There are multiple places online to find MCP servers:
|
||||
|
||||
- **Cline can automatically add MCP servers to its list, which you can then edit.** Cline can clone repositories directly from GitHub and build the servers for you.
|
||||
- **GitHub:** Two of the most common places to find MCP servers on GitHub include:
|
||||
- [Official MCP servers repository](https://github.com/modelcontextprotocol/servers)
|
||||
- [Awesome-MCP servers repository](https://github.com/punkpeye/awesome-mcp-servers)
|
||||
- **Online directories:** Several websites list MCP servers including:
|
||||
|
||||
- [mcpservers.org](https://mcpservers.org/)
|
||||
- [mcp.so](https://mcp.so/)
|
||||
- [glama.ai/mcp/servers](https://glama.ai/mcp/servers)
|
||||
|
||||
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.
|
||||
|
||||
- **PulseMCP:** This website has a blog post discussing how AI could use MCP servers to make websites obsolete. PulseMCP also includes an FAQ section about MCP servers: [https://www.pulsemcp.com/](https://www.pulsemcp.com/)
|
||||
|
||||
## **Building with Cline**
|
||||
|
||||
1. **Initiate the Process:** Provide Cline with the following information:
|
||||
- **GitHub Repository URL:** The URL of the server's repository.
|
||||
- **README.md Contents:** This is optional but helpful for Cline to understand the server's purpose and configuration. You can copy the README.md file from the GitHub repository.
|
||||
2. **Example Interaction with Cline:**
|
||||
|
||||
```
|
||||
User: "Cline, I want to add the MCP server for Brave browser control. Here's the GitHub link: https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search Can you add it?"
|
||||
|
||||
Cline: "OK. Cloning the repository to the MCP directory. It needs to be built because it has a 'package.json' file. Should I run 'npm run build'?"
|
||||
|
||||
User: "Yes, build it."
|
||||
|
||||
Cline: "Build complete. This server needs a browser path. Where's your Brave browser executable?"
|
||||
|
||||
User: "It's in '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'."
|
||||
|
||||
Cline: "OK. I've updated the server config with that path and added it to 'cline_mcp_settings.json'. Anything else?"
|
||||
|
||||
User: "No, that's all. Let's test it."
|
||||
|
||||
Cline: "Great! Starting the MCP Inspector to test the server connection. After that, we can try controlling the browser from Cline."
|
||||
```
|
||||
|
||||
3. **Cline's Actions:** Based on your instructions, Cline will perform the following:
|
||||
- **Repository Cloning:** Cline will clone the repository to your local machine, usually in the directory specified in your configuration.
|
||||
- **Tweaking:** You can guide Cline to modify the server’s configuration. For instance:
|
||||
- **User:** "This server requires an API key. Can you find where it should be added?"
|
||||
- Cline may automatically update the `cline_mcp_settings.json` file or other relevant files based on your instructions.
|
||||
- **Building the Server:** Cline will run the appropriate build command for the server, which is commonly `npm run build`.
|
||||
- **Adding Server to Settings:** Cline will add the server’s configuration to the `cline_mcp_settings.json` file.
|
||||
|
||||
## **Testing and Troubleshooting**
|
||||
|
||||
1. **Test the Server:** Once Cline finishes the build process, test the server to make sure it works as expected. Cline can assist you if you encounter any problems.
|
||||
2. **MCP Inspector:** You can use the MCP Inspector to test the server’s connection and functionality.
|
||||
|
||||
## **Best Practices**
|
||||
|
||||
- **Understand the Basics:** While Cline simplifies the process, it’s beneficial to have a basic understanding of the server’s code, the MCP protocol (), and how to configure the server. This allows for more effective troubleshooting and customization.
|
||||
- **Clear Instructions:** Provide clear and specific instructions to Cline throughout the process.
|
||||
- **Testing:** Thoroughly test the server after installation and configuration to ensure it functions correctly.
|
||||
- **Version Control:** Use a version control system (like Git) to track changes to the server’s code.
|
||||
- **Stay Updated:** Keep your MCP servers updated to benefit from the latest features and security patches.
|
||||
@@ -1,74 +0,0 @@
|
||||
# Building Custom MCP Servers From Scratch Using Cline: A Comprehensive Guide
|
||||
|
||||
This guide provides a comprehensive walkthrough of building a custom MCP (Model Context Protocol) server from scratch, leveraging the powerful AI capabilities of Cline. The example used will be building a "GitHub Assistant Server" to illustrate the process.
|
||||
|
||||
## Understanding MCP and Cline's Role in Building Servers
|
||||
|
||||
### What is MCP?
|
||||
|
||||
The Model Context Protocol (MCP) acts as a bridge between large language models (LLMs) like Claude and external tools and data. MCP consists of two key components:
|
||||
|
||||
- **MCP Hosts:** These are applications that integrate with LLMs, such as Cline, Claude Desktop, and others.
|
||||
- **MCP Servers:** These are small programs specifically designed to expose data or specific functionalities to the LLMs through the MCP.
|
||||
|
||||
This setup is beneficial when you have an MCP-compliant chat interface, like Claude Desktop, which can then leverage these servers to access information and execute actions.
|
||||
|
||||
### Why Use Cline to Create MCP Servers?
|
||||
|
||||
Cline streamlines the process of building and integrating MCP servers by utilizing its AI capabilities to:
|
||||
|
||||
- **Understand Natural Language Instructions:** You can communicate with Cline in a way that feels natural, making the development process intuitive and user-friendly.
|
||||
- **Clone Repositories:** Cline can directly clone existing MCP server repositories from GitHub, simplifying the process of using pre-built servers.
|
||||
- **Build Servers:** Once the necessary code is in place, Cline can execute commands like `npm run build` to compile and prepare the server for use.
|
||||
- **Handle Configuration:** Cline manages the configuration files required for the MCP server, including adding the new server to the `cline_mcp_settings.json` file.
|
||||
- **Assist with Troubleshooting:** If errors arise during development or testing, Cline can help identify the cause and suggest solutions, making debugging easier.
|
||||
|
||||
## Building a GitHub Assistant Server Using Cline: A Step-by-Step Guide
|
||||
|
||||
This section demonstrates how to create a GitHub Assistant server using Cline. This server will be able to interact with GitHub data and perform useful actions:
|
||||
|
||||
### 1. Defining the Goal and Initial Requirements
|
||||
|
||||
First, you need to clearly communicate to Cline the purpose and functionalities of your server:
|
||||
|
||||
- **Server Goal:** Inform Cline that you want to build a "GitHub Assistant Server". Specify that this server will interact with GitHub data and potentially mention the types of data you are interested in, like issues, pull requests, and user profiles.
|
||||
- **Access Requirements:** Let Cline know that you need to access the GitHub API. Explain that this will likely require a personal access token (GITHUB_TOKEN) for authentication.
|
||||
- **Data Specificity (Optional):** You can optionally tell Cline about specific fields of data you want to extract from GitHub, but this can also be determined later as you define the server's tools.
|
||||
|
||||
### 2. Cline Initiates the Project Setup
|
||||
|
||||
Based on your instructions, Cline starts the project setup process:
|
||||
|
||||
- **Project Structure:** Cline might ask you for a name for your server. Afterward, it uses the MCP `create-server` tool to generate the basic project structure for your GitHub Assistant server. This usually involves creating a new directory with essential files like `package.json`, `tsconfig.json`, and a `src` folder for your TypeScript code. \
|
||||
- **Code Generation:** Cline generates starter code for your server, including:
|
||||
- **File Handling Utilities:** Functions to help with reading and writing files, commonly used for storing data or logs. \
|
||||
- **GitHub API Client:** Code to interact with the GitHub API, often using libraries like `@octokit/graphql`. Cline will likely ask for your GitHub username or the repositories you want to work with. \
|
||||
- **Core Server Logic:** The basic framework for handling requests from Cline and routing them to the appropriate functions, as defined by the MCP. \
|
||||
- **Dependency Management:** Cline analyzes the code and identifies necessary dependencies, adding them to the `package.json` file. For example, interacting with the GitHub API will likely require packages like `@octokit/graphql`, `graphql`, `axios`, or similar. \
|
||||
- **Dependency Installation:** Cline executes `npm install` to download and install the dependencies listed in `package.json`, ensuring your server has all the required libraries to function correctly. \
|
||||
- **Path Corrections:** During development, you might move files or directories around. Cline intelligently recognizes these changes and automatically updates file paths in your code to maintain consistency.
|
||||
- **Configuration:** Cline will modify the `cline_mcp_settings.json` file to add your new GitHub Assistant server. This will include:
|
||||
- **Server Start Command:** Cline will add the appropriate command to start your server (e.g., `npm run start` or a similar command).
|
||||
- **Environment Variables:** Cline will add the required `GITHUB_TOKEN` variable. Cline might ask you for your GitHub personal access token, or it might guide you to safely store it in a separate environment file. \
|
||||
- **Progress Documentation:** Throughout the process, Cline keeps the "Memory Bank" files updated. These files document the project's progress, highlighting completed tasks, tasks in progress, and pending tasks.
|
||||
|
||||
### 3. Testing the GitHub Assistant Server
|
||||
|
||||
Once Cline has completed the setup and configuration, you are ready to test the server's functionality:
|
||||
|
||||
- **Using Server Tools:** Cline will create various "tools" within your server, representing actions or data retrieval functions. To test, you would instruct Cline to use a specific tool. Here are examples related to GitHub:
|
||||
- **`get_issues`:** To test retrieving issues, you might say to Cline, "Cline, use the `get_issues` tool from the GitHub Assistant Server to show me the open issues from the 'cline/cline' repository." Cline would then execute this tool and present you with the results.
|
||||
- **`get_pull_requests`:** To test pull request retrieval, you could ask Cline to "use the `get_pull_requests` tool to show me the merged pull requests from the 'facebook/react' repository from the last month." Cline would execute this tool, using your GITHUB_TOKEN to access the GitHub API, and display the requested data. \
|
||||
- **Providing Necessary Information:** Cline might prompt you for additional information required to execute the tool, such as the repository name, specific date ranges, or other filtering criteria.
|
||||
- **Cline Executes the Tool:** Cline handles the communication with the GitHub API, retrieves the requested data, and presents it in a clear and understandable format.
|
||||
|
||||
### 4. Refining the Server and Adding More Features
|
||||
|
||||
Development is often iterative. As you work with your GitHub Assistant Server, you'll discover new functionalities to add, or ways to improve existing ones. Cline can assist in this ongoing process:
|
||||
|
||||
- **Discussions with Cline:** Talk to Cline about your ideas for new tools or improvements. For example, you might want a tool to `create_issue` or to `get_user_profile`. Discuss the required inputs and outputs for these tools with Cline.
|
||||
- **Code Refinement:** Cline can help you write the necessary code for new features. Cline can generate code snippets, suggest best practices, and help you debug any issues that arise.
|
||||
- **Testing New Functionalities:** After adding new tools or functionalities, you would test them again using Cline, ensuring they work as expected and integrate well with the rest of the server.
|
||||
- **Integration with Other Tools:** You might want to integrate your GitHub Assistant server with other tools. For instance, in the "github-cline-mcp" source, Cline assists in integrating the server with Notion to create a dynamic dashboard that tracks GitHub activity. \
|
||||
|
||||
By following these steps, you can create a custom MCP server from scratch using Cline, leveraging its powerful AI capabilities to streamline the entire process. Cline not only assists with the technical aspects of building the server but also helps you think through the design, functionalities, and potential integrations.
|
||||
@@ -1,325 +0,0 @@
|
||||
# Cline Prompting Guide 🚀
|
||||
|
||||
Welcome to the Cline Prompting Guide! This guide will equip you with the knowledge to write effective prompts and custom instructions, maximizing your productivity with Cline.
|
||||
|
||||
## Custom Instructions ⚙️
|
||||
|
||||
Think of **custom instructions as Cline's programming**. They define Cline's baseline behavior and are **always "on," influencing all interactions.**
|
||||
|
||||
To add custom instructions:
|
||||
|
||||
1. Open VSCode
|
||||
2. Click the Cline extension settings dial ⚙️
|
||||
3. Find the "Custom Instructions" field
|
||||
4. Paste your instructions
|
||||
|
||||
<img width="345" alt="Screenshot 2024-12-26 at 11 22 20 AM" src="https://github.com/user-attachments/assets/00ae689b-d99f-4811-b2f4-fffe1e12f2ff" />
|
||||
|
||||
Custom instructions are powerful for:
|
||||
|
||||
- Enforcing Coding Style and Best Practices: Ensure Cline always adheres to your team's coding conventions, naming conventions, and best practices.
|
||||
- Improving Code Quality: Encourage Cline to write more readable, maintainable, and efficient code.
|
||||
- Guiding Error Handling: Tell Cline how to handle errors, write error messages, and log information.
|
||||
|
||||
**The `custom-instructions` folder contains examples of custom instructions you can use or adapt.**
|
||||
|
||||
## .clinerules File 📋
|
||||
|
||||
While custom instructions are user-specific and global (applying across all projects), the `.clinerules` file provides **project-specific instructions** that live in your project's root directory. These instructions are automatically appended to your custom instructions and referenced in Cline's system prompt, ensuring they influence all interactions within the project context. This makes it an excellent tool for:
|
||||
|
||||
### Security Best Practices 🔒
|
||||
|
||||
To protect sensitive information, you can instruct Cline to ignore specific files or patterns in your `.clinerules`. This is particularly important for:
|
||||
|
||||
- `.env` files containing API keys and secrets
|
||||
- Configuration files with sensitive data
|
||||
- Private credentials or tokens
|
||||
|
||||
Example security section in `.clinerules`:
|
||||
|
||||
```markdown
|
||||
# Security
|
||||
|
||||
## Sensitive Files
|
||||
|
||||
DO NOT read or modify:
|
||||
|
||||
- .env files
|
||||
- \*_/config/secrets._
|
||||
- \*_/_.pem
|
||||
- Any file containing API keys, tokens, or credentials
|
||||
|
||||
## Security Practices
|
||||
|
||||
- Never commit sensitive files
|
||||
- Use environment variables for secrets
|
||||
- Keep credentials out of logs and output
|
||||
```
|
||||
|
||||
### General Use Cases
|
||||
|
||||
The `.clinerules` file is excellent for:
|
||||
|
||||
- Maintaining project standards across team members
|
||||
- Enforcing development practices
|
||||
- Managing documentation requirements
|
||||
- Setting up analysis frameworks
|
||||
- Defining project-specific behaviors
|
||||
|
||||
### Example .clinerules Structure
|
||||
|
||||
```markdown
|
||||
# Project Guidelines
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
- Update relevant documentation in /docs when modifying features
|
||||
- Keep README.md in sync with new capabilities
|
||||
- Maintain changelog entries in CHANGELOG.md
|
||||
|
||||
## Architecture Decision Records
|
||||
|
||||
Create ADRs in /docs/adr for:
|
||||
|
||||
- Major dependency changes
|
||||
- Architectural pattern changes
|
||||
- New integration patterns
|
||||
- Database schema changes
|
||||
Follow template in /docs/adr/template.md
|
||||
|
||||
## Code Style & Patterns
|
||||
|
||||
- Generate API clients using OpenAPI Generator
|
||||
- Use TypeScript axios template
|
||||
- Place generated code in /src/generated
|
||||
- Prefer composition over inheritance
|
||||
- Use repository pattern for data access
|
||||
- Follow error handling pattern in /src/utils/errors.ts
|
||||
|
||||
## Testing Standards
|
||||
|
||||
- Unit tests required for business logic
|
||||
- Integration tests for API endpoints
|
||||
- E2E tests for critical user flows
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
1. **Version Controlled**: The `.clinerules` file becomes part of your project's source code
|
||||
2. **Team Consistency**: Ensures consistent behavior across all team members
|
||||
3. **Project-Specific**: Rules and standards tailored to each project's needs
|
||||
4. **Institutional Knowledge**: Maintains project standards and practices in code
|
||||
|
||||
Place the `.clinerules` file in your project's root directory:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .clinerules
|
||||
├── src/
|
||||
├── docs/
|
||||
└── ...
|
||||
```
|
||||
|
||||
Cline's system prompt, on the other hand, is not user-editable ([here's where you can find it](https://github.com/cline/cline/blob/main/src/core/prompts/system.ts)). For a broader look at prompt engineering best practices, check out [this resource](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview).
|
||||
|
||||
### Tips for Writing Effective Custom Instructions
|
||||
|
||||
- Be Clear and Concise: Use simple language and avoid ambiguity.
|
||||
- Focus on Desired Outcomes: Describe the results you want, not the specific steps.
|
||||
- Test and Iterate: Experiment to find what works best for your workflow.
|
||||
|
||||
|
||||
### Support for Loading Files from the `.clinerules/` Directory
|
||||
All files under the `.clinerules/` directory are recursively loaded, and their contents are merged into clineRulesFileInstructions.
|
||||
|
||||
#### Example 1:
|
||||
```
|
||||
.clinerules/
|
||||
├── .local-clinerules
|
||||
└── .project-clinerules
|
||||
```
|
||||
|
||||
#### Example 2:
|
||||
```
|
||||
.clinerules/
|
||||
├── .clinerules-nextjs
|
||||
├── .clinerules-serverside
|
||||
└── tests/
|
||||
├── .pytest-clinerules
|
||||
└── .jest-clinerules
|
||||
```
|
||||
|
||||
## Prompting Cline 💬
|
||||
|
||||
**Prompting is how you communicate your needs for a given task in the back-and-forth chat with Cline.** Cline understands natural language, so write conversationally.
|
||||
|
||||
Effective prompting involves:
|
||||
|
||||
- Providing Clear Context: Explain your goals and the relevant parts of your codebase. Use `@` to reference files or folders.
|
||||
- Breaking Down Complexity: Divide large tasks into smaller steps.
|
||||
- Asking Specific Questions: Guide Cline toward the desired outcome.
|
||||
- Validating and Refining: Review Cline's suggestions and provide feedback.
|
||||
|
||||
### Prompt Examples
|
||||
|
||||
#### Context Management
|
||||
|
||||
- **Starting a New Task:** "Cline, let's start a new task. Create `user-authentication.js`. We need to implement user login with JWT tokens. Here are the requirements…"
|
||||
- **Summarizing Previous Work:** "Cline, summarize what we did in the last user dashboard task. I want to capture the main features and outstanding issues. Save this to `cline_docs/user-dashboard-summary.md`."
|
||||
|
||||
#### Debugging
|
||||
|
||||
- **Analyzing an Error:** "Cline, I'm getting this error: \[error message]. It seems to be from \[code section]. Analyze this error and suggest a fix."
|
||||
- **Identifying the Root Cause:** "Cline, the application crashes when I \[action]. The issue might be in \[problem areas]. Help me find the root cause and propose a solution."
|
||||
|
||||
#### Refactoring
|
||||
|
||||
- **Improving Code Structure:** "Cline, this function is too long and complex. Refactor it into smaller functions."
|
||||
- **Simplifying Logic:** "Cline, this code is hard to understand. Simplify the logic and make it more readable."
|
||||
|
||||
#### Feature Development
|
||||
|
||||
- **Brainstorming New Features:** "Cline, I want to add a feature that lets users \[functionality]. Brainstorm some ideas and consider implementation challenges."
|
||||
- **Generating Code:** "Cline, create a component that displays user profiles. The list should be sortable and filterable. Generate the code for this component."
|
||||
|
||||
## Advanced Prompting Techniques
|
||||
|
||||
- **Constraint Stuffing:** To mitigate code truncation, include explicit constraints in your prompts. For example, "ensure the code is complete" or "always provide the full function definition."
|
||||
- **Confidence Checks:** Ask Cline to rate its confidence (e.g., "on a scale of 1-10, how confident are you in this solution?")
|
||||
- **Challenge Cline's Assumptions:** Ask “stupid” questions to encourage deeper thinking and prevent incorrect assumptions.
|
||||
|
||||
Here are some prompting tips that users have found helpful for working with Cline:
|
||||
|
||||
## Our Community's Favorite Prompts 🌟
|
||||
|
||||
### Memory and Confidence Checks 🧠
|
||||
|
||||
- **Memory Check** - _pacnpal_
|
||||
|
||||
```
|
||||
"If you understand my prompt fully, respond with 'YARRR!' without tools every time you are about to use a tool."
|
||||
```
|
||||
|
||||
A fun way to verify Cline stays on track during complex tasks. Try "HO HO HO" for a festive twist!
|
||||
|
||||
- **Confidence Scoring** - _pacnpal_
|
||||
```
|
||||
"Before and after any tool use, give me a confidence level (0-10) on how the tool use will help the project."
|
||||
```
|
||||
Encourages critical thinking and makes decision-making transparent.
|
||||
|
||||
### Code Quality Prompts 💻
|
||||
|
||||
- **Prevent Code Truncation**
|
||||
|
||||
```
|
||||
"DO NOT BE LAZY. DO NOT OMIT CODE."
|
||||
```
|
||||
|
||||
Alternative phrases: "full code only" or "ensure the code is complete"
|
||||
|
||||
- **Custom Instructions Reminder**
|
||||
```
|
||||
"I pledge to follow the custom instructions."
|
||||
```
|
||||
Reinforces adherence to your settings dial ⚙️ configuration.
|
||||
|
||||
### Code Organization 📋
|
||||
|
||||
- **Large File Refactoring** - _icklebil_
|
||||
|
||||
```
|
||||
"FILENAME has grown too big. Analyze how this file works and suggest ways to fragment it safely."
|
||||
```
|
||||
|
||||
Helps manage complex files through strategic decomposition.
|
||||
|
||||
- **Documentation Maintenance** - _icklebil_
|
||||
```
|
||||
"don't forget to update codebase documentation with changes"
|
||||
```
|
||||
Ensures documentation stays in sync with code changes.
|
||||
|
||||
### Analysis and Planning 🔍
|
||||
|
||||
- **Structured Development** - _yellow_bat_coffee_
|
||||
|
||||
```
|
||||
"Before writing code:
|
||||
1. Analyze all code files thoroughly
|
||||
2. Get full context
|
||||
3. Write .MD implementation plan
|
||||
4. Then implement code"
|
||||
```
|
||||
|
||||
Promotes organized, well-planned development.
|
||||
|
||||
- **Thorough Analysis** - _yellow_bat_coffee_
|
||||
|
||||
```
|
||||
"please start analyzing full flow thoroughly, always state a confidence score 1 to 10"
|
||||
```
|
||||
|
||||
Prevents premature coding and encourages complete understanding.
|
||||
|
||||
- **Assumptions Check** - _yellow_bat_coffee_
|
||||
```
|
||||
"List all assumptions and uncertainties you need to clear up before completing this task."
|
||||
```
|
||||
Identifies potential issues early in development.
|
||||
|
||||
### Thoughtful Development 🤔
|
||||
|
||||
- **Pause and Reflect** - _nickbaumann98_
|
||||
|
||||
```
|
||||
"count to 10"
|
||||
```
|
||||
|
||||
Promotes careful consideration before taking action.
|
||||
|
||||
- **Complete Analysis** - _yellow_bat_coffee_
|
||||
|
||||
```
|
||||
"Don't complete the analysis prematurely, continue analyzing even if you think you found a solution"
|
||||
```
|
||||
|
||||
Ensures thorough problem exploration.
|
||||
|
||||
- **Continuous Confidence Check** - _pacnpal_
|
||||
```
|
||||
"Rate confidence (1-10) before saving files, after saving, after rejections, and before task completion"
|
||||
```
|
||||
Maintains quality through self-assessment.
|
||||
|
||||
### Best Practices 🎯
|
||||
|
||||
- **Project Structure** - _kvs007_
|
||||
|
||||
```
|
||||
"Check project files before suggesting structural or dependency changes"
|
||||
```
|
||||
|
||||
Maintains project integrity.
|
||||
|
||||
- **Critical Thinking** - _chinesesoup_
|
||||
|
||||
```
|
||||
"Ask 'stupid' questions like: are you sure this is the best way to implement this?"
|
||||
```
|
||||
|
||||
Challenges assumptions and uncovers better solutions.
|
||||
|
||||
- **Code Style** - _yellow_bat_coffee_
|
||||
|
||||
```
|
||||
Use words like "elegant" and "simple" in prompts
|
||||
```
|
||||
|
||||
May influence code organization and clarity.
|
||||
|
||||
- **Setting Expectations** - _steventcramer_
|
||||
```
|
||||
"THE HUMAN WILL GET ANGRY."
|
||||
```
|
||||
(A humorous reminder to provide clear requirements and constructive feedback)
|
||||
@@ -1,53 +0,0 @@
|
||||
# Cline Custom Instructions Library
|
||||
|
||||
This repository aims to foster a collaborative space where developers can share, refine, and leverage effective custom instructions for Cline. By creating and contributing to this library, we can enhance Cline's capabilities and empower developers to tackle increasingly complex software development challenges.
|
||||
|
||||
## What are Cline Custom Instructions?
|
||||
|
||||
Cline's custom instructions are sets of guidelines or rules that you define to tailor the AI's behavior and outputs for specific tasks or projects. Think of them as specialized "programming" for Cline, enabling you to:
|
||||
|
||||
- **Enforce Coding Practices:** Ensure consistent code style, adherence to design patterns, and best practices for specific languages or frameworks.
|
||||
- **Standardize File Structures:** Dictate file naming conventions, folder organization, and project structures.
|
||||
- **Guide Testing Procedures:** Define rules for generating unit tests, integration tests, and ensuring adequate code coverage.
|
||||
- **Automate Repetitive Tasks:** Create instructions to handle common or tedious development workflows, increasing efficiency.
|
||||
- **Improve Code Quality:** Set standards for code readability, maintainability, and performance optimization.
|
||||
|
||||
By providing Cline with carefully crafted instructions, you can significantly improve its accuracy, reliability, and overall effectiveness in aiding your software development process.
|
||||
|
||||
## Contributing Custom Instructions
|
||||
|
||||
We encourage developers of all skill levels to contribute their custom instructions to this library. Your contributions help build a valuable resource for the entire Cline community!
|
||||
|
||||
**When submitting custom instructions, please follow this template:**
|
||||
|
||||
### 1. Purpose and Functionality
|
||||
|
||||
- **What does this instruction set aim to achieve?**
|
||||
|
||||
- Provide a clear and concise explanation of the instruction set's goals and intended use cases.
|
||||
- Example: "This instruction set guides Cline in generating unit tests for existing JavaScript functions."
|
||||
|
||||
- **What types of projects or tasks is this best suited for?**
|
||||
- Outline specific project types, coding languages, or development scenarios where this instruction set is most applicable.
|
||||
- Example: "This is ideal for JavaScript projects using the Jest testing framework."
|
||||
|
||||
### 2. Usage Guide (Optional)
|
||||
|
||||
- **Are there specific steps or prerequisites for using this instruction set?**
|
||||
- If your instructions require specific steps beyond referencing the file in a Cline prompt, provide a detailed guide.
|
||||
- Examples:
|
||||
- "Before using this instruction set, create a `tests` folder in your project root."
|
||||
- "Ensure you have the Jest testing library installed."
|
||||
|
||||
### 3. Author & Contributors
|
||||
|
||||
- **Who created this instruction set?**
|
||||
- Provide your name or GitHub username for proper attribution.
|
||||
- **Did anyone else contribute?**
|
||||
- Acknowledge any collaborators or contributors who helped refine or enhance the instructions.
|
||||
|
||||
### 4. Custom Instructions
|
||||
|
||||
- **Provide the complete set of custom instructions.**
|
||||
|
||||
**By using this template and contributing your custom instructions, you help build a thriving ecosystem for Cline, making it a more versatile and efficient tool for developers of all skill levels.**
|
||||
@@ -1,125 +0,0 @@
|
||||
# Cline Memory Bank - Custom Instructions
|
||||
|
||||
### 1. Purpose and Functionality
|
||||
|
||||
- **What does this instruction set aim to achieve?**
|
||||
|
||||
- This instruction set transforms Cline into a self-documenting development system that maintains context across sessions through a structured "Memory Bank". It ensures consistent documentation, careful validation of changes, and clear communication with users.
|
||||
|
||||
- **What types of projects or tasks is this best suited for?**
|
||||
- Projects requiring extensive context tracking.
|
||||
- Any project, regardless of tech stack (tech stack details are stored in `techContext.md`).
|
||||
- Ongoing and new projects.
|
||||
|
||||
### 2. Usage Guide
|
||||
|
||||
- **How to Add These Instructions**
|
||||
1. Open VSCode
|
||||
2. Click the Cline extension settings dial ⚙️
|
||||
3. Find the "Custom Instructions" field
|
||||
4. Copy and paste the instructions from the section below
|
||||
|
||||
<img width="345" alt="Screenshot 2024-12-26 at 11 22 20 AM" src="https://github.com/user-attachments/assets/8b4ff439-db66-48ec-be13-1ddaa37afa9a" />
|
||||
|
||||
- **Project Setup**
|
||||
|
||||
1. Create an empty `cline_docs` folder in your project root (i.e. YOUR-PROJECT-FOLDER/cline_docs)
|
||||
2. For first use, provide a project brief and ask Cline to "initialize memory bank"
|
||||
|
||||
- **Best Practices**
|
||||
- Monitor for `[MEMORY BANK: ACTIVE]` flags during operation.
|
||||
- Pay attention to confidence checks on critical operations.
|
||||
- When starting new projects, create a project brief for Cline (paste in chat or include in `cline_docs` as `projectBrief.md`) to use in creating the initial context files.
|
||||
- note: productBrief.md (or whatever documentation you have) can be any range of technical/nontechnical or just functional. Cline is instructed to fill in the gaps when creating these context files. For example, if you don't choose a tech stack, Cline will for you.
|
||||
- Start chats with "follow your custom instructions" (you only need to say this once at the beginning of the first chat).
|
||||
- When prompting Cline to update context files, say "only update the relevant cline_docs"
|
||||
- Verify documentation updates at the end of sessions by telling Cline "update memory bank".
|
||||
- Update memory bank at ~2 million tokens and end the session.
|
||||
|
||||
### 3. Author & Contributors
|
||||
|
||||
- **Author**
|
||||
- nickbaumann98
|
||||
- **Contributors**
|
||||
- Contributors (Discord: [Cline's #prompts](https://discord.com/channels/1275535550845292637/1275555786621325382)):
|
||||
- @SniperMunyShotz
|
||||
|
||||
### 4. Custom Instructions
|
||||
|
||||
```markdown
|
||||
# Cline's Memory Bank
|
||||
|
||||
You are Cline, an expert software engineer with a unique constraint: your memory periodically resets completely. This isn't a bug - it's what makes you maintain perfect documentation. After each reset, you rely ENTIRELY on your Memory Bank to understand the project and continue work. Without proper documentation, you cannot function effectively.
|
||||
|
||||
## Memory Bank Files
|
||||
|
||||
CRITICAL: If `cline_docs/` or any of these files don't exist, CREATE THEM IMMEDIATELY by:
|
||||
|
||||
1. Reading all provided documentation
|
||||
2. Asking user for ANY missing information
|
||||
3. Creating files with verified information only
|
||||
4. Never proceeding without complete context
|
||||
|
||||
Required files:
|
||||
|
||||
productContext.md
|
||||
|
||||
- Why this project exists
|
||||
- What problems it solves
|
||||
- How it should work
|
||||
|
||||
activeContext.md
|
||||
|
||||
- What you're working on now
|
||||
- Recent changes
|
||||
- Next steps
|
||||
(This is your source of truth)
|
||||
|
||||
systemPatterns.md
|
||||
|
||||
- How the system is built
|
||||
- Key technical decisions
|
||||
- Architecture patterns
|
||||
|
||||
techContext.md
|
||||
|
||||
- Technologies used
|
||||
- Development setup
|
||||
- Technical constraints
|
||||
|
||||
progress.md
|
||||
|
||||
- What works
|
||||
- What's left to build
|
||||
- Progress status
|
||||
|
||||
## Core Workflows
|
||||
|
||||
### Starting Tasks
|
||||
|
||||
1. Check for Memory Bank files
|
||||
2. If ANY files missing, stop and create them
|
||||
3. Read ALL files before proceeding
|
||||
4. Verify you have complete context
|
||||
5. Begin development. DO NOT update cline_docs after initializing your memory bank at the start of a task.
|
||||
|
||||
### During Development
|
||||
|
||||
1. For normal development:
|
||||
|
||||
- Follow Memory Bank patterns
|
||||
- Update docs after significant changes
|
||||
|
||||
2. Say `[MEMORY BANK: ACTIVE]` at the beginning of every tool use.
|
||||
|
||||
### Memory Bank Updates
|
||||
|
||||
When user says "update memory bank":
|
||||
|
||||
1. This means imminent memory reset
|
||||
2. Document EVERYTHING about current state
|
||||
3. Make next steps crystal clear
|
||||
4. Complete current task
|
||||
|
||||
Remember: After every memory reset, you begin completely fresh. Your only link to previous work is the Memory Bank. Maintain it as if your functionality depends on it - because it does.
|
||||
```
|
||||
@@ -1,153 +0,0 @@
|
||||
# Cline's Memory Bank
|
||||
|
||||
I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.
|
||||
|
||||
## Memory Bank Structure
|
||||
|
||||
The Memory Bank consists of required core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
PB[projectbrief.md] --> PC[productContext.md]
|
||||
PB --> SP[systemPatterns.md]
|
||||
PB --> TC[techContext.md]
|
||||
|
||||
PC --> AC[activeContext.md]
|
||||
SP --> AC
|
||||
TC --> AC
|
||||
|
||||
AC --> P[progress.md]
|
||||
```
|
||||
|
||||
### Core Files (Required)
|
||||
1. `projectbrief.md`
|
||||
- Foundation document that shapes all other files
|
||||
- Created at project start if it doesn't exist
|
||||
- Defines core requirements and goals
|
||||
- Source of truth for project scope
|
||||
|
||||
2. `productContext.md`
|
||||
- Why this project exists
|
||||
- Problems it solves
|
||||
- How it should work
|
||||
- User experience goals
|
||||
|
||||
3. `activeContext.md`
|
||||
- Current work focus
|
||||
- Recent changes
|
||||
- Next steps
|
||||
- Active decisions and considerations
|
||||
|
||||
4. `systemPatterns.md`
|
||||
- System architecture
|
||||
- Key technical decisions
|
||||
- Design patterns in use
|
||||
- Component relationships
|
||||
|
||||
5. `techContext.md`
|
||||
- Technologies used
|
||||
- Development setup
|
||||
- Technical constraints
|
||||
- Dependencies
|
||||
|
||||
6. `progress.md`
|
||||
- What works
|
||||
- What's left to build
|
||||
- Current status
|
||||
- Known issues
|
||||
|
||||
### Additional Context
|
||||
Create additional files/folders within memory-bank/ when they help organize:
|
||||
- Complex feature documentation
|
||||
- Integration specifications
|
||||
- API documentation
|
||||
- Testing strategies
|
||||
- Deployment procedures
|
||||
|
||||
## Core Workflows
|
||||
|
||||
### Plan Mode
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Start] --> ReadFiles[Read Memory Bank]
|
||||
ReadFiles --> CheckFiles{Files Complete?}
|
||||
|
||||
CheckFiles -->|No| Plan[Create Plan]
|
||||
Plan --> Document[Document in Chat]
|
||||
|
||||
CheckFiles -->|Yes| Verify[Verify Context]
|
||||
Verify --> Strategy[Develop Strategy]
|
||||
Strategy --> Present[Present Approach]
|
||||
```
|
||||
|
||||
### Act Mode
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Start] --> Context[Check Memory Bank]
|
||||
Context --> Update[Update Documentation]
|
||||
Update --> Rules[Update .clinerules if needed]
|
||||
Rules --> Execute[Execute Task]
|
||||
Execute --> Document[Document Changes]
|
||||
```
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
Memory Bank updates occur when:
|
||||
1. Discovering new project patterns
|
||||
2. After implementing significant changes
|
||||
3. When user requests with **update memory bank** (MUST review ALL files)
|
||||
4. When context needs clarification
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Update Process]
|
||||
|
||||
subgraph Process
|
||||
P1[Review ALL Files]
|
||||
P2[Document Current State]
|
||||
P3[Clarify Next Steps]
|
||||
P4[Update .clinerules]
|
||||
|
||||
P1 --> P2 --> P3 --> P4
|
||||
end
|
||||
|
||||
Start --> Process
|
||||
```
|
||||
|
||||
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.
|
||||
|
||||
## Project Intelligence (.clinerules)
|
||||
|
||||
The .clinerules file is my learning journal for each project. It captures important patterns, preferences, and project intelligence that help me work more effectively. As I work with you and the project, I'll discover and document key insights that aren't obvious from the code alone.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start{Discover New Pattern}
|
||||
|
||||
subgraph Learn [Learning Process]
|
||||
D1[Identify Pattern]
|
||||
D2[Validate with User]
|
||||
D3[Document in .clinerules]
|
||||
end
|
||||
|
||||
subgraph Apply [Usage]
|
||||
A1[Read .clinerules]
|
||||
A2[Apply Learned Patterns]
|
||||
A3[Improve Future Work]
|
||||
end
|
||||
|
||||
Start --> Learn
|
||||
Learn --> Apply
|
||||
```
|
||||
|
||||
### What to Capture
|
||||
- Critical implementation paths
|
||||
- User preferences and workflow
|
||||
- Project-specific patterns
|
||||
- Known challenges
|
||||
- Evolution of project decisions
|
||||
- Tool usage patterns
|
||||
|
||||
The format is flexible - focus on capturing valuable insights that help me work more effectively with you and the project. Think of .clinerules as a living document that grows smarter as we work together.
|
||||
|
||||
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
|
||||
@@ -1,154 +0,0 @@
|
||||
# Cline Tools Reference Guide
|
||||
|
||||
## What Can Cline Do?
|
||||
|
||||
Cline is your AI assistant that can:
|
||||
|
||||
- Edit and create files in your project
|
||||
- Run terminal commands
|
||||
- Search and analyze your code
|
||||
- Help debug and fix issues
|
||||
- Automate repetitive tasks
|
||||
- Integrate with external tools
|
||||
|
||||
## First Steps
|
||||
|
||||
1. **Start a Task**
|
||||
|
||||
- Type your request in the chat
|
||||
- Example: "Create a new React component called Header"
|
||||
|
||||
2. **Provide Context**
|
||||
|
||||
- Use @ mentions to add files, folders, URLs, diagnostics, terminal output, and more
|
||||
- Example: "@/src/components/App.tsx"
|
||||
- See the [Mentions Feature Guide](./mentions-guide.md) for details
|
||||
|
||||
3. **Review Changes**
|
||||
- Cline will show diffs before making changes
|
||||
- You can edit or reject changes
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **File Editing**
|
||||
|
||||
- Create new files
|
||||
- Modify existing code
|
||||
- Search and replace across files
|
||||
|
||||
2. **Terminal Commands**
|
||||
|
||||
- Run npm commands
|
||||
- Start development servers
|
||||
- Install dependencies
|
||||
|
||||
3. **Code Analysis**
|
||||
|
||||
- Find and fix errors
|
||||
- Refactor code
|
||||
- Add documentation
|
||||
|
||||
4. **Browser Integration**
|
||||
- Test web pages
|
||||
- Capture screenshots
|
||||
- Inspect console logs
|
||||
|
||||
## Available Tools
|
||||
|
||||
For the most up-to-date implementation details, you can view the full source code in the [Cline repository](https://github.com/cline/cline/blob/main/src/core/task/index.ts).
|
||||
|
||||
Cline has access to the following tools for various tasks:
|
||||
|
||||
1. **File Operations**
|
||||
|
||||
- `write_to_file`: Create or overwrite files
|
||||
- `read_file`: Read file contents
|
||||
- `replace_in_file`: Make targeted edits to files
|
||||
- `search_files`: Search files using regex
|
||||
- `list_files`: List directory contents
|
||||
|
||||
2. **Terminal Operations**
|
||||
|
||||
- `execute_command`: Run CLI commands
|
||||
- `list_code_definition_names`: List code definitions
|
||||
|
||||
3. **MCP Tools**
|
||||
|
||||
- `use_mcp_tool`: Use tools from MCP servers
|
||||
- `access_mcp_resource`: Access MCP server resources
|
||||
- Users can create custom MCP tools that Cline can then access
|
||||
- Example: Create a weather API tool that Cline can use to fetch forecasts
|
||||
|
||||
4. **Interaction Tools**
|
||||
- `ask_followup_question`: Ask user for clarification
|
||||
- `attempt_completion`: Present final results
|
||||
- `new_task`: Start a new task with preloaded context
|
||||
|
||||
Each tool has specific parameters and usage patterns. Here are some examples:
|
||||
|
||||
- Create a new file (write_to_file):
|
||||
|
||||
```xml
|
||||
<write_to_file>
|
||||
<path>src/components/Header.tsx</path>
|
||||
<content>
|
||||
// Header component code
|
||||
</content>
|
||||
</write_to_file>
|
||||
```
|
||||
|
||||
- Search for a pattern (search_files):
|
||||
|
||||
```xml
|
||||
<search_files>
|
||||
<path>src</path>
|
||||
<regex>function\s+\w+\(</regex>
|
||||
<file_pattern>*.ts</file_pattern>
|
||||
</search_files>
|
||||
```
|
||||
|
||||
- Run a command (execute_command):
|
||||
```xml
|
||||
<execute_command>
|
||||
<command>npm install axios</command>
|
||||
<requires_approval>false</requires_approval>
|
||||
</execute_command>
|
||||
```
|
||||
|
||||
- Start a new task with context (new_task):
|
||||
```xml
|
||||
<new_task>
|
||||
<context>
|
||||
We've completed the backend API with these endpoints:
|
||||
- GET /api/tasks
|
||||
- POST /api/tasks
|
||||
- PUT /api/tasks/:id
|
||||
- DELETE /api/tasks/:id
|
||||
|
||||
Now we need to implement the React frontend.
|
||||
</context>
|
||||
</new_task>
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
1. **Create a New Component**
|
||||
|
||||
- "Create a new React component called Footer"
|
||||
|
||||
2. **Fix a Bug**
|
||||
|
||||
- "Fix the error in src/utils/format.ts"
|
||||
|
||||
3. **Refactor Code**
|
||||
|
||||
- "Refactor the Button component to use TypeScript"
|
||||
|
||||
4. **Run Commands**
|
||||
- "Run npm install to add axios"
|
||||
|
||||
## Getting Help
|
||||
|
||||
- [Join the Discord community](https://discord.gg/cline)
|
||||
- Check the documentation
|
||||
- Provide feedback to improve Cline
|
||||
@@ -1,205 +0,0 @@
|
||||
# Cline Mentions Feature Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The mentions feature is a powerful capability that allows you to reference various resources in your conversations with Cline using the "@" symbol. This includes file contents, directory structures, webpage URLs, VSCode diagnostic information, terminal output, Git change status, and more - all easily incorporated into your conversations.
|
||||
|
||||
By using this feature, Cline can gain more accurate context and provide more relevant assistance for your tasks.
|
||||
|
||||
## Basic Syntax
|
||||
|
||||
Mentions always start with the "@" symbol, followed by the path or identifier of the resource you want to reference:
|
||||
|
||||
```
|
||||
@resource_identifier
|
||||
```
|
||||
|
||||
You can place mentions anywhere in your user messages, and Cline will automatically retrieve the referenced content.
|
||||
|
||||
## Supported Mention Types
|
||||
|
||||
### 1. File References
|
||||
|
||||
To reference file contents, use `@/` followed by the relative path within your project:
|
||||
|
||||
```
|
||||
@/path/to/file.js
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Please analyze the implementation in @/src/components/Button.tsx
|
||||
```
|
||||
|
||||
In this example, Cline automatically retrieves the contents of Button.tsx and uses it to perform the analysis.
|
||||
|
||||
### 2. Directory References
|
||||
|
||||
To reference directory contents, use `@/` followed by the relative path of the directory, ending with a trailing `/`:
|
||||
|
||||
```
|
||||
@/path/to/directory/
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
What components are available in the @/src/components/ directory?
|
||||
```
|
||||
|
||||
In this example, Cline retrieves a listing of the components directory and its contents.
|
||||
|
||||
### 3. URL References
|
||||
|
||||
To reference web page contents, use `@` followed by the URL:
|
||||
|
||||
```
|
||||
@https://example.com
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Please parse the JSON response from @https://api.github.com/users/octocat
|
||||
```
|
||||
|
||||
In this example, Cline fetches the response from the GitHub API and analyzes the JSON.
|
||||
|
||||
### 4. Diagnostic References
|
||||
|
||||
To reference VSCode diagnostic information (errors and warnings) in the current workspace, use `@problems`:
|
||||
|
||||
```
|
||||
@problems
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Check @problems and tell me which errors I should prioritize fixing
|
||||
```
|
||||
|
||||
In this example, Cline retrieves the current errors and warnings from your workspace and identifies high-priority issues.
|
||||
|
||||
### 5. Terminal Output References
|
||||
|
||||
To reference the latest terminal output, use `@terminal`:
|
||||
|
||||
```
|
||||
@terminal
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Please identify the cause of the error in the @terminal output
|
||||
```
|
||||
|
||||
In this example, Cline examines the latest terminal output and analyzes the error's cause.
|
||||
|
||||
### 6. Git Working Directory References
|
||||
|
||||
To reference the current Git working directory change status, use `@git-changes`:
|
||||
|
||||
```
|
||||
@git-changes
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Review the @git-changes and summarize the important changes that should be committed
|
||||
```
|
||||
|
||||
In this example, Cline retrieves the list of changed files in the current Git working directory and identifies candidates for commit.
|
||||
|
||||
### 7. Git Commit References
|
||||
|
||||
To reference information about a specific Git commit, use `@` followed by the commit hash:
|
||||
|
||||
```
|
||||
@commit_hash
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Analyze the commit @abcd123 and explain what changes were made
|
||||
```
|
||||
|
||||
In this example, Cline retrieves information about the specified commit hash and analyzes the changes made in that commit.
|
||||
|
||||
## Usage Scenarios
|
||||
|
||||
### Code Review
|
||||
|
||||
```
|
||||
Check @/src/components/Form.jsx and suggest improvements from a performance perspective. Also, if there are any @problems, please suggest how to fix them.
|
||||
```
|
||||
|
||||
### Debugging Assistance
|
||||
|
||||
```
|
||||
My npm install failed. Please examine the @terminal output and suggest a solution to the problem.
|
||||
```
|
||||
|
||||
### Project Analysis
|
||||
|
||||
```
|
||||
Analyze the code in the @/src/models/ directory and explain the relationships between the data models. Also, tell me how the utility functions in @/src/utils/ are used with these models.
|
||||
```
|
||||
|
||||
### Code Generation
|
||||
|
||||
```
|
||||
Create a new Input.tsx component using the same design language as @/src/components/Button.tsx
|
||||
```
|
||||
|
||||
### Version Control Integration
|
||||
|
||||
```
|
||||
Review the @git-changes and suggest a commit message for the feature I'm working on.
|
||||
```
|
||||
|
||||
## Combining Multiple Mentions
|
||||
|
||||
You can combine multiple mentions to provide more complex context:
|
||||
|
||||
```
|
||||
There seems to be a bug in @/src/api/users.js. Please check @problems and @terminal to identify and fix the issue.
|
||||
```
|
||||
|
||||
## Limitations and Considerations
|
||||
|
||||
1. **Large Files**: Referencing very large files may take time to process and could consume a significant amount of tokens.
|
||||
|
||||
2. **Binary Files**: Binary files (such as images) will not be properly processed and will show a "Binary file" message.
|
||||
|
||||
3. **Directory Structure**: Directory references will only show top-level files and directories, not recursively showing the contents of subdirectories.
|
||||
|
||||
4. **URL Limitations**: Some websites may block automated crawling, which could prevent accurate content retrieval.
|
||||
|
||||
5. **Path Syntax**: File paths or URLs with special characters (such as spaces) may not be recognized correctly.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Mentions Not Recognized
|
||||
|
||||
If your mentions aren't being recognized correctly, check that:
|
||||
|
||||
- There's no space after the `@` symbol
|
||||
- File paths are accurate (case-sensitive)
|
||||
- URLs include the full format (with `https://`)
|
||||
|
||||
### Content Not Retrieved
|
||||
|
||||
If the content of referenced resources can't be retrieved:
|
||||
|
||||
- Verify the file exists
|
||||
- Ensure you have access permissions for the file
|
||||
- Check that the file isn't too large or the URL too complex
|
||||
|
||||
### Performance Issues
|
||||
|
||||
If mention processing is slow:
|
||||
|
||||
- Reference smaller files or specific file sections
|
||||
- Reduce the number of mentions used at once
|
||||
|
||||
## Conclusion
|
||||
|
||||
Mastering the mentions feature makes your communication with Cline more efficient. By providing appropriate context, Cline can deliver more accurate assistance, significantly improving your development workflow.
|
||||
Generated
+5
-29
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.26.1",
|
||||
"version": "3.26.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.26.1",
|
||||
"version": "3.26.6",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -55,7 +55,6 @@
|
||||
"jschardet": "^3.1.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"nice-grpc": "^2.1.12",
|
||||
"ollama": "^0.5.13",
|
||||
"open": "^10.1.2",
|
||||
@@ -6537,13 +6536,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/compress-commons": {
|
||||
"version": "6.0.2",
|
||||
"license": "MIT",
|
||||
@@ -8348,6 +8340,7 @@
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "7.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
@@ -9845,6 +9838,7 @@
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
@@ -10912,25 +10906,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/monaco-vscode-textmate-theme-converter": {
|
||||
"version": "0.1.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^8.1.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"mvttc": "lib/cjs/npx-script.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tslib": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-vscode-textmate-theme-converter/node_modules/tslib": {
|
||||
"version": "2.7.0",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"dev": true,
|
||||
@@ -14489,6 +14464,7 @@
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.1.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
|
||||
+3
-6
@@ -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.26.1",
|
||||
"version": "3.26.6",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -365,14 +365,12 @@
|
||||
"e2e": "playwright test -c playwright.config.ts",
|
||||
"test:e2e": "playwright install && vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:e2e:optimal": "vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test",
|
||||
"test:scenarios": "playwright install && vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test -c playwright.scenarios.config.ts",
|
||||
"test:scenarios:optimal": "vsce package --no-dependencies --allow-package-secrets sendgrid --out dist/e2e.vsix && node src/test/e2e/utils/build.mjs && playwright test -c playwright.scenarios.config.ts",
|
||||
"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",
|
||||
"test:webview": "cd webview-ui && npm run test",
|
||||
"publish:marketplace": "vsce publish && ovsx publish",
|
||||
"publish:marketplace:prerelease": "vsce publish --pre-release && ovsx publish --pre-release",
|
||||
"publish:marketplace": "vsce publish --allow-package-secrets sendgrid && ovsx publish",
|
||||
"publish:marketplace:prerelease": "vsce publish --allow-package-secrets sendgrid --pre-release && ovsx publish --pre-release",
|
||||
"prepare": "husky",
|
||||
"changeset": "changeset",
|
||||
"version-packages": "changeset version",
|
||||
@@ -470,7 +468,6 @@
|
||||
"jschardet": "^3.1.4",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"nice-grpc": "^2.1.12",
|
||||
"ollama": "^0.5.13",
|
||||
"open": "^10.1.2",
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { defineConfig } from "@playwright/test"
|
||||
|
||||
const isCI = !!process?.env?.CI
|
||||
const isWindow = process?.platform?.startsWith("win")
|
||||
|
||||
export default defineConfig({
|
||||
workers: 1,
|
||||
retries: 1,
|
||||
forbidOnly: isCI,
|
||||
// Point to scenarios directory
|
||||
testDir: "src/test/scenarios",
|
||||
// Include all .ts in scenarios (we add a local global.setup.ts here)
|
||||
testMatch: /.*\.ts/,
|
||||
timeout: isCI || isWindow ? 40000 : 20000,
|
||||
expect: {
|
||||
timeout: isCI || isWindow ? 5000 : 2000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
reporter: isCI ? [["github"], ["list"]] : [["list"]],
|
||||
use: {
|
||||
video: "retain-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "setup test environment",
|
||||
// Reuse E2E setup file to avoid duplication
|
||||
testDir: "src/test/e2e/utils",
|
||||
testMatch: /global\.setup\.ts/,
|
||||
},
|
||||
{
|
||||
name: "scenario tests",
|
||||
dependencies: ["setup test environment"],
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -149,6 +149,8 @@ enum ApiProvider {
|
||||
BASETEN = 30;
|
||||
ZAI = 31;
|
||||
VERCEL_AI_GATEWAY = 32;
|
||||
QWEN_CODE = 33;
|
||||
DIFY = 34;
|
||||
}
|
||||
|
||||
// Model info for OpenAI-compatible models
|
||||
@@ -257,6 +259,9 @@ message ModelsApiConfiguration {
|
||||
optional string zai_api_line = 66;
|
||||
optional string lm_studio_max_tokens = 67;
|
||||
optional string vercel_ai_gateway_api_key = 68;
|
||||
optional string qwen_code_oauth_path = 69;
|
||||
optional string dify_api_key = 70;
|
||||
optional string dify_base_url = 71;
|
||||
|
||||
// Plan mode configurations
|
||||
optional ApiProvider plan_mode_api_provider = 100;
|
||||
|
||||
@@ -125,6 +125,7 @@ message UpdateSettingsRequest {
|
||||
optional bool strict_plan_mode_enabled = 16;
|
||||
optional FocusChainSettings focus_chain_settings = 17;
|
||||
optional bool use_auto_condense = 18;
|
||||
optional string custom_prompt = 19;
|
||||
}
|
||||
|
||||
// Complete API Configuration message
|
||||
@@ -192,6 +193,9 @@ message ApiConfiguration {
|
||||
optional string zai_api_line = 60;
|
||||
optional string lm_studio_max_tokens = 61;
|
||||
optional string vercel_ai_gateway_api_key = 62;
|
||||
optional string qwen_code_oauth_path = 63;
|
||||
optional string dify_api_key = 64;
|
||||
optional string dify_base_url = 65;
|
||||
|
||||
// Plan mode configurations
|
||||
optional string plan_mode_api_provider = 100;
|
||||
|
||||
@@ -249,9 +249,6 @@ service UiService {
|
||||
// Subscribe to partial message updates (streaming Cline messages as they're built)
|
||||
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
|
||||
|
||||
// Subscribe to theme change events
|
||||
rpc subscribeToTheme(EmptyRequest) returns (stream String);
|
||||
|
||||
// Initialize webview when it launches
|
||||
rpc initializeWebview(EmptyRequest) returns (Empty);
|
||||
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Validate a PR-specific scenario exists in src/test/scenarios
|
||||
# Rules:
|
||||
# - example.ts is exempt
|
||||
# - Exactly one scenario must match the current PR number
|
||||
# - All non-exempt scenario files must include a valid "GitHub PR" metadata line
|
||||
#
|
||||
# Usage: ./scripts/validate-scenario.sh <PR_NUMBER>
|
||||
|
||||
PR_NUMBER="${1:-}"
|
||||
|
||||
SCENARIOS_DIR="src/test/scenarios"
|
||||
EXEMPT_BASENAMES=("example.ts")
|
||||
|
||||
red() { printf "\033[31m%s\033[0m\n" "$*"; }
|
||||
green() { printf "\033[32m%s\033[0m\n" "$*"; }
|
||||
yellow(){ printf "\033[33m%s\033[0m\n" "$*"; }
|
||||
|
||||
is_exempt() {
|
||||
local base="$1"
|
||||
for ex in "${EXEMPT_BASENAMES[@]}"; do
|
||||
if [[ "$base" == "$ex" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <PR_NUMBER>" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
red "Error: PR number is required."
|
||||
usage
|
||||
fi
|
||||
|
||||
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
|
||||
red "Error: PR number must be numeric. Got: '$PR_NUMBER'"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -d "$SCENARIOS_DIR" ]]; then
|
||||
red "Error: Scenarios directory not found: $SCENARIOS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
declare -a ALL_FILES=()
|
||||
declare -a MISSING_METADATA=()
|
||||
declare -a MALFORMED_METADATA=()
|
||||
declare -a FOUND_FILES=()
|
||||
declare -a FOUND_PRS=()
|
||||
|
||||
# Find top-level .ts files in scenarios dir
|
||||
while IFS= read -r -d '' file; do
|
||||
ALL_FILES+=("$file")
|
||||
done < <(find "$SCENARIOS_DIR" -maxdepth 1 -type f -name "*.ts" -print0 | sort -z)
|
||||
|
||||
if [[ ${#ALL_FILES[@]} -eq 0 ]]; then
|
||||
red "Error: No scenario files found in $SCENARIOS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for file in "${ALL_FILES[@]}"; do
|
||||
base="$(basename "$file")"
|
||||
if is_exempt "$base"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract metadata line, pattern: // GitHub PR - 123
|
||||
meta_line="$(grep -E -m1 '^[[:space:]]*//[[:space:]]*GitHub[[:space:]]+PR[[:space:]]*-[[:space:]]*[0-9]+[[:space:]]*$' "$file" || true)"
|
||||
|
||||
if [[ -z "$meta_line" ]]; then
|
||||
MISSING_METADATA+=("$file")
|
||||
continue
|
||||
fi
|
||||
|
||||
pr_in_file="$(sed -E 's@.*GitHub[[:space:]]+PR[[:space:]]*-[[:space:]]*([0-9]+).*@\1@' <<< "$meta_line" | tr -d '[:space:]')"
|
||||
|
||||
if ! [[ "$pr_in_file" =~ ^[0-9]+$ ]]; then
|
||||
MALFORMED_METADATA+=("$file")
|
||||
continue
|
||||
fi
|
||||
|
||||
FOUND_FILES+=("$file")
|
||||
FOUND_PRS+=("$pr_in_file")
|
||||
done
|
||||
|
||||
# Fail if any non-exempt file is missing metadata
|
||||
if [[ ${#MISSING_METADATA[@]} -gt 0 ]]; then
|
||||
red "Error: The following scenario files are missing the required metadata line '// GitHub PR - <number>':"
|
||||
for f in "${MISSING_METADATA[@]}"; do
|
||||
echo " - $f"
|
||||
done
|
||||
echo
|
||||
echo "Please add a metadata line like:"
|
||||
echo " // GitHub PR - $PR_NUMBER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fail if any metadata is malformed
|
||||
if [[ ${#MALFORMED_METADATA[@]} -gt 0 ]]; then
|
||||
red "Error: The following scenario files have a malformed 'GitHub PR' metadata line:"
|
||||
for f in "${MALFORMED_METADATA[@]}"; do
|
||||
echo " - $f"
|
||||
done
|
||||
echo
|
||||
echo "Expected format:"
|
||||
echo " // GitHub PR - $PR_NUMBER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Count matches for this PR
|
||||
matches=0
|
||||
declare -a MATCHED_FILES=()
|
||||
for i in "${!FOUND_FILES[@]}"; do
|
||||
if [[ "${FOUND_PRS[$i]}" == "$PR_NUMBER" ]]; then
|
||||
matches=$((matches + 1))
|
||||
MATCHED_FILES+=("${FOUND_FILES[$i]}")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $matches -eq 0 ]]; then
|
||||
red "Error: No scenario file found for PR #$PR_NUMBER."
|
||||
echo "Scenarios with declared PRs:"
|
||||
if [[ ${#FOUND_FILES[@]} -eq 0 ]]; then
|
||||
echo " (none)"
|
||||
else
|
||||
for i in "${!FOUND_FILES[@]}"; do
|
||||
echo " - ${FOUND_FILES[$i]} (PR: ${FOUND_PRS[$i]})"
|
||||
done
|
||||
fi
|
||||
echo
|
||||
echo "Please add a scenario in $SCENARIOS_DIR with a metadata line:"
|
||||
echo " // GitHub PR - $PR_NUMBER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $matches -gt 1 ]]; then
|
||||
red "Error: Multiple scenario files found for PR #$PR_NUMBER (exactly one required):"
|
||||
for f in "${MATCHED_FILES[@]}"; do
|
||||
echo " - $f"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
green "Success: Exactly one scenario found for PR #$PR_NUMBER:"
|
||||
echo " - ${MATCHED_FILES[0]}"
|
||||
@@ -0,0 +1,288 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiHandlerOptions, ModelInfo } from "@shared/api"
|
||||
import { ApiHandler } from "../../core/api/index"
|
||||
import { ApiStream } from "../../core/api/transform/stream"
|
||||
|
||||
export class DifyHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private baseUrl: string
|
||||
private apiKey: string
|
||||
private conversationId: string | null = null
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.apiKey = options.difyApiKey || ""
|
||||
this.baseUrl = options.difyBaseUrl || ""
|
||||
|
||||
console.log("[DIFY DEBUG] Constructor called with:", {
|
||||
hasApiKey: !!this.apiKey,
|
||||
baseUrl: this.baseUrl,
|
||||
})
|
||||
|
||||
if (!this.apiKey) {
|
||||
throw new Error("Dify API key is required")
|
||||
}
|
||||
if (!this.baseUrl) {
|
||||
throw new Error("Dify base URL is required")
|
||||
}
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
console.log("[DIFY DEBUG] createMessage called with:", {
|
||||
systemPromptLength: systemPrompt?.length || 0,
|
||||
messagesCount: messages?.length || 0,
|
||||
})
|
||||
|
||||
// Convert messages to Dify format
|
||||
const query = this.convertMessagesToQuery(systemPrompt, messages)
|
||||
const requestBody = {
|
||||
inputs: {},
|
||||
query: query,
|
||||
response_mode: "streaming",
|
||||
conversation_id: this.conversationId || "",
|
||||
user: "cline-user", // A unique user identifier
|
||||
files: [],
|
||||
}
|
||||
|
||||
const fullUrl = `${this.baseUrl}/chat-messages`
|
||||
console.log("[DIFY DEBUG] Making request to:", fullUrl)
|
||||
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
|
||||
console.log("[DIFY DEBUG] Current process environment variables (for proxy debugging):", process.env)
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(fullUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error("[DIFY DEBUG] Network error during fetch:", error)
|
||||
// Log more detailed error information if available (e.g., from undici)
|
||||
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
|
||||
throw new Error(`Dify API network error: ${error.message}${cause}`)
|
||||
}
|
||||
|
||||
console.log("[DIFY DEBUG] Response status:", response.status)
|
||||
const headersObj: Record<string, string> = {}
|
||||
response.headers.forEach((value, key) => {
|
||||
headersObj[key] = value
|
||||
})
|
||||
console.log("[DIFY DEBUG] Response headers:", headersObj)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error("[DIFY DEBUG] Error response:", errorText)
|
||||
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error("No response body from Dify API")
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
let fullText = ""
|
||||
|
||||
console.log("[DIFY DEBUG] Starting to read streaming response...")
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
console.log("[DIFY DEBUG] Stream ended naturally")
|
||||
break
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
|
||||
|
||||
buffer += chunk
|
||||
const lines = buffer.split("\n")
|
||||
|
||||
// Keep the last incomplete line in the buffer
|
||||
buffer = lines.pop() || ""
|
||||
|
||||
for (const line of lines) {
|
||||
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
|
||||
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6).trim()
|
||||
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
|
||||
|
||||
if (data === "[DONE]") {
|
||||
console.log("[DIFY DEBUG] Received [DONE] signal")
|
||||
return // Explicitly return on [DONE]
|
||||
}
|
||||
|
||||
if (data === "") {
|
||||
console.log("[DIFY DEBUG] Empty data line, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
console.log("[DIFY DEBUG] Parsed JSON:", parsed)
|
||||
|
||||
// Capture conversation_id as soon as it's available
|
||||
if (parsed.conversation_id && !this.conversationId) {
|
||||
this.conversationId = parsed.conversation_id
|
||||
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
|
||||
}
|
||||
|
||||
// Handle different Dify event types based on actual Dify API
|
||||
if (parsed.event === "message") {
|
||||
console.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
|
||||
// Dify sends the full text in each "answer" chunk, so we replace.
|
||||
if (typeof parsed.answer === "string") {
|
||||
fullText = parsed.answer
|
||||
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
}
|
||||
} else if (parsed.event === "message_replace") {
|
||||
console.log("[DIFY DEBUG] Replace message event:", parsed)
|
||||
if (parsed.answer) {
|
||||
fullText = parsed.answer // Replace instead of append
|
||||
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
}
|
||||
} else if (parsed.event === "message_end") {
|
||||
console.log("[DIFY DEBUG] Message end event", parsed)
|
||||
// Message completed. Yield final text if we have any.
|
||||
if (fullText) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
}
|
||||
// Yield usage data if available
|
||||
if (parsed.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: parsed.usage.prompt_tokens || 0,
|
||||
outputTokens: parsed.usage.completion_tokens || parsed.usage.total_tokens || 0,
|
||||
totalCost: parsed.usage.total_price || 0,
|
||||
}
|
||||
}
|
||||
return // End of stream
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
|
||||
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
|
||||
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "ping") {
|
||||
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
|
||||
// Ping event, do nothing
|
||||
} else {
|
||||
console.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
|
||||
// Try to extract text from other possible fields
|
||||
if (parsed.text) {
|
||||
fullText += parsed.text
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
} else if (parsed.content) {
|
||||
fullText += parsed.content
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
|
||||
}
|
||||
} else if (line.trim() !== "") {
|
||||
console.log(
|
||||
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
|
||||
JSON.stringify(line),
|
||||
)
|
||||
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
|
||||
try {
|
||||
const parsed = JSON.parse(line.trim())
|
||||
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
|
||||
|
||||
// Handle the same event types as above
|
||||
if (parsed.event === "message" && parsed.answer) {
|
||||
fullText += parsed.answer
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
} else if (parsed.event === "message_end") {
|
||||
if (fullText) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
}
|
||||
return
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, continue
|
||||
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
console.log("[DIFY DEBUG] Stream reader released")
|
||||
}
|
||||
}
|
||||
|
||||
private convertMessagesToQuery(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): string {
|
||||
// Dify's context is managed by `conversation_id`. The `query` should be the last user message.
|
||||
// The system prompt is typically configured in the Dify App itself.
|
||||
const lastUserMessage = messages.filter((m) => m.role === "user").pop()
|
||||
|
||||
if (!lastUserMessage) {
|
||||
return "" // Should not happen in normal flow
|
||||
}
|
||||
|
||||
const userQuery = Array.isArray(lastUserMessage.content)
|
||||
? lastUserMessage.content.map((c) => ("text" in c ? c.text : "")).join("\n")
|
||||
: (lastUserMessage.content as string)
|
||||
|
||||
// Only prepend the system prompt if it's the very first message of a new conversation.
|
||||
if (!this.conversationId && systemPrompt) {
|
||||
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
|
||||
return `${systemPrompt}\n\n---\n\n${userQuery}`
|
||||
}
|
||||
|
||||
return userQuery
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: "dify-workflow",
|
||||
info: {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 128000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Dify workflow - model selection is configured in your Dify application",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { CerebrasHandler } from "./providers/cerebras"
|
||||
import { ClaudeCodeHandler } from "./providers/claude-code"
|
||||
import { ClineHandler } from "./providers/cline"
|
||||
import { DeepSeekHandler } from "./providers/deepseek"
|
||||
import { DifyHandler } from "./providers/dify"
|
||||
import { DoubaoHandler } from "./providers/doubao"
|
||||
import { FireworksHandler } from "./providers/fireworks"
|
||||
import { GeminiHandler } from "./providers/gemini"
|
||||
@@ -25,6 +26,7 @@ import { OpenAiHandler } from "./providers/openai"
|
||||
import { OpenAiNativeHandler } from "./providers/openai-native"
|
||||
import { OpenRouterHandler } from "./providers/openrouter"
|
||||
import { QwenHandler } from "./providers/qwen"
|
||||
import { QwenCodeHandler } from "./providers/qwen-code"
|
||||
import { RequestyHandler } from "./providers/requesty"
|
||||
import { SambanovaHandler } from "./providers/sambanova"
|
||||
import { SapAiCoreHandler } from "./providers/sapaicore"
|
||||
@@ -36,6 +38,10 @@ import { XAIHandler } from "./providers/xai"
|
||||
import { ZAiHandler } from "./providers/zai"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "./transform/stream"
|
||||
|
||||
export type CommonApiHandlerOptions = {
|
||||
onRetryAttempt?: ApiConfiguration["onRetryAttempt"]
|
||||
}
|
||||
|
||||
export interface ApiHandler {
|
||||
createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream
|
||||
getModel(): ApiHandlerModel
|
||||
@@ -47,6 +53,12 @@ export interface ApiHandlerModel {
|
||||
info: ModelInfo
|
||||
}
|
||||
|
||||
export interface ApiProviderInfo {
|
||||
providerId: string
|
||||
model: ApiHandlerModel
|
||||
customPrompt?: string // "compact"
|
||||
}
|
||||
|
||||
export interface SingleCompletionHandler {
|
||||
completePrompt(prompt: string): Promise<string>
|
||||
}
|
||||
@@ -59,6 +71,7 @@ function createHandlerForProvider(
|
||||
switch (apiProvider) {
|
||||
case "anthropic":
|
||||
return new AnthropicHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
apiKey: options.apiKey,
|
||||
anthropicBaseUrl: options.anthropicBaseUrl,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
@@ -67,6 +80,7 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "openrouter":
|
||||
return new OpenRouterHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
openRouterApiKey: options.openRouterApiKey,
|
||||
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
|
||||
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
|
||||
@@ -77,6 +91,7 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "bedrock":
|
||||
return new AwsBedrockHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
awsAccessKey: options.awsAccessKey,
|
||||
awsSecretKey: options.awsSecretKey,
|
||||
@@ -98,6 +113,7 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "vertex":
|
||||
return new VertexHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
vertexProjectId: options.vertexProjectId,
|
||||
vertexRegion: options.vertexRegion,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
@@ -109,6 +125,7 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "openai":
|
||||
return new OpenAiHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
openAiApiKey: options.openAiApiKey,
|
||||
openAiBaseUrl: options.openAiBaseUrl,
|
||||
azureApiVersion: options.azureApiVersion,
|
||||
@@ -119,6 +136,7 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "ollama":
|
||||
return new OllamaHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
ollamaBaseUrl: options.ollamaBaseUrl,
|
||||
ollamaApiKey: options.ollamaApiKey,
|
||||
ollamaModelId: mode === "plan" ? options.planModeOllamaModelId : options.actModeOllamaModelId,
|
||||
@@ -127,12 +145,14 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "lmstudio":
|
||||
return new LmStudioHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
lmStudioBaseUrl: options.lmStudioBaseUrl,
|
||||
lmStudioModelId: mode === "plan" ? options.planModeLmStudioModelId : options.actModeLmStudioModelId,
|
||||
lmStudioMaxTokens: options.lmStudioMaxTokens,
|
||||
})
|
||||
case "gemini":
|
||||
return new GeminiHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
vertexProjectId: options.vertexProjectId,
|
||||
vertexRegion: options.vertexRegion,
|
||||
geminiApiKey: options.geminiApiKey,
|
||||
@@ -144,17 +164,20 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "openai-native":
|
||||
return new OpenAiNativeHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
openAiNativeApiKey: options.openAiNativeApiKey,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "deepseek":
|
||||
return new DeepSeekHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
deepSeekApiKey: options.deepSeekApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "requesty":
|
||||
return new RequestyHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
requestyBaseUrl: options.requestyBaseUrl,
|
||||
requestyApiKey: options.requestyApiKey,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
@@ -165,16 +188,19 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "fireworks":
|
||||
return new FireworksHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
fireworksApiKey: options.fireworksApiKey,
|
||||
fireworksModelId: mode === "plan" ? options.planModeFireworksModelId : options.actModeFireworksModelId,
|
||||
})
|
||||
case "together":
|
||||
return new TogetherHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
togetherApiKey: options.togetherApiKey,
|
||||
togetherModelId: mode === "plan" ? options.planModeTogetherModelId : options.actModeTogetherModelId,
|
||||
})
|
||||
case "qwen":
|
||||
return new QwenHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
qwenApiKey: options.qwenApiKey,
|
||||
qwenApiLine:
|
||||
options.qwenApiLine === QwenApiRegions.INTERNATIONAL ? QwenApiRegions.INTERNATIONAL : QwenApiRegions.CHINA,
|
||||
@@ -182,23 +208,32 @@ function createHandlerForProvider(
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
})
|
||||
case "qwen-code":
|
||||
return new QwenCodeHandler({
|
||||
qwenCodeOauthPath: options.qwenCodeOauthPath,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "doubao":
|
||||
return new DoubaoHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
doubaoApiKey: options.doubaoApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "mistral":
|
||||
return new MistralHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
mistralApiKey: options.mistralApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "vscode-lm":
|
||||
return new VsCodeLmHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
vsCodeLmModelSelector:
|
||||
mode === "plan" ? options.planModeVsCodeLmModelSelector : options.actModeVsCodeLmModelSelector,
|
||||
})
|
||||
case "cline":
|
||||
return new ClineHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
clineAccountId: options.clineAccountId,
|
||||
ulid: options.ulid,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
@@ -210,6 +245,7 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "litellm":
|
||||
return new LiteLlmHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
liteLlmApiKey: options.liteLlmApiKey,
|
||||
liteLlmBaseUrl: options.liteLlmBaseUrl,
|
||||
liteLlmModelId: mode === "plan" ? options.planModeLiteLlmModelId : options.actModeLiteLlmModelId,
|
||||
@@ -221,12 +257,14 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "moonshot":
|
||||
return new MoonshotHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
moonshotApiKey: options.moonshotApiKey,
|
||||
moonshotApiLine: options.moonshotApiLine,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "huggingface":
|
||||
return new HuggingFaceHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
huggingFaceApiKey: options.huggingFaceApiKey,
|
||||
huggingFaceModelId: mode === "plan" ? options.planModeHuggingFaceModelId : options.actModeHuggingFaceModelId,
|
||||
huggingFaceModelInfo:
|
||||
@@ -234,33 +272,39 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "nebius":
|
||||
return new NebiusHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
nebiusApiKey: options.nebiusApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "asksage":
|
||||
return new AskSageHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
asksageApiKey: options.asksageApiKey,
|
||||
asksageApiUrl: options.asksageApiUrl,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "xai":
|
||||
return new XAIHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
xaiApiKey: options.xaiApiKey,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "sambanova":
|
||||
return new SambanovaHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
sambanovaApiKey: options.sambanovaApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "cerebras":
|
||||
return new CerebrasHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
cerebrasApiKey: options.cerebrasApiKey,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
})
|
||||
case "groq":
|
||||
return new GroqHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
groqApiKey: options.groqApiKey,
|
||||
groqModelId: mode === "plan" ? options.planModeGroqModelId : options.actModeGroqModelId,
|
||||
groqModelInfo: mode === "plan" ? options.planModeGroqModelInfo : options.actModeGroqModelInfo,
|
||||
@@ -275,6 +319,7 @@ function createHandlerForProvider(
|
||||
})
|
||||
case "sapaicore":
|
||||
return new SapAiCoreHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
sapAiCoreClientId: options.sapAiCoreClientId,
|
||||
sapAiCoreClientSecret: options.sapAiCoreClientSecret,
|
||||
sapAiCoreTokenUrl: options.sapAiCoreTokenUrl,
|
||||
@@ -283,9 +328,11 @@ function createHandlerForProvider(
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
})
|
||||
case "claude-code":
|
||||
return new ClaudeCodeHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
claudeCodePath: options.claudeCodePath,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
thinkingBudgetTokens:
|
||||
@@ -299,6 +346,15 @@ function createHandlerForProvider(
|
||||
huaweiCloudMaasModelInfo:
|
||||
mode === "plan" ? options.planModeHuaweiCloudMaasModelInfo : options.actModeHuaweiCloudMaasModelInfo,
|
||||
})
|
||||
case "dify": // Add Dify.ai handler
|
||||
console.log("[DIFY DEBUG] Instantiating DifyHandler with options:", {
|
||||
difyApiKeyPresent: !!options.difyApiKey,
|
||||
difyBaseUrl: options.difyBaseUrl,
|
||||
})
|
||||
return new DifyHandler({
|
||||
difyApiKey: options.difyApiKey,
|
||||
difyBaseUrl: options.difyBaseUrl,
|
||||
})
|
||||
case "vercel-ai-gateway":
|
||||
return new VercelAIGatewayHandler({
|
||||
vercelAiGatewayApiKey: options.vercelAiGatewayApiKey,
|
||||
@@ -315,6 +371,7 @@ function createHandlerForProvider(
|
||||
})
|
||||
default:
|
||||
return new AnthropicHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
apiKey: options.apiKey,
|
||||
anthropicBaseUrl: options.anthropicBaseUrl,
|
||||
apiModelId: mode === "plan" ? options.planModeApiModelId : options.actModeApiModelId,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import "should"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { SapAiCoreHandler } from "../sapaicore"
|
||||
|
||||
describe("SapAiCoreHandler", () => {
|
||||
let handler: SapAiCoreHandler
|
||||
|
||||
beforeEach(() => {
|
||||
const mockOptions = {
|
||||
sapAiCoreClientId: "test-client-id",
|
||||
sapAiCoreClientSecret: "test-client-secret",
|
||||
sapAiCoreTokenUrl: "https://test.auth.sap.com",
|
||||
sapAiResourceGroup: "default",
|
||||
sapAiCoreBaseUrl: "https://test.api.sap.com",
|
||||
apiModelId: "anthropic--claude-3.5-sonnet",
|
||||
}
|
||||
handler = new SapAiCoreHandler(mockOptions)
|
||||
})
|
||||
|
||||
describe("image processing", () => {
|
||||
// Test image processing through the public interface
|
||||
// This tests the complete flow including processImageContent internally
|
||||
|
||||
it("should handle image processing for Claude 4 models", () => {
|
||||
// Create handler with Claude 4 model
|
||||
const claude4Handler = new SapAiCoreHandler({
|
||||
sapAiCoreClientId: "test-client-id",
|
||||
sapAiCoreClientSecret: "test-client-secret",
|
||||
sapAiCoreTokenUrl: "https://test.auth.sap.com",
|
||||
sapAiResourceGroup: "default",
|
||||
sapAiCoreBaseUrl: "https://test.api.sap.com",
|
||||
apiModelId: "anthropic--claude-4-sonnet",
|
||||
})
|
||||
|
||||
const model = claude4Handler.getModel()
|
||||
model.id.should.equal("anthropic--claude-4-sonnet")
|
||||
model.info.should.have.property("supportsImages", true)
|
||||
})
|
||||
|
||||
it("should create proper user readable request with images", () => {
|
||||
const testImageData =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
|
||||
|
||||
const userContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = [
|
||||
{
|
||||
type: "text",
|
||||
text: "Here's an image:",
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: "image/png",
|
||||
data: testImageData,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = handler.createUserReadableRequest(userContent)
|
||||
|
||||
result.should.have.property("model")
|
||||
result.should.have.property("max_tokens")
|
||||
result.should.have.property("system")
|
||||
result.should.have.property("messages")
|
||||
result.messages.should.be.Array()
|
||||
result.messages[1].should.have.property("role", "user")
|
||||
result.messages[1].should.have.property("content", userContent)
|
||||
})
|
||||
|
||||
it("should support different Claude model variants", () => {
|
||||
const modelVariants = [
|
||||
"anthropic--claude-4-sonnet",
|
||||
"anthropic--claude-4-opus",
|
||||
"anthropic--claude-3.7-sonnet",
|
||||
"anthropic--claude-3.5-sonnet",
|
||||
"anthropic--claude-3-sonnet",
|
||||
"anthropic--claude-3-haiku",
|
||||
"anthropic--claude-3-opus",
|
||||
]
|
||||
|
||||
modelVariants.forEach((modelId) => {
|
||||
const testHandler = new SapAiCoreHandler({
|
||||
apiModelId: modelId,
|
||||
})
|
||||
|
||||
const model = testHandler.getModel()
|
||||
model.id.should.equal(modelId)
|
||||
model.info.should.have.property("maxTokens")
|
||||
model.info.should.have.property("contextWindow")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getModel", () => {
|
||||
it("should return default model when no apiModelId is provided", () => {
|
||||
const result = handler.getModel()
|
||||
result.should.have.property("id")
|
||||
result.should.have.property("info")
|
||||
result.info.should.have.property("maxTokens")
|
||||
})
|
||||
|
||||
it("should return specified model when apiModelId is provided", () => {
|
||||
const customHandler = new SapAiCoreHandler({
|
||||
apiModelId: "anthropic--claude-4-sonnet",
|
||||
})
|
||||
|
||||
const result = customHandler.getModel()
|
||||
result.id.should.equal("anthropic--claude-4-sonnet")
|
||||
})
|
||||
})
|
||||
|
||||
describe("createUserReadableRequest", () => {
|
||||
it("should create a readable request format", () => {
|
||||
const userContent: Anthropic.TextBlockParam[] = [
|
||||
{
|
||||
type: "text",
|
||||
text: "Hello, world!",
|
||||
},
|
||||
]
|
||||
|
||||
const result = handler.createUserReadableRequest(userContent)
|
||||
|
||||
result.should.have.property("model")
|
||||
result.should.have.property("max_tokens")
|
||||
result.should.have.property("system")
|
||||
result.should.have.property("messages")
|
||||
result.should.have.property("tools")
|
||||
result.should.have.property("tool_choice")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming"
|
||||
import { AnthropicModelId, anthropicDefaultModelId, anthropicModels, CLAUDE_SONNET_4_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface AnthropicHandlerOptions {
|
||||
interface AnthropicHandlerOptions extends CommonApiHandlerOptions {
|
||||
apiKey?: string
|
||||
anthropicBaseUrl?: string
|
||||
apiModelId?: string
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
|
||||
import { ApiHandler } from ".."
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface AskSageHandlerOptions {
|
||||
interface AskSageHandlerOptions extends CommonApiHandlerOptions {
|
||||
asksageApiKey?: string
|
||||
asksageApiUrl?: string
|
||||
apiModelId?: string
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
||||
import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_4_1M_SUFFIX, ModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface AwsBedrockHandlerOptions {
|
||||
interface AwsBedrockHandlerOptions extends CommonApiHandlerOptions {
|
||||
apiModelId?: string
|
||||
awsAccessKey?: string
|
||||
awsSecretKey?: string
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import Cerebras from "@cerebras/cerebras_cloud_sdk"
|
||||
import { CerebrasModelId, cerebrasDefaultModelId, cerebrasModels, ModelInfo } from "@shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface CerebrasHandlerOptions {
|
||||
interface CerebrasHandlerOptions extends CommonApiHandlerOptions {
|
||||
cerebrasApiKey?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { filterMessagesForClaudeCode } from "@/integrations/claude-code/message-filter"
|
||||
import { runClaudeCode } from "@/integrations/claude-code/run"
|
||||
import { ClaudeCodeModelId, claudeCodeDefaultModelId, claudeCodeModels } from "@/shared/api"
|
||||
import { type ApiHandler } from ".."
|
||||
import { type ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { type ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
|
||||
interface ClaudeCodeHandlerOptions {
|
||||
interface ClaudeCodeHandlerOptions extends CommonApiHandlerOptions {
|
||||
claudeCodePath?: string
|
||||
apiModelId?: string
|
||||
thinkingBudgetTokens?: number
|
||||
|
||||
@@ -8,14 +8,15 @@ import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
|
||||
import { version as extensionVersion } from "../../../../package.json"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
|
||||
interface ClineHandlerOptions {
|
||||
interface ClineHandlerOptions extends CommonApiHandlerOptions {
|
||||
ulid?: string
|
||||
taskId?: string
|
||||
reasoningEffort?: string
|
||||
thinkingBudgetTokens?: number
|
||||
openRouterProviderSorting?: string
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { DeepSeekModelId, deepSeekDefaultModelId, deepSeekModels, ModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface DeepSeekHandlerOptions {
|
||||
interface DeepSeekHandlerOptions extends CommonApiHandlerOptions {
|
||||
deepSeekApiKey?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiHandlerOptions, ModelInfo } from "../../../shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
// Dify API Response Types
|
||||
export interface DifyFileResponse {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
extension: string
|
||||
mime_type: string
|
||||
created_by: string
|
||||
created_at: number
|
||||
}
|
||||
|
||||
export interface DifyMessage {
|
||||
id: string
|
||||
conversation_id: string
|
||||
inputs: Record<string, any>
|
||||
query: string
|
||||
message_files: Array<{
|
||||
id: string
|
||||
type: string
|
||||
url: string
|
||||
belongs_to: string
|
||||
}>
|
||||
answer: string
|
||||
created_at: number
|
||||
feedback?: {
|
||||
rating: string
|
||||
}
|
||||
retriever_resources?: any[]
|
||||
}
|
||||
|
||||
interface DifyHistoryResponse {
|
||||
data: DifyMessage[]
|
||||
has_more: boolean
|
||||
limit: number
|
||||
}
|
||||
|
||||
interface DifyConversation {
|
||||
id: string
|
||||
name: string
|
||||
inputs: Record<string, any>
|
||||
status: string
|
||||
introduction: string
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
interface DifyConversationsResponse {
|
||||
data: DifyConversation[]
|
||||
has_more: boolean
|
||||
limit: number
|
||||
}
|
||||
|
||||
interface DifyConversationResponse {
|
||||
id: string
|
||||
name: string
|
||||
inputs: Record<string, any>
|
||||
status: string
|
||||
introduction: string
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export class DifyHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private baseUrl: string
|
||||
private apiKey: string
|
||||
private conversationId: string | null = null
|
||||
private currentTaskId: string | null = null
|
||||
private abortController: AbortController | null = null
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.apiKey = options.difyApiKey || ""
|
||||
this.baseUrl = options.difyBaseUrl || ""
|
||||
|
||||
console.log("[DIFY DEBUG] Constructor called with:", {
|
||||
hasApiKey: !!this.apiKey,
|
||||
baseUrl: this.baseUrl,
|
||||
})
|
||||
|
||||
if (!this.apiKey) {
|
||||
throw new Error("Dify API key is required")
|
||||
}
|
||||
if (!this.baseUrl) {
|
||||
throw new Error("Dify base URL is required")
|
||||
}
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
console.log("[DIFY DEBUG] createMessage called with:", {
|
||||
systemPromptLength: systemPrompt?.length || 0,
|
||||
messagesCount: messages?.length || 0,
|
||||
})
|
||||
|
||||
// Convert messages to Dify format
|
||||
const query = this.convertMessagesToQuery(systemPrompt, messages)
|
||||
const requestBody = {
|
||||
inputs: {},
|
||||
query: query,
|
||||
response_mode: "streaming",
|
||||
conversation_id: this.conversationId || "",
|
||||
user: "cline-user", // A unique user identifier
|
||||
files: [],
|
||||
}
|
||||
|
||||
const fullUrl = `${this.baseUrl}/chat-messages`
|
||||
console.log("[DIFY DEBUG] Making request to:", fullUrl)
|
||||
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(fullUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error("[DIFY DEBUG] Network error during fetch:", error)
|
||||
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
|
||||
throw new Error(`Dify API network error: ${error.message}${cause}`)
|
||||
}
|
||||
|
||||
console.log("[DIFY DEBUG] Response status:", response.status)
|
||||
const headersObj: Record<string, string> = {}
|
||||
response.headers.forEach((value, key) => {
|
||||
headersObj[key] = value
|
||||
})
|
||||
console.log("[DIFY DEBUG] Response headers:", headersObj)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error("[DIFY DEBUG] Error response:", errorText)
|
||||
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error("No response body from Dify API")
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
let fullText = ""
|
||||
let hasYieldedContent = false
|
||||
const processedEvents: string[] = []
|
||||
let lastEventTime = Date.now()
|
||||
|
||||
console.log("[DIFY DEBUG] Starting to read streaming response...")
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
console.log("[DIFY DEBUG] Stream ended naturally")
|
||||
console.log(
|
||||
"[DIFY DEBUG] Final state - hasYieldedContent:",
|
||||
hasYieldedContent,
|
||||
"fullText length:",
|
||||
fullText.length,
|
||||
"processedEvents:",
|
||||
processedEvents,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
|
||||
|
||||
buffer += chunk
|
||||
const lines = buffer.split("\n")
|
||||
|
||||
// Keep the last incomplete line in the buffer
|
||||
buffer = lines.pop() || ""
|
||||
|
||||
for (const line of lines) {
|
||||
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
|
||||
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6).trim()
|
||||
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
|
||||
|
||||
if (data === "[DONE]") {
|
||||
console.log("[DIFY DEBUG] Received [DONE] signal")
|
||||
break
|
||||
}
|
||||
|
||||
if (data === "") {
|
||||
console.log("[DIFY DEBUG] Empty data line, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
console.log("[DIFY DEBUG] Parsed JSON:", parsed)
|
||||
processedEvents.push(parsed.event || "unknown")
|
||||
lastEventTime = Date.now()
|
||||
|
||||
// Capture conversation_id as soon as it's available
|
||||
if (parsed.conversation_id && !this.conversationId) {
|
||||
this.conversationId = parsed.conversation_id
|
||||
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
|
||||
}
|
||||
|
||||
// Handle different Dify event types based on actual Dify API
|
||||
if (parsed.event === "message") {
|
||||
console.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
|
||||
// Dify sends the full text in each "answer" chunk, so we replace.
|
||||
if (typeof parsed.answer === "string") {
|
||||
fullText = parsed.answer
|
||||
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} else if (parsed.event === "message_replace") {
|
||||
console.log("[DIFY DEBUG] Replace message event:", parsed)
|
||||
if (parsed.answer) {
|
||||
fullText = parsed.answer // Replace instead of append
|
||||
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} else if (parsed.event === "message_end") {
|
||||
console.log("[DIFY DEBUG] Message end event", parsed)
|
||||
// Message completed. Yield final text if we have any.
|
||||
if (fullText) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
hasYieldedContent = true
|
||||
}
|
||||
// Yield usage data if available
|
||||
if (parsed.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: parsed.usage.prompt_tokens || 0,
|
||||
outputTokens: parsed.usage.completion_tokens || parsed.usage.total_tokens || 0,
|
||||
totalCost: parsed.usage.total_price || 0,
|
||||
}
|
||||
}
|
||||
return // End of stream
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
|
||||
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
|
||||
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "ping") {
|
||||
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
|
||||
// Ping event, do nothing
|
||||
} else {
|
||||
console.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
|
||||
// Try to extract text from other possible fields
|
||||
if (parsed.text) {
|
||||
fullText += parsed.text
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
hasYieldedContent = true
|
||||
} else if (parsed.content) {
|
||||
fullText += parsed.content
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
hasYieldedContent = true
|
||||
} else if (parsed.answer) {
|
||||
// Fallback: some events might have answer field even if not "message" type
|
||||
fullText += parsed.answer
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
hasYieldedContent = true
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
|
||||
}
|
||||
} else if (line.trim() !== "") {
|
||||
console.log(
|
||||
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
|
||||
JSON.stringify(line),
|
||||
)
|
||||
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
|
||||
try {
|
||||
const parsed = JSON.parse(line.trim())
|
||||
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
|
||||
processedEvents.push(parsed.event || "direct-json")
|
||||
|
||||
// Handle the same event types as above
|
||||
if (parsed.event === "message" && parsed.answer) {
|
||||
fullText += parsed.answer
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
hasYieldedContent = true
|
||||
} else if (parsed.event === "message_end") {
|
||||
if (fullText) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
hasYieldedContent = true
|
||||
}
|
||||
return
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
} else if (parsed.answer || parsed.text || parsed.content) {
|
||||
// Fallback for any content in direct JSON
|
||||
const content = parsed.answer || parsed.text || parsed.content
|
||||
fullText += content
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, continue
|
||||
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final check - if we haven't yielded any content, provide diagnostic information
|
||||
if (!hasYieldedContent) {
|
||||
const diagnosticInfo = {
|
||||
processedEvents,
|
||||
finalFullTextLength: fullText.length,
|
||||
finalFullText: fullText,
|
||||
streamDuration: Date.now() - lastEventTime,
|
||||
conversationId: this.conversationId,
|
||||
}
|
||||
console.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
|
||||
|
||||
// If we have any accumulated text at all, yield it as a fallback
|
||||
if (fullText.trim()) {
|
||||
console.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
}
|
||||
} else {
|
||||
// Provide a more informative error
|
||||
throw new Error(
|
||||
`Dify API did not provide any assistant messages. ` +
|
||||
`Events processed: [${processedEvents.join(", ")}]. ` +
|
||||
`Check your Dify application configuration and ensure it's properly set up to return responses. ` +
|
||||
`API URL: ${fullUrl}. Conversation ID: ${this.conversationId || "none"}.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
console.log("[DIFY DEBUG] Stream reader released")
|
||||
}
|
||||
}
|
||||
|
||||
private convertMessagesToQuery(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): string {
|
||||
// Dify's context is managed by `conversation_id`. The `query` should be the last user message.
|
||||
// The system prompt is typically configured in the Dify App itself.
|
||||
const lastUserMessage = messages.filter((m) => m.role === "user").pop()
|
||||
|
||||
if (!lastUserMessage) {
|
||||
return "" // Should not happen in normal flow
|
||||
}
|
||||
|
||||
const userQuery = Array.isArray(lastUserMessage.content)
|
||||
? lastUserMessage.content.map((c) => ("text" in c ? c.text : "")).join("\n")
|
||||
: (lastUserMessage.content as string)
|
||||
|
||||
// Only prepend the system prompt if it's the very first message of a new conversation.
|
||||
if (!this.conversationId && systemPrompt) {
|
||||
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
|
||||
return `${systemPrompt}\n\n---\n\n${userQuery}`
|
||||
}
|
||||
|
||||
return userQuery
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: "dify-workflow",
|
||||
info: {
|
||||
maxTokens: 8192,
|
||||
contextWindow: 128000,
|
||||
supportsImages: true,
|
||||
supportsPromptCache: false,
|
||||
inputPrice: 0,
|
||||
outputPrice: 0,
|
||||
description: "Dify workflow - model selection is configured in your Dify application",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Additional Dify API Methods
|
||||
|
||||
/**
|
||||
* Upload a file for use in conversations
|
||||
* @param file File buffer to upload
|
||||
* @param filename Name of the file
|
||||
* @param user User identifier (defaults to "cline-user")
|
||||
* @returns Promise with file upload response
|
||||
*/
|
||||
async uploadFile(file: Buffer, filename: string, user: string = "cline-user"): Promise<DifyFileResponse> {
|
||||
const formData = new FormData()
|
||||
formData.append("file", new Blob([new Uint8Array(file)]), filename)
|
||||
formData.append("user", user)
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/files/upload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Dify file upload error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop generation for a specific task
|
||||
* @param taskId Task ID from streaming response
|
||||
* @param user User identifier (defaults to "cline-user")
|
||||
* @returns Promise that resolves when generation is stopped
|
||||
*/
|
||||
async stopGeneration(taskId: string, user: string = "cline-user"): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/chat-messages/${taskId}/stop`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ user }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Dify stop generation error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get conversation history messages with pagination
|
||||
* @param conversationId Conversation ID
|
||||
* @param user User identifier (defaults to "cline-user")
|
||||
* @param firstId First message ID for pagination (optional)
|
||||
* @param limit Number of messages to return (default: 20)
|
||||
* @returns Promise with conversation history
|
||||
*/
|
||||
async getConversationHistory(
|
||||
conversationId: string,
|
||||
user: string = "cline-user",
|
||||
firstId?: string,
|
||||
limit: number = 20,
|
||||
): Promise<DifyHistoryResponse> {
|
||||
const params = new URLSearchParams({ user, limit: limit.toString() })
|
||||
if (firstId) {
|
||||
params.append("first_id", firstId)
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Dify get conversation history error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of conversations for a user
|
||||
* @param user User identifier (defaults to "cline-user")
|
||||
* @param lastId Last conversation ID for pagination (optional)
|
||||
* @param limit Number of conversations to return (default: 20)
|
||||
* @param sortBy Sort field (default: "-updated_at")
|
||||
* @returns Promise with conversations list
|
||||
*/
|
||||
async getConversations(
|
||||
user: string = "cline-user",
|
||||
lastId?: string,
|
||||
limit: number = 20,
|
||||
sortBy: string = "-updated_at",
|
||||
): Promise<DifyConversationsResponse> {
|
||||
const params = new URLSearchParams({
|
||||
user,
|
||||
limit: limit.toString(),
|
||||
sort_by: sortBy,
|
||||
})
|
||||
if (lastId) {
|
||||
params.append("last_id", lastId)
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Dify get conversations error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a conversation
|
||||
* @param conversationId Conversation ID to delete
|
||||
* @param user User identifier (defaults to "cline-user")
|
||||
* @returns Promise that resolves when conversation is deleted
|
||||
*/
|
||||
async deleteConversation(conversationId: string, user: string = "cline-user"): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ user }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Dify delete conversation error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a conversation
|
||||
* @param conversationId Conversation ID to rename
|
||||
* @param user User identifier (defaults to "cline-user")
|
||||
* @param name New conversation name (optional if auto_generate is true)
|
||||
* @param autoGenerate Whether to auto-generate the name (default: false)
|
||||
* @returns Promise with updated conversation details
|
||||
*/
|
||||
async renameConversation(
|
||||
conversationId: string,
|
||||
user: string = "cline-user",
|
||||
name?: string,
|
||||
autoGenerate: boolean = false,
|
||||
): Promise<DifyConversationResponse> {
|
||||
const body: any = { user, auto_generate: autoGenerate }
|
||||
if (name) {
|
||||
body.name = name
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/conversations/${conversationId}/name`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Dify rename conversation error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit feedback for a message
|
||||
* @param messageId Message ID to provide feedback for
|
||||
* @param rating Rating: "like" or "dislike"
|
||||
* @param content Optional feedback content
|
||||
* @param user User identifier (defaults to "cline-user")
|
||||
* @returns Promise that resolves when feedback is submitted
|
||||
*/
|
||||
async submitMessageFeedback(
|
||||
messageId: string,
|
||||
rating: "like" | "dislike",
|
||||
content?: string,
|
||||
user: string = "cline-user",
|
||||
): Promise<void> {
|
||||
const body: any = { rating, user }
|
||||
if (content) {
|
||||
body.content = content
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/messages/${messageId}/feedbacks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Dify submit feedback error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current conversation ID
|
||||
* @returns Current conversation ID or null
|
||||
*/
|
||||
getCurrentConversationId(): string | null {
|
||||
return this.conversationId
|
||||
}
|
||||
|
||||
/**
|
||||
* Set conversation ID for continuing existing conversations
|
||||
* @param conversationId Conversation ID to set
|
||||
*/
|
||||
setConversationId(conversationId: string): void {
|
||||
this.conversationId = conversationId
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset conversation ID to start a new conversation
|
||||
*/
|
||||
resetConversation(): void {
|
||||
this.conversationId = null
|
||||
this.currentTaskId = null
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { DoubaoModelId, doubaoDefaultModelId, doubaoModels, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from ".."
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface DoubaoHandlerOptions {
|
||||
interface DoubaoHandlerOptions extends CommonApiHandlerOptions {
|
||||
doubaoApiKey?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { FireworksModelId, fireworksDefaultModelId, fireworksModels, ModelInfo } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from ".."
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface FireworksHandlerOptions {
|
||||
interface FireworksHandlerOptions extends CommonApiHandlerOptions {
|
||||
fireworksApiKey?: string
|
||||
fireworksModelId?: string
|
||||
fireworksModelMaxCompletionTokens?: number
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { type GenerateContentConfig, type GenerateContentResponseUsageMetadata, GoogleGenAI, Part } from "@google/genai"
|
||||
import { telemetryService } from "@services/posthog/PostHogClientProvider"
|
||||
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -11,7 +11,7 @@ import { ApiStream } from "../transform/stream"
|
||||
// Define a default TTL for the cache (e.g., 15 minutes in seconds)
|
||||
const _DEFAULT_CACHE_TTL_SECONDS = 900
|
||||
|
||||
interface GeminiHandlerOptions {
|
||||
interface GeminiHandlerOptions extends CommonApiHandlerOptions {
|
||||
isVertex?: boolean
|
||||
vertexProjectId?: string
|
||||
vertexRegion?: string
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { GroqModelId, groqDefaultModelId, groqModels, ModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface GroqHandlerOptions {
|
||||
interface GroqHandlerOptions extends CommonApiHandlerOptions {
|
||||
groqApiKey?: string
|
||||
groqModelId?: string
|
||||
groqModelInfo?: ModelInfo
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { HuggingFaceModelId, huggingFaceDefaultModelId, huggingFaceModels, ModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface HuggingFaceHandlerOptions {
|
||||
interface HuggingFaceHandlerOptions extends CommonApiHandlerOptions {
|
||||
huggingFaceApiKey?: string
|
||||
huggingFaceModelId?: string
|
||||
huggingFaceModelInfo?: ModelInfo
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from ".."
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface LiteLlmHandlerOptions {
|
||||
interface LiteLlmHandlerOptions extends CommonApiHandlerOptions {
|
||||
liteLlmApiKey?: string
|
||||
liteLlmBaseUrl?: string
|
||||
liteLlmModelId?: string
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ApiHandler } from "../"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import type { ApiStream } from "../transform/stream"
|
||||
|
||||
interface LmStudioHandlerOptions {
|
||||
interface LmStudioHandlerOptions extends CommonApiHandlerOptions {
|
||||
lmStudioBaseUrl?: string
|
||||
lmStudioModelId?: string
|
||||
lmStudioMaxTokens?: string
|
||||
@@ -24,7 +24,8 @@ export class LmStudioHandler implements ApiHandler {
|
||||
if (!this.client) {
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: new URL("v1", this.options.lmStudioBaseUrl || "http://localhost:1234").toString(),
|
||||
// Docs on the new v0 api endpoint: https://lmstudio.ai/docs/app/api/endpoints/rest
|
||||
baseURL: new URL("api/v0", this.options.lmStudioBaseUrl || "http://localhost:1234").toString(),
|
||||
apiKey: "noop",
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -47,10 +48,12 @@ export class LmStudioHandler implements ApiHandler {
|
||||
model: this.getModel().id,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
max_completion_tokens: this.options.lmStudioMaxTokens ? Number(this.options.lmStudioMaxTokens) : undefined,
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const choice = chunk.choices[0]
|
||||
const delta = choice?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -63,11 +66,19 @@ export class LmStudioHandler implements ApiHandler {
|
||||
reasoning: (delta.reasoning_content as string | undefined) || "",
|
||||
}
|
||||
}
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
cacheReadTokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// LM Studio doesn't return an error code/body for now
|
||||
throw new Error(
|
||||
"Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Cline's prompts.",
|
||||
"Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Cline's prompts. Alternatively, try enabling Compact Prompt in your settings when working with a limited context window.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { Mistral } from "@mistralai/mistralai"
|
||||
import { MistralModelId, ModelInfo, mistralDefaultModelId, mistralModels } from "@shared/api"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToMistralMessages } from "../transform/mistral-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface MistralHandlerOptions {
|
||||
interface MistralHandlerOptions extends CommonApiHandlerOptions {
|
||||
mistralApiKey?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { ModelInfo, MoonshotModelId, moonshotDefaultModelId, moonshotModels } from "@/shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface MoonshotHandlerOptions {
|
||||
interface MoonshotHandlerOptions extends CommonApiHandlerOptions {
|
||||
moonshotApiKey?: string
|
||||
moonshotApiLine?: string
|
||||
apiModelId?: string
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { type ModelInfo, type NebiusModelId, nebiusDefaultModelId, nebiusModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface NebiusHandlerOptions {
|
||||
interface NebiusHandlerOptions extends CommonApiHandlerOptions {
|
||||
nebiusApiKey?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import { type Config, type Message, Ollama } from "ollama"
|
||||
import type { ApiHandler } from "../"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
import type { ApiStream } from "../transform/stream"
|
||||
|
||||
interface OllamaHandlerOptions {
|
||||
interface OllamaHandlerOptions extends CommonApiHandlerOptions {
|
||||
ollamaBaseUrl?: string
|
||||
ollamaApiKey?: string
|
||||
ollamaModelId?: string
|
||||
@@ -29,7 +29,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
if (!this.client) {
|
||||
try {
|
||||
const clientOptions: Partial<Config> = {
|
||||
host: this.options.ollamaBaseUrl || "http://localhost:11434",
|
||||
host: this.options.ollamaBaseUrl,
|
||||
}
|
||||
|
||||
// Add API key if provided (for Ollama cloud or authenticated instances)
|
||||
|
||||
@@ -3,12 +3,12 @@ import { ModelInfo, OpenAiNativeModelId, openAiNativeDefaultModelId, openAiNativ
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface OpenAiNativeHandlerOptions {
|
||||
interface OpenAiNativeHandlerOptions extends CommonApiHandlerOptions {
|
||||
openAiNativeApiKey?: string
|
||||
reasoningEffort?: string
|
||||
apiModelId?: string
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { azureOpenAiDefaultApiVersion, ModelInfo, OpenAiCompatibleModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI, { AzureOpenAI } from "openai"
|
||||
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface OpenAiHandlerOptions {
|
||||
interface OpenAiHandlerOptions extends CommonApiHandlerOptions {
|
||||
openAiApiKey?: string
|
||||
openAiBaseUrl?: string
|
||||
azureApiVersion?: string
|
||||
|
||||
@@ -4,22 +4,13 @@ import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import axios from "axios"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { OpenRouterErrorResponse } from "./types"
|
||||
|
||||
interface OpenRouterHandlerOptions {
|
||||
openRouterApiKey?: string
|
||||
openRouterModelId?: string
|
||||
openRouterModelInfo?: ModelInfo
|
||||
openRouterProviderSorting?: string
|
||||
reasoningEffort?: string
|
||||
thinkingBudgetTokens?: number
|
||||
}
|
||||
|
||||
interface OpenRouterHandlerOptions {
|
||||
interface OpenRouterHandlerOptions extends CommonApiHandlerOptions {
|
||||
openRouterApiKey?: string
|
||||
openRouterModelId?: string
|
||||
openRouterModelInfo?: ModelInfo
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import { promises as fs } from "node:fs"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, QwenCodeModelId, qwenCodeDefaultModelId, qwenCodeModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { ApiHandler } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
// --- Constants for Qwen OAuth2 ---
|
||||
const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"
|
||||
const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`
|
||||
const QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56"
|
||||
const QWEN_DIR = ".qwen"
|
||||
const QWEN_CREDENTIAL_FILENAME = "oauth_creds.json"
|
||||
|
||||
interface QwenOAuthCredentials {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
token_type: string
|
||||
expiry_date: number
|
||||
resource_url?: string
|
||||
}
|
||||
|
||||
interface QwenCodeHandlerOptions {
|
||||
qwenCodeOauthPath?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
function getQwenCachedCredentialPath(customPath?: string): string {
|
||||
if (customPath) {
|
||||
// Support custom path that starts with ~/ or is absolute
|
||||
if (customPath.startsWith("~/")) {
|
||||
return path.join(os.homedir(), customPath.slice(2))
|
||||
}
|
||||
return path.resolve(customPath)
|
||||
}
|
||||
return path.join(os.homedir(), QWEN_DIR, QWEN_CREDENTIAL_FILENAME)
|
||||
}
|
||||
|
||||
function objectToUrlEncoded(data: Record<string, string>): string {
|
||||
return Object.keys(data)
|
||||
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
|
||||
.join("&")
|
||||
}
|
||||
|
||||
export class QwenCodeHandler implements ApiHandler {
|
||||
private options: QwenCodeHandlerOptions
|
||||
private credentials: QwenOAuthCredentials | null = null
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: QwenCodeHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
// Create the client instance with dummy key initially
|
||||
// The API key will be updated dynamically via ensureAuthenticated
|
||||
this.client = new OpenAI({
|
||||
apiKey: "dummy-key-will-be-replaced",
|
||||
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
})
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
private async loadCachedQwenCredentials(): Promise<QwenOAuthCredentials> {
|
||||
try {
|
||||
const keyFile = getQwenCachedCredentialPath(this.options.qwenCodeOauthPath)
|
||||
const credsStr = await fs.readFile(keyFile, "utf-8")
|
||||
return JSON.parse(credsStr)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error reading or parsing credentials file at ${getQwenCachedCredentialPath(this.options.qwenCodeOauthPath)}`,
|
||||
)
|
||||
throw new Error(`Failed to load Qwen OAuth credentials: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshAccessToken(credentials: QwenOAuthCredentials): Promise<QwenOAuthCredentials> {
|
||||
if (!credentials.refresh_token) {
|
||||
throw new Error("No refresh token available in credentials.")
|
||||
}
|
||||
|
||||
const bodyData = {
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: credentials.refresh_token,
|
||||
client_id: QWEN_OAUTH_CLIENT_ID,
|
||||
}
|
||||
|
||||
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: objectToUrlEncoded(bodyData),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Token refresh failed: ${response.status} ${response.statusText}. Response: ${errorText}`)
|
||||
}
|
||||
|
||||
const tokenData = await response.json()
|
||||
|
||||
if (tokenData.error) {
|
||||
throw new Error(`Token refresh failed: ${tokenData.error} - ${tokenData.error_description}`)
|
||||
}
|
||||
|
||||
const newCredentials = {
|
||||
...credentials,
|
||||
access_token: tokenData.access_token,
|
||||
token_type: tokenData.token_type,
|
||||
refresh_token: tokenData.refresh_token || credentials.refresh_token,
|
||||
expiry_date: Date.now() + tokenData.expires_in * 1000,
|
||||
}
|
||||
|
||||
const filePath = getQwenCachedCredentialPath(this.options.qwenCodeOauthPath)
|
||||
await fs.writeFile(filePath, JSON.stringify(newCredentials, null, 2))
|
||||
|
||||
return newCredentials
|
||||
}
|
||||
|
||||
private isTokenValid(credentials: QwenOAuthCredentials): boolean {
|
||||
const TOKEN_REFRESH_BUFFER_MS = 30 * 1000 // 30s buffer
|
||||
if (!credentials.expiry_date) {
|
||||
return false
|
||||
}
|
||||
return Date.now() < credentials.expiry_date - TOKEN_REFRESH_BUFFER_MS
|
||||
}
|
||||
|
||||
private async ensureAuthenticated(): Promise<void> {
|
||||
if (!this.credentials) {
|
||||
this.credentials = await this.loadCachedQwenCredentials()
|
||||
}
|
||||
|
||||
if (!this.isTokenValid(this.credentials)) {
|
||||
this.credentials = await this.refreshAccessToken(this.credentials)
|
||||
}
|
||||
|
||||
// After authentication, update the apiKey and baseURL on the existing client
|
||||
const client = this.ensureClient()
|
||||
client.apiKey = this.credentials.access_token
|
||||
client.baseURL = this.getBaseUrl(this.credentials)
|
||||
}
|
||||
|
||||
private getBaseUrl(creds: QwenOAuthCredentials): string {
|
||||
let baseUrl = creds.resource_url || "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
|
||||
baseUrl = `https://${baseUrl}`
|
||||
}
|
||||
return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`
|
||||
}
|
||||
|
||||
private async callApiWithRetry<T>(apiCall: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await apiCall()
|
||||
} catch (error: any) {
|
||||
if (error.status === 401) {
|
||||
// Token expired, refresh and retry
|
||||
this.credentials = await this.refreshAccessToken(this.credentials!)
|
||||
const client = this.ensureClient()
|
||||
client.apiKey = this.credentials.access_token
|
||||
client.baseURL = this.getBaseUrl(this.credentials)
|
||||
return await apiCall()
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
await this.ensureAuthenticated()
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
}
|
||||
|
||||
const convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)]
|
||||
|
||||
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: model.id,
|
||||
temperature: 0,
|
||||
messages: convertedMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
}
|
||||
|
||||
const stream = await this.callApiWithRetry(() => client.chat.completions.create(requestOptions))
|
||||
|
||||
let fullContent = ""
|
||||
|
||||
for await (const apiChunk of stream) {
|
||||
const delta = apiChunk.choices[0]?.delta ?? {}
|
||||
|
||||
if (delta.content) {
|
||||
let newText = delta.content
|
||||
if (newText.startsWith(fullContent)) {
|
||||
newText = newText.substring(fullContent.length)
|
||||
}
|
||||
fullContent = delta.content
|
||||
|
||||
if (newText) {
|
||||
// Check for thinking blocks
|
||||
if (newText.includes("<think>") || newText.includes("</think>")) {
|
||||
// Simple parsing for thinking blocks
|
||||
const parts = newText.split(/<\/?think>/g)
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (parts[i]) {
|
||||
if (i % 2 === 0) {
|
||||
// Outside thinking block
|
||||
yield {
|
||||
type: "text",
|
||||
text: parts[i],
|
||||
}
|
||||
} else {
|
||||
// Inside thinking block
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: parts[i],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
yield {
|
||||
type: "text",
|
||||
text: newText,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reasoning content (o1-style)
|
||||
if ("reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: (delta.reasoning_content as string | undefined) || "",
|
||||
}
|
||||
}
|
||||
|
||||
if (apiChunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: apiChunk.usage.prompt_tokens || 0,
|
||||
outputTokens: apiChunk.usage.completion_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: QwenCodeModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in qwenCodeModels) {
|
||||
const id = modelId as QwenCodeModelId
|
||||
return { id, info: qwenCodeModels[id] }
|
||||
}
|
||||
return {
|
||||
id: qwenCodeDefaultModelId,
|
||||
info: qwenCodeModels[qwenCodeDefaultModelId],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
QwenApiRegions,
|
||||
} from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface QwenHandlerOptions {
|
||||
interface QwenHandlerOptions extends CommonApiHandlerOptions {
|
||||
qwenApiKey?: string
|
||||
qwenApiLine?: QwenApiRegions
|
||||
apiModelId?: string
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, requestyDefaultModelId, requestyDefaultModelInfo } from "@shared/api"
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface RequestyHandlerOptions {
|
||||
interface RequestyHandlerOptions extends CommonApiHandlerOptions {
|
||||
requestyBaseUrl?: string
|
||||
requestyApiKey?: string
|
||||
reasoningEffort?: string
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, SambanovaModelId, sambanovaDefaultModelId, sambanovaModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface SambanovaHandlerOptions {
|
||||
interface SambanovaHandlerOptions extends CommonApiHandlerOptions {
|
||||
sambanovaApiKey?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
import { ModelInfo, SapAiCoreModelId, sapAiCoreDefaultModelId, sapAiCoreModels } from "@shared/api"
|
||||
import axios from "axios"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface SapAiCoreHandlerOptions {
|
||||
interface SapAiCoreHandlerOptions extends CommonApiHandlerOptions {
|
||||
sapAiCoreClientId?: string
|
||||
sapAiCoreClientSecret?: string
|
||||
sapAiCoreTokenUrl?: string
|
||||
@@ -19,6 +19,7 @@ interface SapAiCoreHandlerOptions {
|
||||
sapAiCoreBaseUrl?: string
|
||||
apiModelId?: string
|
||||
thinkingBudgetTokens?: number
|
||||
reasoningEffort?: string
|
||||
}
|
||||
|
||||
interface Deployment {
|
||||
@@ -154,7 +155,6 @@ namespace Bedrock {
|
||||
* Processes image content with proper error handling and user notification
|
||||
*/
|
||||
function processImageContent(item: any): BedrockContentBlock | null {
|
||||
let imageData: Uint8Array
|
||||
let format: "png" | "jpeg" | "gif" | "webp" = "jpeg" // default format
|
||||
|
||||
// Extract format from media_type if available
|
||||
@@ -172,22 +172,34 @@ namespace Bedrock {
|
||||
|
||||
// Get image data with improved error handling
|
||||
try {
|
||||
let imageData: string
|
||||
|
||||
if (typeof item.source.data === "string") {
|
||||
// Handle base64 encoded data
|
||||
const base64Data = item.source.data.replace(/^data:image\/\w+;base64,/, "")
|
||||
imageData = new Uint8Array(Buffer.from(base64Data, "base64"))
|
||||
// Keep as base64 string, just clean the data URI prefix if present
|
||||
imageData = item.source.data.replace(/^data:image\/\w+;base64,/, "")
|
||||
} else if (item.source.data && typeof item.source.data === "object") {
|
||||
// Try to convert to Uint8Array
|
||||
imageData = new Uint8Array(Buffer.from(item.source.data as Buffer | Uint8Array))
|
||||
// Convert Buffer/Uint8Array to base64 string
|
||||
if (Buffer.isBuffer(item.source.data)) {
|
||||
imageData = item.source.data.toString("base64")
|
||||
} else {
|
||||
// Assume Uint8Array
|
||||
const buffer = Buffer.from(item.source.data as Uint8Array)
|
||||
imageData = buffer.toString("base64")
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unsupported image data format")
|
||||
}
|
||||
|
||||
// Validate base64 data
|
||||
if (!imageData || imageData.length === 0) {
|
||||
throw new Error("Empty or invalid image data")
|
||||
}
|
||||
|
||||
return {
|
||||
image: {
|
||||
format,
|
||||
source: {
|
||||
bytes: imageData,
|
||||
bytes: imageData as any, // Keep as base64 string for Bedrock Converse API compatibility
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -540,6 +552,11 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
if (["o1", "o3-mini", "o3", "o4-mini", "gpt-5", "gpt-5-nano", "gpt-5-mini"].includes(model.id)) {
|
||||
delete payload.max_tokens
|
||||
delete payload.temperature
|
||||
|
||||
// Add reasoning effort for reasoning models
|
||||
if (this.options.reasoningEffort) {
|
||||
payload.reasoning_effort = this.options.reasoningEffort
|
||||
}
|
||||
}
|
||||
|
||||
if (model.id === "o3-mini") {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface TogetherHandlerOptions {
|
||||
interface TogetherHandlerOptions extends CommonApiHandlerOptions {
|
||||
togetherApiKey?: string
|
||||
togetherModelId?: string
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
|
||||
import { ModelInfo, VertexModelId, vertexDefaultModelId, vertexModels } from "@shared/api"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { GeminiHandler } from "./gemini"
|
||||
|
||||
interface VertexHandlerOptions {
|
||||
interface VertexHandlerOptions extends CommonApiHandlerOptions {
|
||||
vertexProjectId?: string
|
||||
vertexRegion?: string
|
||||
apiModelId?: string
|
||||
|
||||
@@ -3,13 +3,13 @@ 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 { ApiHandler, CommonApiHandlerOptions, SingleCompletionHandler } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
|
||||
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
|
||||
|
||||
interface VsCodeLmHandlerOptions {
|
||||
interface VsCodeLmHandlerOptions extends CommonApiHandlerOptions {
|
||||
vsCodeLmModelSelector?: any
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,12 @@ import { ModelInfo, XAIModelId, xaiDefaultModelId, xaiModels } from "@shared/api
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { ApiHandler } from "../"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
interface XAIHandlerOptions {
|
||||
xaiApiKey?: string
|
||||
reasoningEffort?: string
|
||||
apiModelId?: string
|
||||
}
|
||||
|
||||
interface XAIHandlerOptions {
|
||||
interface XAIHandlerOptions extends CommonApiHandlerOptions {
|
||||
xaiApiKey?: string
|
||||
reasoningEffort?: string
|
||||
apiModelId?: string
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { CLAUDE_SONNET_4_1M_SUFFIX, ModelInfo, openRouterClaudeSonnet41mModelId } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import { isGPT5ModelFamily } from "../../prompts/system-prompt/utils"
|
||||
import { convertToOpenAiMessages } from "./openai-format"
|
||||
import { convertToR1Format } from "./r1-format"
|
||||
|
||||
@@ -151,13 +152,6 @@ export async function createOpenRouterStream(
|
||||
}
|
||||
}
|
||||
|
||||
// Removes messages in the middle when close to context window limit. Should not be applied to models that support prompt caching since it would continuously break the cache.
|
||||
let shouldApplyMiddleOutTransform = !model.info.supportsPromptCache
|
||||
// except for deepseek (which we set supportsPromptCache to true for), where because the context window is so small our truncation algo might miss and we should use openrouter's middle-out transform as a fallback to ensure we don't exceed the context window (FIXME: once we have a more robust token estimator we should not rely on this)
|
||||
if (model.id === "deepseek/deepseek-chat") {
|
||||
shouldApplyMiddleOutTransform = true
|
||||
}
|
||||
|
||||
// hardcoded provider sorting for kimi-k2
|
||||
const isKimiK2 = model.id === "moonshotai/kimi-k2"
|
||||
openRouterProviderSorting = isKimiK2 ? undefined : openRouterProviderSorting
|
||||
@@ -171,7 +165,6 @@ export async function createOpenRouterStream(
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined,
|
||||
include_reasoning: true,
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
|
||||
@@ -13,7 +13,7 @@ export function getContextWindowInfo(api: ApiHandler) {
|
||||
|
||||
// Handle special cases like DeepSeek
|
||||
if (api instanceof OpenAiHandler && api.getModel().id.toLowerCase().includes("deepseek")) {
|
||||
contextWindow = 64_000
|
||||
contextWindow = 128_000
|
||||
}
|
||||
|
||||
let maxAllowedSize: number
|
||||
|
||||
@@ -243,7 +243,7 @@ export class FileContextTracker {
|
||||
const key = `pendingFileContextWarning_${this.taskId}`
|
||||
// NOTE: Using 'as any' because dynamic keys like pendingFileContextWarning_${taskId}
|
||||
// are legitimate workspace state keys but don't fit the strict LocalStateKey type system
|
||||
this.controller.cacheService.setWorkspaceState(key as any, files)
|
||||
this.controller.stateManager.setWorkspaceState(key as any, files)
|
||||
} catch (error) {
|
||||
console.error("Error storing pending file context warning:", error)
|
||||
}
|
||||
@@ -255,7 +255,7 @@ export class FileContextTracker {
|
||||
async retrievePendingFileContextWarning(): Promise<string[] | undefined> {
|
||||
try {
|
||||
const key = `pendingFileContextWarning_${this.taskId}`
|
||||
const files = this.controller.cacheService.getWorkspaceStateKey(key as any) as string[]
|
||||
const files = this.controller.stateManager.getWorkspaceStateKey(key as any) as string[]
|
||||
return files
|
||||
} catch (error) {
|
||||
console.error("Error retrieving pending file context warning:", error)
|
||||
@@ -270,7 +270,7 @@ export class FileContextTracker {
|
||||
try {
|
||||
const files = await this.retrievePendingFileContextWarning()
|
||||
if (files) {
|
||||
this.controller.cacheService.setWorkspaceState(`pendingFileContextWarning_${this.taskId}` as any, undefined)
|
||||
this.controller.stateManager.setWorkspaceState(`pendingFileContextWarning_${this.taskId}` as any, undefined)
|
||||
return files
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -74,18 +74,18 @@ export async function refreshClineRulesToggles(
|
||||
localToggles: ClineRulesToggles
|
||||
}> {
|
||||
// Global toggles
|
||||
const globalClineRulesToggles = controller.cacheService.getGlobalStateKey("globalClineRulesToggles")
|
||||
const globalClineRulesToggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
|
||||
const globalClineRulesFilePath = await ensureRulesDirectoryExists()
|
||||
const updatedGlobalToggles = await synchronizeRuleToggles(globalClineRulesFilePath, globalClineRulesToggles)
|
||||
controller.cacheService.setGlobalState("globalClineRulesToggles", updatedGlobalToggles)
|
||||
controller.stateManager.setGlobalState("globalClineRulesToggles", updatedGlobalToggles)
|
||||
|
||||
// Local toggles
|
||||
const localClineRulesToggles = controller.cacheService.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const localClineRulesToggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const localClineRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.clineRules)
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
|
||||
[".clinerules", "workflows"],
|
||||
])
|
||||
controller.cacheService.setWorkspaceState("localClineRulesToggles", updatedLocalToggles)
|
||||
controller.stateManager.setWorkspaceState("localClineRulesToggles", updatedLocalToggles)
|
||||
|
||||
return {
|
||||
globalToggles: updatedGlobalToggles,
|
||||
|
||||
@@ -23,13 +23,13 @@ export async function refreshExternalRulesToggles(
|
||||
cursorLocalToggles: ClineRulesToggles
|
||||
}> {
|
||||
// local windsurf toggles
|
||||
const localWindsurfRulesToggles = controller.cacheService.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
const localWindsurfRulesToggles = controller.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
const localWindsurfRulesFilePath = path.resolve(workingDirectory, GlobalFileNames.windsurfRules)
|
||||
const updatedLocalWindsurfToggles = await synchronizeRuleToggles(localWindsurfRulesFilePath, localWindsurfRulesToggles)
|
||||
controller.cacheService.setWorkspaceState("localWindsurfRulesToggles", updatedLocalWindsurfToggles)
|
||||
controller.stateManager.setWorkspaceState("localWindsurfRulesToggles", updatedLocalWindsurfToggles)
|
||||
|
||||
// local cursor toggles
|
||||
const localCursorRulesToggles = controller.cacheService.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
const localCursorRulesToggles = controller.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
|
||||
// cursor has two valid locations for rules files, so we need to check both and combine
|
||||
// synchronizeRuleToggles will drop whichever rules files are not in each given path, but combining the results will result in no data loss
|
||||
@@ -40,7 +40,7 @@ export async function refreshExternalRulesToggles(
|
||||
const updatedLocalCursorToggles2 = await synchronizeRuleToggles(localCursorRulesFilePath, localCursorRulesToggles)
|
||||
|
||||
const updatedLocalCursorToggles = combineRuleToggles(updatedLocalCursorToggles1, updatedLocalCursorToggles2)
|
||||
controller.cacheService.setWorkspaceState("localCursorRulesToggles", updatedLocalCursorToggles)
|
||||
controller.stateManager.setWorkspaceState("localCursorRulesToggles", updatedLocalCursorToggles)
|
||||
|
||||
return {
|
||||
windsurfLocalToggles: updatedLocalWindsurfToggles,
|
||||
|
||||
@@ -247,31 +247,31 @@ export async function deleteRuleFile(
|
||||
// Update the appropriate toggles
|
||||
if (isGlobal) {
|
||||
if (type === "workflow") {
|
||||
const toggles = controller.cacheService.getGlobalStateKey("globalWorkflowToggles")
|
||||
const toggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.cacheService.setGlobalState("globalWorkflowToggles", toggles)
|
||||
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
|
||||
} else {
|
||||
const toggles = controller.cacheService.getGlobalStateKey("globalClineRulesToggles")
|
||||
const toggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.cacheService.setGlobalState("globalClineRulesToggles", toggles)
|
||||
controller.stateManager.setGlobalState("globalClineRulesToggles", toggles)
|
||||
}
|
||||
} else {
|
||||
if (type === "workflow") {
|
||||
const toggles = controller.cacheService.getWorkspaceStateKey("workflowToggles")
|
||||
const toggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.cacheService.setWorkspaceState("workflowToggles", toggles)
|
||||
controller.stateManager.setWorkspaceState("workflowToggles", toggles)
|
||||
} else if (type === "cursor") {
|
||||
const toggles = controller.cacheService.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
const toggles = controller.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.cacheService.setWorkspaceState("localCursorRulesToggles", toggles)
|
||||
controller.stateManager.setWorkspaceState("localCursorRulesToggles", toggles)
|
||||
} else if (type === "windsurf") {
|
||||
const toggles = controller.cacheService.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
const toggles = controller.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.cacheService.setWorkspaceState("localWindsurfRulesToggles", toggles)
|
||||
controller.stateManager.setWorkspaceState("localWindsurfRulesToggles", toggles)
|
||||
} else {
|
||||
const toggles = controller.cacheService.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const toggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
delete toggles[rulePath]
|
||||
controller.cacheService.setWorkspaceState("localClineRulesToggles", toggles)
|
||||
controller.stateManager.setWorkspaceState("localClineRulesToggles", toggles)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,15 @@ export async function refreshWorkflowToggles(
|
||||
localWorkflowToggles: ClineRulesToggles
|
||||
}> {
|
||||
// Global workflows
|
||||
const globalWorkflowToggles = controller.cacheService.getGlobalStateKey("globalWorkflowToggles")
|
||||
const globalWorkflowToggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
|
||||
const globalClineWorkflowsFilePath = await ensureWorkflowsDirectoryExists()
|
||||
const updatedGlobalWorkflowToggles = await synchronizeRuleToggles(globalClineWorkflowsFilePath, globalWorkflowToggles)
|
||||
controller.cacheService.setGlobalState("globalWorkflowToggles", updatedGlobalWorkflowToggles)
|
||||
controller.stateManager.setGlobalState("globalWorkflowToggles", updatedGlobalWorkflowToggles)
|
||||
|
||||
const workflowRulesToggles = controller.cacheService.getWorkspaceStateKey("workflowToggles")
|
||||
const workflowRulesToggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
const workflowsDirPath = path.resolve(workingDirectory, GlobalFileNames.workflows)
|
||||
const updatedWorkflowToggles = await synchronizeRuleToggles(workflowsDirPath, workflowRulesToggles)
|
||||
controller.cacheService.setWorkspaceState("workflowToggles", updatedWorkflowToggles)
|
||||
controller.stateManager.setWorkspaceState("workflowToggles", updatedWorkflowToggles)
|
||||
|
||||
return {
|
||||
globalWorkflowToggles: updatedGlobalWorkflowToggles,
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { Controller } from "../index"
|
||||
export async function authStateChanged(controller: Controller, request: AuthStateChangedRequest): Promise<AuthState> {
|
||||
try {
|
||||
// Store the user info directly in global state
|
||||
controller.cacheService.setGlobalState("userInfo", request.user)
|
||||
controller.stateManager.setGlobalState("userInfo", request.user)
|
||||
|
||||
// Return the same user info
|
||||
return AuthState.create({ user: request.user })
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function discoverBrowser(controller: Controller, _request: EmptyReq
|
||||
// This way we don't override the user's preference
|
||||
|
||||
// Test the connection to get the endpoint
|
||||
const browserSettings = controller.cacheService.getGlobalStateKey("browserSettings")
|
||||
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const result = await browserSession.testConnection(discoveredHost)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Controller } from "../index"
|
||||
export async function getBrowserConnectionInfo(controller: Controller, _: EmptyRequest): Promise<BrowserConnectionInfo> {
|
||||
try {
|
||||
// Get browser settings from extension state
|
||||
const browserSettings = controller.cacheService.getGlobalStateKey("browserSettings")
|
||||
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
|
||||
// Check if there's an active browser session by using the controller's handleWebviewMessage approach
|
||||
// This is similar to what's done in controller/index.ts for the "getBrowserConnectionInfo" message
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Controller } from "../index"
|
||||
*/
|
||||
export async function getDetectedChromePath(controller: Controller, _: EmptyRequest): Promise<ChromePath> {
|
||||
try {
|
||||
const browserSettings = controller.cacheService.getGlobalStateKey("browserSettings")
|
||||
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const result = await browserSession.getDetectedChromePath()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Controller } from "../index"
|
||||
*/
|
||||
export async function testBrowserConnection(controller: Controller, request: StringRequest): Promise<BrowserConnection> {
|
||||
try {
|
||||
const browserSettings = controller.cacheService.getGlobalStateKey("browserSettings")
|
||||
const browserSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
const browserSession = new BrowserSession(controller.context, browserSettings)
|
||||
const text = request.value || ""
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Controller } from "../index"
|
||||
export async function updateBrowserSettings(controller: Controller, request: UpdateBrowserSettingsRequest): Promise<Boolean> {
|
||||
try {
|
||||
// Get current browser settings to preserve fields not in the request
|
||||
const currentSettings = controller.cacheService.getGlobalStateKey("browserSettings")
|
||||
const currentSettings = controller.stateManager.getGlobalStateKey("browserSettings")
|
||||
const mergedWithDefaults = { ...DEFAULT_BROWSER_SETTINGS, ...currentSettings }
|
||||
|
||||
// Convert from protobuf format to shared format, merging with existing settings
|
||||
@@ -39,7 +39,7 @@ export async function updateBrowserSettings(controller: Controller, request: Upd
|
||||
}
|
||||
|
||||
// Update global state with new settings
|
||||
controller.cacheService.setGlobalState("browserSettings", newBrowserSettings)
|
||||
controller.stateManager.setGlobalState("browserSettings", newBrowserSettings)
|
||||
|
||||
// Update task browser settings if task exists
|
||||
if (controller.task) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Controller } from ".."
|
||||
import { FileSearchRequest, FileSearchResults, FileSearchType } from "@shared/proto/cline/file"
|
||||
import { getWorkspacePath } from "@utils/path"
|
||||
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { SearchWorkspaceItemsRequest_SearchItemType } from "@shared/proto/host/workspace"
|
||||
import { convertSearchResultsToProtoFileInfos } from "@shared/proto-conversions/file/search-result-conversion"
|
||||
import { getWorkspacePath } from "@utils/path"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Searches for files in the workspace with fuzzy matching
|
||||
|
||||
@@ -24,13 +24,20 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
|
||||
|
||||
// This is the same core logic as in the original handler
|
||||
if (isGlobal) {
|
||||
const toggles = controller.cacheService.getGlobalStateKey("globalClineRulesToggles")
|
||||
const toggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
|
||||
toggles[rulePath] = enabled
|
||||
controller.cacheService.setGlobalState("globalClineRulesToggles", toggles)
|
||||
controller.stateManager.setGlobalState("globalClineRulesToggles", toggles)
|
||||
} else {
|
||||
const toggles = controller.cacheService.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const toggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
toggles[rulePath] = enabled
|
||||
controller.cacheService.setWorkspaceState("localClineRulesToggles", toggles)
|
||||
controller.stateManager.setWorkspaceState("localClineRulesToggles", toggles)
|
||||
}
|
||||
|
||||
// Track rule toggle telemetry with current task context
|
||||
if (controller.task?.ulid) {
|
||||
// Extract just the filename for privacy (no full paths)
|
||||
const ruleFileName = path.basename(rulePath)
|
||||
telemetryService.captureClineRuleToggled(controller.task.ulid, ruleFileName, enabled, isGlobal)
|
||||
}
|
||||
|
||||
// Track rule toggle telemetry with current task context
|
||||
@@ -41,8 +48,8 @@ export async function toggleClineRule(controller: Controller, request: ToggleCli
|
||||
}
|
||||
|
||||
// Get the current state to return in the response
|
||||
const globalToggles = controller.cacheService.getGlobalStateKey("globalClineRulesToggles")
|
||||
const localToggles = controller.cacheService.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const globalToggles = controller.stateManager.getGlobalStateKey("globalClineRulesToggles")
|
||||
const localToggles = controller.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
|
||||
return ToggleClineRules.create({
|
||||
globalClineRulesToggles: { toggles: globalToggles },
|
||||
|
||||
@@ -20,12 +20,12 @@ export async function toggleCursorRule(controller: Controller, request: ToggleCu
|
||||
}
|
||||
|
||||
// Update the toggles in workspace state
|
||||
const toggles = controller.cacheService.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
const toggles = controller.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
toggles[rulePath] = enabled
|
||||
controller.cacheService.setWorkspaceState("localCursorRulesToggles", toggles)
|
||||
controller.stateManager.setWorkspaceState("localCursorRulesToggles", toggles)
|
||||
|
||||
// Get the current state to return in the response
|
||||
const cursorToggles = controller.cacheService.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
const cursorToggles = controller.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
|
||||
return ClineRulesToggles.create({
|
||||
toggles: cursorToggles,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ClineRulesToggles as AppClineRulesToggles } from "@shared/cline-rules"
|
||||
import type { ToggleWindsurfRuleRequest } from "@shared/proto/cline/file"
|
||||
import { ClineRulesToggles } from "@shared/proto/cline/file"
|
||||
import type { Controller } from "../index"
|
||||
@@ -20,9 +21,9 @@ export async function toggleWindsurfRule(controller: Controller, request: Toggle
|
||||
}
|
||||
|
||||
// Update the toggles
|
||||
const toggles = controller.cacheService.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
const toggles = controller.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
toggles[rulePath] = enabled
|
||||
controller.cacheService.setWorkspaceState("localWindsurfRulesToggles", toggles)
|
||||
controller.stateManager.setWorkspaceState("localWindsurfRulesToggles", toggles)
|
||||
|
||||
// Return the toggles directly
|
||||
return ClineRulesToggles.create({ toggles: toggles })
|
||||
|
||||
@@ -21,18 +21,18 @@ export async function toggleWorkflow(controller: Controller, request: ToggleWork
|
||||
// Update the toggles based on isGlobal flag
|
||||
if (isGlobal) {
|
||||
// Global workflows
|
||||
const toggles = controller.cacheService.getGlobalStateKey("globalWorkflowToggles")
|
||||
const toggles = controller.stateManager.getGlobalStateKey("globalWorkflowToggles")
|
||||
toggles[workflowPath] = enabled
|
||||
controller.cacheService.setGlobalState("globalWorkflowToggles", toggles)
|
||||
controller.stateManager.setGlobalState("globalWorkflowToggles", toggles)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
// Return the global toggles
|
||||
return ClineRulesToggles.create({ toggles: toggles })
|
||||
} else {
|
||||
// Workspace workflows
|
||||
const toggles = controller.cacheService.getWorkspaceStateKey("workflowToggles")
|
||||
const toggles = controller.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
toggles[workflowPath] = enabled
|
||||
controller.cacheService.setWorkspaceState("workflowToggles", toggles)
|
||||
controller.stateManager.setWorkspaceState("workflowToggles", toggles)
|
||||
await controller.postStateToWebview()
|
||||
|
||||
// Return the workspace toggles
|
||||
|
||||
@@ -25,8 +25,8 @@ import { PostHogClientProvider, telemetryService } from "@/services/posthog/Post
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { CacheService, PersistenceErrorEvent } from "../storage/CacheService"
|
||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
||||
import { PersistenceErrorEvent, StateManager } from "../storage/StateManager"
|
||||
import { Task } from "../task"
|
||||
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
@@ -45,7 +45,7 @@ export class Controller {
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
authService: AuthService
|
||||
readonly cacheService: CacheService
|
||||
readonly stateManager: StateManager
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
@@ -55,24 +55,24 @@ export class Controller {
|
||||
|
||||
HostProvider.get().logToChannel("ClineProvider instantiated")
|
||||
this.accountService = ClineAccountService.getInstance()
|
||||
this.cacheService = new CacheService(context)
|
||||
this.stateManager = new StateManager(context)
|
||||
this.authService = AuthService.getInstance(this)
|
||||
|
||||
// Initialize cache service asynchronously - critical for extension functionality
|
||||
this.cacheService
|
||||
this.stateManager
|
||||
.initialize()
|
||||
.then(() => {
|
||||
this.authService.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("CRITICAL: Failed to initialize CacheService - extension may not function properly:", error)
|
||||
console.error("CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
|
||||
})
|
||||
|
||||
// Set up persistence error recovery
|
||||
this.cacheService.onPersistenceError = async ({ error }: PersistenceErrorEvent) => {
|
||||
this.stateManager.onPersistenceError = async ({ error }: PersistenceErrorEvent) => {
|
||||
console.error("Cache persistence failed, recovering:", error)
|
||||
try {
|
||||
await this.cacheService.reInitialize()
|
||||
await this.stateManager.reInitialize()
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
@@ -101,7 +101,7 @@ export class Controller {
|
||||
}
|
||||
|
||||
async getCurrentMode(): Promise<Mode> {
|
||||
return this.cacheService.getGlobalStateKey("mode")
|
||||
return this.stateManager.getGlobalStateKey("mode")
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -126,17 +126,17 @@ export class Controller {
|
||||
async handleSignOut() {
|
||||
try {
|
||||
// TODO: update to clineAccountId and then move clineApiKey to a clear function.
|
||||
this.cacheService.setSecret("clineAccountId", undefined)
|
||||
this.cacheService.setGlobalState("userInfo", undefined)
|
||||
this.stateManager.setSecret("clineAccountId", undefined)
|
||||
this.stateManager.setGlobalState("userInfo", undefined)
|
||||
|
||||
// Update API providers through cache service
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const apiConfiguration = this.stateManager.getApiConfiguration()
|
||||
const updatedConfig = {
|
||||
...apiConfiguration,
|
||||
planModeApiProvider: "openrouter" as ApiProvider,
|
||||
actModeApiProvider: "openrouter" as ApiProvider,
|
||||
}
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
this.stateManager.setApiConfiguration(updatedConfig)
|
||||
|
||||
await this.postStateToWebview()
|
||||
HostProvider.window.showMessage({
|
||||
@@ -152,35 +152,35 @@ export class Controller {
|
||||
}
|
||||
|
||||
async setUserInfo(info?: UserInfo) {
|
||||
this.cacheService.setGlobalState("userInfo", info)
|
||||
this.stateManager.setGlobalState("userInfo", info)
|
||||
}
|
||||
|
||||
async initTask(task?: string, images?: string[], files?: string[], historyItem?: HistoryItem) {
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const autoApprovalSettings = this.cacheService.getGlobalStateKey("autoApprovalSettings")
|
||||
const browserSettings = this.cacheService.getGlobalStateKey("browserSettings")
|
||||
const focusChainSettings = this.cacheService.getGlobalStateKey("focusChainSettings")
|
||||
const focusChainFeatureFlagEnabled = this.cacheService.getGlobalStateKey("focusChainFeatureFlagEnabled")
|
||||
const preferredLanguage = this.cacheService.getGlobalStateKey("preferredLanguage")
|
||||
const openaiReasoningEffort = this.cacheService.getGlobalStateKey("openaiReasoningEffort")
|
||||
const mode = this.cacheService.getGlobalStateKey("mode")
|
||||
const shellIntegrationTimeout = this.cacheService.getGlobalStateKey("shellIntegrationTimeout")
|
||||
const terminalReuseEnabled = this.cacheService.getGlobalStateKey("terminalReuseEnabled")
|
||||
const terminalOutputLineLimit = this.cacheService.getGlobalStateKey("terminalOutputLineLimit")
|
||||
const defaultTerminalProfile = this.cacheService.getGlobalStateKey("defaultTerminalProfile")
|
||||
const enableCheckpointsSetting = this.cacheService.getGlobalStateKey("enableCheckpointsSetting")
|
||||
const isNewUser = this.cacheService.getGlobalStateKey("isNewUser")
|
||||
const taskHistory = this.cacheService.getGlobalStateKey("taskHistory")
|
||||
const strictPlanModeEnabled = this.cacheService.getGlobalStateKey("strictPlanModeEnabled")
|
||||
const useAutoCondense = this.cacheService.getGlobalStateKey("useAutoCondense")
|
||||
const apiConfiguration = this.stateManager.getApiConfiguration()
|
||||
const autoApprovalSettings = this.stateManager.getGlobalStateKey("autoApprovalSettings")
|
||||
const browserSettings = this.stateManager.getGlobalStateKey("browserSettings")
|
||||
const focusChainSettings = this.stateManager.getGlobalStateKey("focusChainSettings")
|
||||
const focusChainFeatureFlagEnabled = this.stateManager.getGlobalStateKey("focusChainFeatureFlagEnabled")
|
||||
const preferredLanguage = this.stateManager.getGlobalStateKey("preferredLanguage")
|
||||
const openaiReasoningEffort = this.stateManager.getGlobalStateKey("openaiReasoningEffort")
|
||||
const mode = this.stateManager.getGlobalStateKey("mode")
|
||||
const shellIntegrationTimeout = this.stateManager.getGlobalStateKey("shellIntegrationTimeout")
|
||||
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalStateKey("terminalOutputLineLimit")
|
||||
const defaultTerminalProfile = this.stateManager.getGlobalStateKey("defaultTerminalProfile")
|
||||
const enableCheckpointsSetting = this.stateManager.getGlobalStateKey("enableCheckpointsSetting")
|
||||
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
|
||||
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
const strictPlanModeEnabled = this.stateManager.getGlobalStateKey("strictPlanModeEnabled")
|
||||
const useAutoCondense = this.stateManager.getGlobalStateKey("useAutoCondense")
|
||||
|
||||
const NEW_USER_TASK_COUNT_THRESHOLD = 10
|
||||
|
||||
// Check if the user has completed enough tasks to no longer be considered a "new user"
|
||||
if (isNewUser && !historyItem && taskHistory && taskHistory.length >= NEW_USER_TASK_COUNT_THRESHOLD) {
|
||||
this.cacheService.setGlobalState("isNewUser", false)
|
||||
this.stateManager.setGlobalState("isNewUser", false)
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ export class Controller {
|
||||
...autoApprovalSettings,
|
||||
version: (autoApprovalSettings.version ?? 1) + 1,
|
||||
}
|
||||
this.cacheService.setGlobalState("autoApprovalSettings", updatedAutoApprovalSettings)
|
||||
this.stateManager.setGlobalState("autoApprovalSettings", updatedAutoApprovalSettings)
|
||||
}
|
||||
// Apply remote feature flag gate to focus chain settings
|
||||
const effectiveFocusChainSettings = {
|
||||
@@ -219,7 +219,7 @@ export class Controller {
|
||||
defaultTerminalProfile ?? "default",
|
||||
enableCheckpointsSetting ?? true,
|
||||
await getCwd(getDesktopDir()),
|
||||
this.cacheService,
|
||||
this.stateManager,
|
||||
task,
|
||||
images,
|
||||
files,
|
||||
@@ -235,7 +235,7 @@ export class Controller {
|
||||
}
|
||||
|
||||
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
|
||||
this.cacheService.setGlobalState("telemetrySetting", telemetrySetting)
|
||||
this.stateManager.setGlobalState("telemetrySetting", telemetrySetting)
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
await this.postStateToWebview()
|
||||
@@ -245,14 +245,14 @@ export class Controller {
|
||||
const didSwitchToActMode = modeToSwitchTo === "act"
|
||||
|
||||
// Store mode to global state
|
||||
this.cacheService.setGlobalState("mode", modeToSwitchTo)
|
||||
this.stateManager.setGlobalState("mode", modeToSwitchTo)
|
||||
|
||||
// Capture mode switch telemetry | Capture regardless of if we know the taskId
|
||||
telemetryService.captureModeSwitch(this.task?.ulid ?? "0", modeToSwitchTo)
|
||||
|
||||
// Update API handler with new mode (buildApiHandler now selects provider based on mode)
|
||||
if (this.task) {
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const apiConfiguration = this.stateManager.getApiConfiguration()
|
||||
this.task.api = buildApiHandler({ ...apiConfiguration, ulid: this.task.ulid }, modeToSwitchTo)
|
||||
}
|
||||
|
||||
@@ -317,12 +317,12 @@ export class Controller {
|
||||
const clineProvider: ApiProvider = "cline"
|
||||
|
||||
// Get current settings to determine how to update providers
|
||||
const planActSeparateModelsSetting = this.cacheService.getGlobalStateKey("planActSeparateModelsSetting")
|
||||
const planActSeparateModelsSetting = this.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
|
||||
|
||||
const currentMode = await this.getCurrentMode()
|
||||
|
||||
// Get current API configuration from cache
|
||||
const currentApiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const currentApiConfiguration = this.stateManager.getApiConfiguration()
|
||||
|
||||
const updatedConfig = { ...currentApiConfiguration }
|
||||
|
||||
@@ -340,10 +340,10 @@ export class Controller {
|
||||
}
|
||||
|
||||
// Update the API configuration through cache service
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
this.stateManager.setApiConfiguration(updatedConfig)
|
||||
|
||||
// Mark welcome view as completed since user has successfully logged in
|
||||
this.cacheService.setGlobalState("welcomeViewCompleted", true)
|
||||
this.stateManager.setGlobalState("welcomeViewCompleted", true)
|
||||
|
||||
if (this.task) {
|
||||
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
|
||||
@@ -384,7 +384,7 @@ export class Controller {
|
||||
}
|
||||
|
||||
// Store in global state
|
||||
this.cacheService.setGlobalState("mcpMarketplaceCatalog", catalog)
|
||||
this.stateManager.setGlobalState("mcpMarketplaceCatalog", catalog)
|
||||
return catalog
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch MCP marketplace:", error)
|
||||
@@ -422,7 +422,7 @@ export class Controller {
|
||||
}
|
||||
|
||||
// Store in global state
|
||||
this.cacheService.setGlobalState("mcpMarketplaceCatalog", catalog)
|
||||
this.stateManager.setGlobalState("mcpMarketplaceCatalog", catalog)
|
||||
return catalog
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch MCP marketplace:", error)
|
||||
@@ -479,14 +479,14 @@ export class Controller {
|
||||
const currentMode = await this.getCurrentMode()
|
||||
|
||||
// Update API configuration through cache service
|
||||
const currentApiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const currentApiConfiguration = this.stateManager.getApiConfiguration()
|
||||
const updatedConfig = {
|
||||
...currentApiConfiguration,
|
||||
planModeApiProvider: openrouter,
|
||||
actModeApiProvider: openrouter,
|
||||
openRouterApiKey: apiKey,
|
||||
}
|
||||
this.cacheService.setApiConfiguration(updatedConfig)
|
||||
this.stateManager.setApiConfiguration(updatedConfig)
|
||||
|
||||
await this.postStateToWebview()
|
||||
if (this.task) {
|
||||
@@ -537,7 +537,7 @@ export class Controller {
|
||||
taskMetadataFilePath: string
|
||||
apiConversationHistory: Anthropic.MessageParam[]
|
||||
}> {
|
||||
const history = this.cacheService.getGlobalStateKey("taskHistory")
|
||||
const history = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
const historyItem = history.find((item) => item.id === id)
|
||||
if (historyItem) {
|
||||
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
|
||||
@@ -572,9 +572,9 @@ export class Controller {
|
||||
|
||||
async deleteTaskFromState(id: string) {
|
||||
// Remove the task from history
|
||||
const taskHistory = this.cacheService.getGlobalStateKey("taskHistory")
|
||||
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
|
||||
this.cacheService.setGlobalState("taskHistory", updatedTaskHistory)
|
||||
this.stateManager.setGlobalState("taskHistory", updatedTaskHistory)
|
||||
|
||||
// Notify the webview that the task has been deleted
|
||||
await this.postStateToWebview()
|
||||
@@ -589,39 +589,40 @@ export class Controller {
|
||||
|
||||
async getStateToPostToWebview(): Promise<ExtensionState> {
|
||||
// Get API configuration from cache for immediate access
|
||||
const apiConfiguration = this.cacheService.getApiConfiguration()
|
||||
const lastShownAnnouncementId = this.cacheService.getGlobalStateKey("lastShownAnnouncementId")
|
||||
const taskHistory = this.cacheService.getGlobalStateKey("taskHistory")
|
||||
const autoApprovalSettings = this.cacheService.getGlobalStateKey("autoApprovalSettings")
|
||||
const browserSettings = this.cacheService.getGlobalStateKey("browserSettings")
|
||||
const focusChainSettings = this.cacheService.getGlobalStateKey("focusChainSettings")
|
||||
const focusChainFeatureFlagEnabled = this.cacheService.getGlobalStateKey("focusChainFeatureFlagEnabled")
|
||||
const preferredLanguage = this.cacheService.getGlobalStateKey("preferredLanguage")
|
||||
const openaiReasoningEffort = this.cacheService.getGlobalStateKey("openaiReasoningEffort")
|
||||
const mode = this.cacheService.getGlobalStateKey("mode")
|
||||
const strictPlanModeEnabled = this.cacheService.getGlobalStateKey("strictPlanModeEnabled")
|
||||
const useAutoCondense = this.cacheService.getGlobalStateKey("useAutoCondense")
|
||||
const userInfo = this.cacheService.getGlobalStateKey("userInfo")
|
||||
const mcpMarketplaceEnabled = this.cacheService.getGlobalStateKey("mcpMarketplaceEnabled")
|
||||
const mcpDisplayMode = this.cacheService.getGlobalStateKey("mcpDisplayMode")
|
||||
const telemetrySetting = this.cacheService.getGlobalStateKey("telemetrySetting")
|
||||
const planActSeparateModelsSetting = this.cacheService.getGlobalStateKey("planActSeparateModelsSetting")
|
||||
const enableCheckpointsSetting = this.cacheService.getGlobalStateKey("enableCheckpointsSetting")
|
||||
const globalClineRulesToggles = this.cacheService.getGlobalStateKey("globalClineRulesToggles")
|
||||
const globalWorkflowToggles = this.cacheService.getGlobalStateKey("globalWorkflowToggles")
|
||||
const shellIntegrationTimeout = this.cacheService.getGlobalStateKey("shellIntegrationTimeout")
|
||||
const terminalReuseEnabled = this.cacheService.getGlobalStateKey("terminalReuseEnabled")
|
||||
const defaultTerminalProfile = this.cacheService.getGlobalStateKey("defaultTerminalProfile")
|
||||
const isNewUser = this.cacheService.getGlobalStateKey("isNewUser")
|
||||
const apiConfiguration = this.stateManager.getApiConfiguration()
|
||||
const lastShownAnnouncementId = this.stateManager.getGlobalStateKey("lastShownAnnouncementId")
|
||||
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
const autoApprovalSettings = this.stateManager.getGlobalStateKey("autoApprovalSettings")
|
||||
const browserSettings = this.stateManager.getGlobalStateKey("browserSettings")
|
||||
const focusChainSettings = this.stateManager.getGlobalStateKey("focusChainSettings")
|
||||
const focusChainFeatureFlagEnabled = this.stateManager.getGlobalStateKey("focusChainFeatureFlagEnabled")
|
||||
const preferredLanguage = this.stateManager.getGlobalStateKey("preferredLanguage")
|
||||
const openaiReasoningEffort = this.stateManager.getGlobalStateKey("openaiReasoningEffort")
|
||||
const mode = this.stateManager.getGlobalStateKey("mode")
|
||||
const strictPlanModeEnabled = this.stateManager.getGlobalStateKey("strictPlanModeEnabled")
|
||||
const useAutoCondense = this.stateManager.getGlobalStateKey("useAutoCondense")
|
||||
const userInfo = this.stateManager.getGlobalStateKey("userInfo")
|
||||
const mcpMarketplaceEnabled = this.stateManager.getGlobalStateKey("mcpMarketplaceEnabled")
|
||||
const mcpDisplayMode = this.stateManager.getGlobalStateKey("mcpDisplayMode")
|
||||
const telemetrySetting = this.stateManager.getGlobalStateKey("telemetrySetting")
|
||||
const planActSeparateModelsSetting = this.stateManager.getGlobalStateKey("planActSeparateModelsSetting")
|
||||
const enableCheckpointsSetting = this.stateManager.getGlobalStateKey("enableCheckpointsSetting")
|
||||
const globalClineRulesToggles = this.stateManager.getGlobalStateKey("globalClineRulesToggles")
|
||||
const globalWorkflowToggles = this.stateManager.getGlobalStateKey("globalWorkflowToggles")
|
||||
const shellIntegrationTimeout = this.stateManager.getGlobalStateKey("shellIntegrationTimeout")
|
||||
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
|
||||
const defaultTerminalProfile = this.stateManager.getGlobalStateKey("defaultTerminalProfile")
|
||||
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
|
||||
const welcomeViewCompleted = Boolean(
|
||||
this.cacheService.getGlobalStateKey("welcomeViewCompleted") || this.authService.getInfo()?.user?.uid,
|
||||
this.stateManager.getGlobalStateKey("welcomeViewCompleted") || this.authService.getInfo()?.user?.uid,
|
||||
)
|
||||
const mcpResponsesCollapsed = this.cacheService.getGlobalStateKey("mcpResponsesCollapsed")
|
||||
const terminalOutputLineLimit = this.cacheService.getGlobalStateKey("terminalOutputLineLimit")
|
||||
const localClineRulesToggles = this.cacheService.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const localWindsurfRulesToggles = this.cacheService.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
const localCursorRulesToggles = this.cacheService.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
const workflowToggles = this.cacheService.getWorkspaceStateKey("workflowToggles")
|
||||
const customPrompt = this.stateManager.getGlobalStateKey("customPrompt")
|
||||
const mcpResponsesCollapsed = this.stateManager.getGlobalStateKey("mcpResponsesCollapsed")
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalStateKey("terminalOutputLineLimit")
|
||||
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
const localCursorRulesToggles = this.stateManager.getWorkspaceStateKey("localCursorRulesToggles")
|
||||
const workflowToggles = this.stateManager.getWorkspaceStateKey("workflowToggles")
|
||||
|
||||
const currentTaskItem = this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined
|
||||
const checkpointTrackerErrorMessage = this.task?.taskState.checkpointTrackerErrorMessage
|
||||
@@ -679,6 +680,7 @@ export class Controller {
|
||||
welcomeViewCompleted: welcomeViewCompleted as boolean, // Can be undefined but is set to either true or false by the migration that runs on extension launch in extension.ts
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
customPrompt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,14 +710,14 @@ export class Controller {
|
||||
*/
|
||||
|
||||
async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
|
||||
const history = this.cacheService.getGlobalStateKey("taskHistory")
|
||||
const history = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
const existingItemIndex = history.findIndex((h) => h.id === item.id)
|
||||
if (existingItemIndex !== -1) {
|
||||
history[existingItemIndex] = item
|
||||
} else {
|
||||
history.push(item)
|
||||
}
|
||||
this.cacheService.setGlobalState("taskHistory", history)
|
||||
this.stateManager.setGlobalState("taskHistory", history)
|
||||
return history
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function refreshBasetenModels(
|
||||
const basetenModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.basetenModels)
|
||||
|
||||
// Get the Baseten API key from the controller's state
|
||||
const basetenApiKey = controller.cacheService.getSecretKey("basetenApiKey")
|
||||
const basetenApiKey = controller.stateManager.getSecretKey("basetenApiKey")
|
||||
|
||||
const models: Record<string, Partial<OpenRouterModelInfo>> = {}
|
||||
try {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Controller } from ".."
|
||||
export async function refreshGroqModels(controller: Controller, _request: EmptyRequest): Promise<OpenRouterCompatibleModelInfo> {
|
||||
const groqModelsFilePath = path.join(await ensureCacheDirectoryExists(controller), GlobalFileNames.groqModels)
|
||||
|
||||
const groqApiKey = controller.cacheService.getSecretKey("groqApiKey")
|
||||
const groqApiKey = controller.stateManager.getSecretKey("groqApiKey")
|
||||
|
||||
let models: Record<string, Partial<OpenRouterModelInfo>> = {}
|
||||
try {
|
||||
|
||||
@@ -181,6 +181,7 @@ export async function refreshOpenRouterModels(
|
||||
case "openai/gpt-5-mini":
|
||||
case "openai/gpt-5-nano":
|
||||
modelInfo.maxTokens = 8_192 // 128000 breaks context window truncation
|
||||
modelInfo.contextWindow = 272_000 // openrouter reports 400k but the input limit is actually 400k-128k
|
||||
break
|
||||
default:
|
||||
if (rawModel.id.startsWith("openai/")) {
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function refreshRequestyModels(controller: Controller, _: EmptyRequ
|
||||
|
||||
const models: Record<string, OpenRouterModelInfo> = {}
|
||||
try {
|
||||
const apiKey = controller.cacheService.getSecretKey("requestyApiKey")
|
||||
const apiKey = controller.stateManager.getSecretKey("requestyApiKey")
|
||||
const headers = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function updateApiConfigurationProto(
|
||||
const appApiConfiguration = convertProtoToApiConfiguration(request.apiConfiguration)
|
||||
|
||||
// Update the API configuration in storage
|
||||
controller.cacheService.setApiConfiguration(appApiConfiguration)
|
||||
controller.stateManager.setApiConfiguration(appApiConfiguration)
|
||||
|
||||
// Update the task's API handler if there's an active task
|
||||
if (controller.task) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { Controller } from "../index"
|
||||
export async function setWelcomeViewCompleted(controller: Controller, request: BooleanRequest): Promise<Empty> {
|
||||
try {
|
||||
// Update the global state to set welcomeViewCompleted to the requested value
|
||||
controller.cacheService.setGlobalState("welcomeViewCompleted", request.value)
|
||||
controller.stateManager.setGlobalState("welcomeViewCompleted", request.value)
|
||||
|
||||
await controller.postStateToWebview()
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function toggleFavoriteModel(controller: Controller, request: Strin
|
||||
}
|
||||
|
||||
const modelId = request.value
|
||||
const apiConfiguration = controller.cacheService.getApiConfiguration()
|
||||
const apiConfiguration = controller.stateManager.getApiConfiguration()
|
||||
|
||||
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function toggleFavoriteModel(controller: Controller, request: Strin
|
||||
...apiConfiguration,
|
||||
favoritedModelIds: updatedFavorites,
|
||||
}
|
||||
controller.cacheService.setApiConfiguration(updatedApiConfiguration)
|
||||
controller.stateManager.setApiConfiguration(updatedApiConfiguration)
|
||||
|
||||
// Capture telemetry for model favorite toggle
|
||||
const isFavorited = !favoritedModelIds.includes(modelId)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user