mirror of
https://github.com/cline/cline.git
synced 2026-09-12 09:14:50 +08:00
Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fe23fe319 | ||
|
|
963abc190e | ||
|
|
631a7d6566 | ||
|
|
e43ab0ea7a | ||
|
|
242e3321a2 | ||
|
|
ea6cb4b29e | ||
|
|
2c75285566 | ||
|
|
8279f2e145 | ||
|
|
5b94ba3ef9 | ||
|
|
703146182a | ||
|
|
9603643b77 | ||
|
|
c6dce7fb17 | ||
|
|
cc0d4ae6cb | ||
|
|
a9365e30e9 | ||
|
|
efe468d9b1 | ||
|
|
1b6202604d | ||
|
|
ea1dbd8bea | ||
|
|
e43517519e | ||
|
|
520d08c5f2 | ||
|
|
31a55ac87e | ||
|
|
a442983742 | ||
|
|
7b71eff294 | ||
|
|
6d1890f8bb | ||
|
|
11d17fc17e | ||
|
|
42a3dc6150 | ||
|
|
4032e51e8d | ||
|
|
1bbc90487c | ||
|
|
d422ebbb27 | ||
|
|
7470d234ef | ||
|
|
21b81f1844 | ||
|
|
362429a317 | ||
|
|
09cb9ac9ac | ||
|
|
94160faeef | ||
|
|
f526f70e3a | ||
|
|
d9b47378c6 | ||
|
|
0671c59e6d | ||
|
|
bf87887501 | ||
|
|
66a81a6efa | ||
|
|
eee64c5204 | ||
|
|
748ba99c1c | ||
|
|
82b1a01644 | ||
|
|
1c6307e8ad | ||
|
|
58d9c0af18 | ||
|
|
bf213c24ea | ||
|
|
80cceaa3ae | ||
|
|
359e088eb6 | ||
|
|
47a464defe | ||
|
|
38f619cfd9 | ||
|
|
050773ac31 | ||
|
|
6d67ff0b94 | ||
|
|
18f4ef8b49 | ||
|
|
085e69d142 | ||
|
|
46aa66ed9d | ||
|
|
2ebbe954d9 | ||
|
|
067f5eea09 | ||
|
|
2a48bad28c | ||
|
|
c6f4584f7d | ||
|
|
a17b31070f | ||
|
|
cad82d518d | ||
|
|
b4d7ec187f | ||
|
|
42af8414e4 | ||
|
|
8f6b9e8362 | ||
|
|
f1430359db | ||
|
|
932695f70b | ||
|
|
489ee936c2 | ||
|
|
dff7f61175 | ||
|
|
aead42c6b8 | ||
|
|
db50a1c671 | ||
|
|
bb20f60f1d | ||
|
|
a7333b7177 | ||
|
|
f30837a850 | ||
|
|
5660b2513f | ||
|
|
64e7e5fa4c |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add create-pull-request skill
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix the selection of remotely configured providers
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Harden act_mode_respond to prevent consecutive calls
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: Verify selected index is not -1 when checking if an option is selectable in the context menu
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
name: create-pull-request
|
||||
description: Create a GitHub pull request following project conventions. Use when the user asks to create a PR, submit changes for review, or open a pull request. Handles commit analysis, branch management, and PR creation using the gh CLI tool.
|
||||
---
|
||||
|
||||
# Create Pull Request
|
||||
|
||||
This skill guides you through creating a well-structured GitHub pull request that follows project conventions and best practices.
|
||||
|
||||
## Prerequisites Check
|
||||
|
||||
Before proceeding, verify the following:
|
||||
|
||||
### 1. Check if `gh` CLI is installed
|
||||
|
||||
```bash
|
||||
gh --version
|
||||
```
|
||||
|
||||
If not installed, inform the user:
|
||||
> The GitHub CLI (`gh`) is required but not installed. Please install it:
|
||||
> - macOS: `brew install gh`
|
||||
> - Other: https://cli.github.com/
|
||||
|
||||
### 2. Check if authenticated with GitHub
|
||||
|
||||
```bash
|
||||
gh auth status
|
||||
```
|
||||
|
||||
If not authenticated, guide the user to run `gh auth login`.
|
||||
|
||||
### 3. Verify clean working directory
|
||||
|
||||
```bash
|
||||
git status
|
||||
```
|
||||
|
||||
If there are uncommitted changes, ask the user whether to:
|
||||
- Commit them as part of this PR
|
||||
- Stash them temporarily
|
||||
- Discard them (with caution)
|
||||
|
||||
## Gather Context
|
||||
|
||||
### 1. Identify the current branch
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
Ensure you're not on `main` or `master`. If so, ask the user to create or switch to a feature branch.
|
||||
|
||||
### 2. Find the base branch
|
||||
|
||||
```bash
|
||||
git remote show origin | grep "HEAD branch"
|
||||
```
|
||||
|
||||
This is typically `main` or `master`.
|
||||
|
||||
### 3. Analyze recent commits relevant to this PR
|
||||
|
||||
```bash
|
||||
git log origin/main..HEAD --oneline --no-decorate
|
||||
```
|
||||
|
||||
Review these commits to understand:
|
||||
- What changes are being introduced
|
||||
- The scope of the PR (single feature/fix or multiple changes)
|
||||
- Whether commits should be squashed or reorganized
|
||||
|
||||
### 4. Review the diff
|
||||
|
||||
```bash
|
||||
git diff origin/main..HEAD --stat
|
||||
```
|
||||
|
||||
This shows which files changed and helps identify the type of change.
|
||||
|
||||
## Information Gathering
|
||||
|
||||
Before creating the PR, you need the following information. Check if it can be inferred from:
|
||||
- Commit messages
|
||||
- Branch name (e.g., `fix/issue-123`, `feature/new-login`)
|
||||
- Changed files and their content
|
||||
|
||||
If any critical information is missing, use `ask_followup_question` to ask the user:
|
||||
|
||||
### Required Information
|
||||
|
||||
1. **Related Issue Number**: Look for patterns like `#123`, `fixes #123`, or `closes #123` in commit messages
|
||||
2. **Description**: What problem does this solve? Why were these changes made?
|
||||
3. **Type of Change**: Bug fix, new feature, breaking change, refactor, cosmetic, documentation, or workflow
|
||||
4. **Test Procedure**: How was this tested? What could break?
|
||||
|
||||
### Example clarifying question
|
||||
|
||||
If the issue number is not found:
|
||||
> I couldn't find a related issue number in the commit messages or branch name. What GitHub issue does this PR address? (Enter the issue number, e.g., "123" or "N/A" for small fixes)
|
||||
|
||||
## Git Best Practices
|
||||
|
||||
Before creating the PR, consider these best practices:
|
||||
|
||||
### Commit Hygiene
|
||||
|
||||
1. **Atomic commits**: Each commit should represent a single logical change
|
||||
2. **Clear commit messages**: Follow conventional commit format when possible
|
||||
3. **No merge commits**: Prefer rebasing over merging to keep history clean
|
||||
|
||||
### Branch Management
|
||||
|
||||
1. **Rebase on latest main** (if needed):
|
||||
```bash
|
||||
git fetch origin
|
||||
git rebase origin/main
|
||||
```
|
||||
|
||||
2. **Squash if appropriate**: If there are many small "WIP" commits, consider interactive rebase:
|
||||
```bash
|
||||
git rebase -i origin/main
|
||||
```
|
||||
Only suggest this if commits appear messy and the user is comfortable with rebasing.
|
||||
|
||||
### Push Changes
|
||||
|
||||
Ensure all commits are pushed:
|
||||
```bash
|
||||
git push origin HEAD
|
||||
```
|
||||
|
||||
If the branch was rebased, you may need:
|
||||
```bash
|
||||
git push origin HEAD --force-with-lease
|
||||
```
|
||||
|
||||
## Create the Pull Request
|
||||
|
||||
**IMPORTANT**: Read and use the PR template at `.github/pull_request_template.md`. The PR body format must **strictly match** the template structure. Do not deviate from the template format.
|
||||
|
||||
When filling out the template:
|
||||
- Replace `#XXXX` with the actual issue number, or keep as `#XXXX` if no issue exists (for small fixes)
|
||||
- Fill in all sections with relevant information gathered from commits and context
|
||||
- Mark the appropriate "Type of Change" checkbox(es)
|
||||
- Complete the "Pre-flight Checklist" items that apply
|
||||
|
||||
### Create PR with gh CLI
|
||||
|
||||
```bash
|
||||
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main
|
||||
```
|
||||
|
||||
Alternatively, create as draft if the user wants review before marking ready:
|
||||
```bash
|
||||
gh pr create --title "PR_TITLE" --body "PR_BODY" --base main --draft
|
||||
```
|
||||
|
||||
## Post-Creation
|
||||
|
||||
After creating the PR:
|
||||
|
||||
1. **Display the PR URL** so the user can review it
|
||||
2. **Remind about CI checks**: Tests and linting will run automatically
|
||||
3. **Suggest next steps**:
|
||||
- Add reviewers if needed: `gh pr edit --add-reviewer USERNAME`
|
||||
- Add labels if needed: `gh pr edit --add-label "bug"`
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **No commits ahead of main**: The branch has no changes to submit
|
||||
- Ask if the user meant to work on a different branch
|
||||
|
||||
2. **Branch not pushed**: Remote doesn't have the branch
|
||||
- Push the branch first: `git push -u origin HEAD`
|
||||
|
||||
3. **PR already exists**: A PR for this branch already exists
|
||||
- Show the existing PR: `gh pr view`
|
||||
- Ask if they want to update it instead
|
||||
|
||||
4. **Merge conflicts**: Branch conflicts with base
|
||||
- Guide user through resolving conflicts or rebasing
|
||||
|
||||
## Summary Checklist
|
||||
|
||||
Before finalizing, ensure:
|
||||
- [ ] `gh` CLI is installed and authenticated
|
||||
- [ ] Working directory is clean
|
||||
- [ ] All commits are pushed
|
||||
- [ ] Branch is up-to-date with base branch
|
||||
- [ ] Related issue number is identified, or placeholder is used
|
||||
- [ ] PR description follows the template exactly
|
||||
- [ ] Appropriate type of change is selected
|
||||
- [ ] Pre-flight checklist items are addressed
|
||||
+3
-3
@@ -72,12 +72,12 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
|
||||
# Example configurations:
|
||||
#
|
||||
# Console debugging (logs only):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_TELEMETRY_ENABLED=true
|
||||
# OTEL_LOGS_EXPORTER=console
|
||||
# TEL_DEBUG_DIAGNOSTICS=true
|
||||
#
|
||||
# OTLP with gRPC (insecure, for local testing):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_TELEMETRY_ENABLED=true
|
||||
# OTEL_LOGS_EXPORTER=otlp
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
|
||||
@@ -85,7 +85,7 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
|
||||
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
|
||||
#
|
||||
# OTLP with HTTP/JSON (production):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_TELEMETRY_ENABLED=true
|
||||
# OTEL_LOGS_EXPORTER=otlp
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/docs/
|
||||
/.github/ @saoudrizwan @garoth @sjf
|
||||
/README.md @saoudrizwan @nickbaumann98
|
||||
/src/core/storage/ @celestial-vault @abeatrix
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
name: Cline PR Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
[opened, ready_for_review]
|
||||
# Manual trigger for backfilling existing PRs. Run from terminal:
|
||||
# gh workflow run cline-pr-review.yml -f pr_number=1234
|
||||
# Or batch process open PRs:
|
||||
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
|
||||
# gh workflow run cline-pr-review.yml -f pr_number=$num
|
||||
# sleep 60
|
||||
# done
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: "PR number to review"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: pr-review-${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cline-pr-review:
|
||||
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
# SECURITY: These permissions are intentionally restrictive.
|
||||
# - contents: read -> cline can read the codebase but CANNOT write/push any code
|
||||
# - pull-requests: write -> cline can post reviews and inline suggestions
|
||||
# - issues: read -> cline can search for related issues
|
||||
# NOTE: Even with pull-requests: write, cline CANNOT merge PRs because branch protection
|
||||
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
|
||||
- name: Install and Verify Cline CLI
|
||||
run: |
|
||||
npx cline version # verify installation
|
||||
|
||||
- name: Configure Cline with Anthropic
|
||||
run: |
|
||||
npx cline auth --provider anthropic \
|
||||
--apikey "${{ secrets.ANTHROPIC_API_KEY }}" \
|
||||
--modelid claude-opus-4-5-20251101
|
||||
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Review PR with Cline
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.pr.outputs.number }}
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CLINE_COMMAND_PERMISSIONS: |
|
||||
{
|
||||
"allow": [
|
||||
"gh pr diff *",
|
||||
"gh pr view *",
|
||||
"gh pr checks *",
|
||||
"gh pr list *",
|
||||
"gh label list *",
|
||||
"gh issue list *",
|
||||
"gh issue view *",
|
||||
"git log *",
|
||||
"gh pr comment ${{ steps.pr.outputs.number }} *",
|
||||
"gh pr edit ${{ steps.pr.outputs.number }} *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews *"
|
||||
]
|
||||
}
|
||||
run: |
|
||||
npx cline --yolo 'You'\''re a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
|
||||
|
||||
PR: #'"${PR_NUMBER}"'
|
||||
|
||||
## Gather context
|
||||
|
||||
```bash
|
||||
# Get full PR details
|
||||
gh pr view '"${PR_NUMBER}"' --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
|
||||
|
||||
# Get the diff
|
||||
gh pr diff '"${PR_NUMBER}"'
|
||||
|
||||
# Check CI status
|
||||
gh pr checks '"${PR_NUMBER}"'
|
||||
|
||||
# Get existing review comments (to understand context and your previous feedback)
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/comments --jq '\''.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'\''
|
||||
|
||||
# Get conversation comments
|
||||
gh pr view '"${PR_NUMBER}"' --comments
|
||||
```
|
||||
|
||||
If this is a re-review (workflow_dispatch event):
|
||||
Read your previous comments carefully. Understand what you asked for before.
|
||||
Check if new commits or comments address your previous feedback.
|
||||
|
||||
## Check contributing guidelines
|
||||
|
||||
Flag (but don'\''t block) if:
|
||||
- Missing changeset - For user-facing changes, check if there'\''s a `.changeset/` file:
|
||||
```bash
|
||||
gh pr diff '"${PR_NUMBER}"' --name-only | grep '\''.changeset/'\'' || echo '\''No changeset found'\''
|
||||
```
|
||||
If missing, ask them to run `npm run changeset`
|
||||
- Missing tests - New features should have tests
|
||||
|
||||
## Find related issues and PRs
|
||||
|
||||
Search thoroughly for context that might help with the review:
|
||||
|
||||
```bash
|
||||
# Find related issues for context
|
||||
gh issue list --search '\''<keywords from the PR>'\'' --state all --limit 30
|
||||
gh issue list --search '\''<error messages or feature names>'\'' --state all --limit 20
|
||||
|
||||
# Find similar PRs for reference
|
||||
gh pr list --search '\''<keywords>'\'' --state all --limit 30
|
||||
```
|
||||
|
||||
For each relevant issue or PR you find, read it including comments:
|
||||
```bash
|
||||
gh issue view <number> --comments
|
||||
gh pr view <number> --comments
|
||||
```
|
||||
|
||||
Look for:
|
||||
- Open issues this PR might fix that weren'\''t linked in the description
|
||||
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
|
||||
- Context from maintainer discussions that could inform your review
|
||||
|
||||
## Find subject matter experts
|
||||
|
||||
For files changed in this PR, find who knows the code best:
|
||||
```bash
|
||||
# Get files changed
|
||||
gh pr diff '"${PR_NUMBER}"' --name-only
|
||||
|
||||
# For each relevant path, find contributors
|
||||
git log --since='\''6 months ago'\'' --format='\''%an'\'' -- <path> | sort | uniq -c | sort -rn | head -5
|
||||
```
|
||||
|
||||
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
|
||||
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username1 | Authored PR #X which modified this area |
|
||||
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
|
||||
| @username3 | Reviewed similar PR #Y with extensive feedback |
|
||||
|
||||
## Bash command usage
|
||||
|
||||
Don'\''t use operators like `|`, `&&`, or `;` - run each command separately and analyze the output.
|
||||
|
||||
When referencing command outputs, quote them properly to avoid formatting issues.
|
||||
|
||||
## Deep code review
|
||||
|
||||
This is the most important part. Don'\''t just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
|
||||
|
||||
Step 1: Understand the intent
|
||||
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
|
||||
|
||||
Step 2: Form your own opinion first
|
||||
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
|
||||
|
||||
Step 3: Compare approaches
|
||||
Now look at their implementation. How does it compare to what you would have done?
|
||||
- Is their approach better in some ways? Note what they did well.
|
||||
- Is their approach missing something? Be specific about what and why.
|
||||
- Are there edge cases they haven'\''t considered?
|
||||
- Does it follow the patterns established in similar parts of the codebase?
|
||||
|
||||
Step 4: Look at the bigger picture
|
||||
- What other files or systems does this change interact with?
|
||||
- Could this break anything else?
|
||||
- Is there additional work needed beyond this PR to complete the feature?
|
||||
- Does this fit well with the overall architecture?
|
||||
|
||||
Step 5: Find reference implementations
|
||||
Look for similar changes in the codebase:
|
||||
```bash
|
||||
git log --oneline --all --grep='\''<relevant keywords>'\'' | head -20
|
||||
git log --oneline -- <similar files> | head -20
|
||||
```
|
||||
|
||||
If this is adding a new API provider, look at how other providers are implemented.
|
||||
If this is adding a new feature, look at how similar features were added.
|
||||
Note where their implementation aligns with or diverges from established patterns.
|
||||
|
||||
Step 6: Standard code review checks
|
||||
- DRY: Is there duplicated code that could be extracted?
|
||||
- Error handling: Are errors handled appropriately?
|
||||
- Security: Any injection risks, credential exposure, unsafe dependencies?
|
||||
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
|
||||
- Types: Is TypeScript used correctly? Any unsafe type assertions?
|
||||
- Naming: Are variables and functions named clearly?
|
||||
- Comments: Is complex logic explained? Are there outdated comments?
|
||||
|
||||
## Inline code suggestions
|
||||
|
||||
For specific code improvements, use GitHub'\''s suggestion syntax via `gh api`.
|
||||
This creates suggestions the author can commit with one click.
|
||||
|
||||
Single-line suggestion:
|
||||
```bash
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
|
||||
-f event='\''COMMENT'\'' \
|
||||
-f body='\'''\'' \
|
||||
-F comments='\''[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"line": 42,
|
||||
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
|
||||
}
|
||||
]'\''
|
||||
```
|
||||
|
||||
Multi-line suggestion (replacing lines 40-45):
|
||||
```bash
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
|
||||
-f event='\''COMMENT'\'' \
|
||||
-f body='\'''\'' \
|
||||
-F comments='\''[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"start_line": 40,
|
||||
"line": 45,
|
||||
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
|
||||
}
|
||||
]'\''
|
||||
```
|
||||
|
||||
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
|
||||
|
||||
## Post your review
|
||||
|
||||
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
|
||||
|
||||
Start with a warm thank you for their contribution. Be conversational, not robotic.
|
||||
|
||||
Include what'\''s relevant:
|
||||
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author'\''s intent, why they made the changes, how they implemented it, and what files/systems are affected. Don'\''t just summarize - explain.
|
||||
- Related issues/PRs you found that provide useful context (link to them)
|
||||
- Your review findings (issues to address, suggestions, etc.)
|
||||
- Clear next steps for the author
|
||||
|
||||
Include a '\''For Maintainers'\'' section with:
|
||||
- Anything else useful to help the maintainer resolve this PR
|
||||
- Related issues/PRs with context on why they'\''re relevant
|
||||
- Open issues this PR might fix that weren'\''t linked in the description
|
||||
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
|
||||
- SME table - who should review this and why
|
||||
|
||||
For the SME table:
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username | Primary contributor to affected files |
|
||||
|
||||
## Update labels
|
||||
|
||||
Add appropriate labels based on your analysis:
|
||||
```bash
|
||||
gh label list --json name,description --limit 100
|
||||
gh pr edit '"${PR_NUMBER}"' --add-label '\''label1,label2'\''
|
||||
```
|
||||
|
||||
When done, add the reviewed label:
|
||||
```bash
|
||||
gh pr edit '"${PR_NUMBER}"' --add-label '\''Bot Reviewed'\''
|
||||
```
|
||||
|
||||
## Remember
|
||||
|
||||
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like '\''let me know if you have questions'\'', '\''I can help you with'\'', or '\''feel free to ask'\'' - you won'\''t be there to follow up. Frame any questions for the maintainers who will follow up.
|
||||
- Be helpful and welcoming - Many contributors are new to the project
|
||||
- Be specific - Point to exact lines and suggest fixes, don'\''t give vague feedback
|
||||
- Think deeply - Don'\''t just surface-level review, understand the intent and evaluate the approach
|
||||
- Use inline suggestions - Make it easy for authors to accept changes
|
||||
- You'\''re a first-pass reviewer - A human maintainer will do final approval'
|
||||
@@ -0,0 +1,130 @@
|
||||
name: Publish NPM Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirm_publish:
|
||||
description: 'Type "publish" to confirm you want to publish to NPM'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish-npm-release:
|
||||
needs: test
|
||||
name: Publish Cline CLI to NPM
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && github.event.inputs.confirm_publish == 'publish'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
cache-dependency-path: cli/go.sum
|
||||
|
||||
# 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') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- name: Read release version
|
||||
id: version
|
||||
run: |
|
||||
# Read version from cli/package.json (stable version)
|
||||
VERSION=$(node -p "require('./cli/package.json').version")
|
||||
echo "Release version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
run: npm run download-ripgrep
|
||||
|
||||
- name: Clean previous builds
|
||||
run: rm -rf dist-standalone
|
||||
|
||||
- name: Generate Protos (First Pass)
|
||||
run: npm run protos && npm run protos-go
|
||||
|
||||
- name: Compile CLI
|
||||
run: npm run compile-cli
|
||||
|
||||
- name: Compile CLI for all platforms
|
||||
run: npm run compile-cli-all-platforms
|
||||
|
||||
- name: Build standalone NPM package
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
OTEL_TELEMETRY_ENABLED: "1"
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: npm run compile-standalone-npm
|
||||
|
||||
- name: Generate Protos (Second Pass - Bug Workaround)
|
||||
run: npm run protos && npm run protos-go
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
echo "Checking dist-standalone directory..."
|
||||
ls -la dist-standalone/
|
||||
|
||||
echo "Verifying CLI binaries..."
|
||||
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
|
||||
|
||||
echo "Checking package.json in dist-standalone..."
|
||||
cat dist-standalone/package.json | grep version
|
||||
|
||||
- name: Publish to NPM with latest tag
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'latest'..."
|
||||
cd dist-standalone
|
||||
npm publish --tag latest --access public
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'latest'"
|
||||
echo ""
|
||||
echo "📦 Install with: npm install -g cline"
|
||||
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
|
||||
@@ -0,0 +1,175 @@
|
||||
name: Publish NPM Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish-npm-nightly:
|
||||
needs: test
|
||||
name: Publish Cline CLI (Nightly) to NPM
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for recent commits
|
||||
id: check_commits
|
||||
run: |
|
||||
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
|
||||
echo "No commits in last 24 hours, skipping publish"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Found recent commits, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup Go
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
cache-dependency-path: cli/go.sum
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
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
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- name: Generate nightly version with timestamp
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
id: version
|
||||
run: |
|
||||
# Read base version from cli/package.json (e.g., "1.0.9")
|
||||
BASE_VERSION=$(node -p "require('./cli/package.json').version")
|
||||
|
||||
# Generate timestamp (Unix epoch seconds)
|
||||
TIMESTAMP=$(date +%s)
|
||||
|
||||
# Create unique nightly version: 1.0.9-nightly.1736365200
|
||||
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
|
||||
|
||||
echo "Base version: $BASE_VERSION"
|
||||
echo "Generated nightly version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update cli/package.json with nightly version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
# Update version with timestamp-based nightly version
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('cli/package.json', 'utf8'));
|
||||
pkg.version = '${{ steps.version.outputs.version }}';
|
||||
fs.writeFileSync('cli/package.json', JSON.stringify(pkg, null, '\t'));
|
||||
"
|
||||
|
||||
echo "Using version ${{ steps.version.outputs.version }} for build"
|
||||
cat cli/package.json | grep '"version"'
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run download-ripgrep
|
||||
|
||||
- name: Clean previous builds
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: rm -rf dist-standalone
|
||||
|
||||
- name: Generate Protos (First Pass)
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run protos && npm run protos-go
|
||||
|
||||
- name: Compile CLI
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run compile-cli
|
||||
|
||||
- name: Compile CLI for all platforms
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run compile-cli-all-platforms
|
||||
|
||||
- name: Build standalone NPM package
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
OTEL_TELEMETRY_ENABLED: "1"
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: npm run compile-standalone-npm
|
||||
|
||||
- name: Generate Protos (Second Pass - Bug Workaround)
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run protos && npm run protos-go
|
||||
|
||||
- name: Verify build output
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "Checking dist-standalone directory..."
|
||||
ls -la dist-standalone/
|
||||
|
||||
echo "Verifying CLI binaries..."
|
||||
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
|
||||
|
||||
echo "Checking package.json in dist-standalone..."
|
||||
cat dist-standalone/package.json | grep version
|
||||
|
||||
- name: Publish to NPM with nightly tag
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
|
||||
cd dist-standalone
|
||||
npm publish --tag nightly --access public
|
||||
|
||||
- name: Summary
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'nightly'"
|
||||
echo ""
|
||||
echo "📦 Install with: npm install -g cline@nightly"
|
||||
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
|
||||
@@ -29,6 +29,8 @@ coverage-unit
|
||||
|
||||
*evals.env
|
||||
.env
|
||||
.secrets
|
||||
.github/act/.secrets
|
||||
|
||||
.worktrees
|
||||
|
||||
@@ -39,3 +41,8 @@ webview-ui/src/services/grpc-client.ts
|
||||
|
||||
# E2E Tests
|
||||
test-results
|
||||
|
||||
/.github/act
|
||||
/pkg
|
||||
.secrets
|
||||
|
||||
|
||||
+39
-1
@@ -1,8 +1,46 @@
|
||||
# Changelog
|
||||
|
||||
## [3.49.1]
|
||||
|
||||
### Added
|
||||
|
||||
- Add telemetry to track usage of skills feature
|
||||
- Add version headers to Cline backend requests
|
||||
- Phase in Responses API usage instead of defaulting for every supported model
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix workflow slash command search to be case-insensitive
|
||||
- Fix model display in ModelPickerModal when using LiteLLM
|
||||
- Fix LiteLLM model fetching with default base URL
|
||||
- Fix crash when OpenAI-compatible APIs send usage chunks with empty or null choices arrays at end of streaming
|
||||
- Fix model ID for Kat Coder Pro Free model
|
||||
|
||||
## [3.49.0]
|
||||
|
||||
- Enable configuring an OTEL collector at runtime
|
||||
- Removing Minimax-2.1 from free model list as the free trial has ended
|
||||
- Improved image display in MCP responses
|
||||
- Auto-sync remote MCP servers from remote config to local settings
|
||||
|
||||
## [3.48.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add Skills system for reusable, on-demand agent instructions
|
||||
- Add new websearch tooling in Cline provider
|
||||
- Add zai-glm-4.7 to Cerebras model list
|
||||
- Add model refresh and improve reasoning support for Vercel AI Gateway
|
||||
|
||||
### Fixed
|
||||
|
||||
- Revert #8341 due to regressions in diff view/document truncation (see #8423, #8429)
|
||||
- Fixed extension crash when using context menu selector
|
||||
|
||||
## [3.47.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Added experimental support for Background Edits (allows editing files in background without opening the diff view)
|
||||
- Updated free model to MiniMax M2.1 (replacing MiniMax M2)
|
||||
- Added support for Azure based identity authentication in OpenAI Compatible provider and Azure OpenAI
|
||||
@@ -1680,4 +1718,4 @@ Add Opus 4.1 through Claude Code
|
||||
|
||||
## [0.0.6]
|
||||
|
||||
- Initial release
|
||||
- Initial release
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "cline-core.js",
|
||||
"bin": {
|
||||
|
||||
@@ -358,6 +358,9 @@ func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cli
|
||||
if openRouterInfo, ok := modelInfo.(*cline.OpenRouterModelInfo); ok {
|
||||
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
|
||||
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
|
||||
} else if ocaInfo, ok := modelInfo.(*cline.OcaModelInfo); ok {
|
||||
apiConfig.PlanModeOcaModelInfo = ocaInfo
|
||||
apiConfig.ActModeOcaModelInfo = ocaInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +429,9 @@ func UpdateProviderPartial(ctx context.Context, manager *task.Manager, provider
|
||||
if openRouterInfo, ok := updates.ModelInfo.(*cline.OpenRouterModelInfo); ok {
|
||||
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
|
||||
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
|
||||
} else if ocaInfo, ok := updates.ModelInfo.(*cline.OcaModelInfo); ok {
|
||||
apiConfig.PlanModeOcaModelInfo = ocaInfo
|
||||
apiConfig.ActModeOcaModelInfo = ocaInfo
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
|
||||
}
|
||||
|
||||
// Step 3: Select model
|
||||
modelID, _, err := pw.selectModel(cline.ApiProvider_OCA, "")
|
||||
modelID, modelInfo, err := pw.selectModel(cline.ApiProvider_OCA, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("model selection failed: %w", err)
|
||||
}
|
||||
@@ -198,7 +198,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
|
||||
// Step 4: Apply the OCA model configuration and set as active
|
||||
updates := ProviderUpdatesPartial{
|
||||
ModelID: &modelID,
|
||||
ModelInfo: nil,
|
||||
ModelInfo: modelInfo,
|
||||
}
|
||||
|
||||
if err := UpdateProviderPartial(pw.ctx, pw.manager, cline.ApiProvider_OCA, updates, true); err != nil {
|
||||
|
||||
@@ -339,6 +339,13 @@ func (tr *ToolRenderer) RenderCommandOutput(output string) string {
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func (tr *ToolRenderer) RenderCommandPermissionDenied(command string) string {
|
||||
command = strings.TrimSpace(command)
|
||||
rendered := tr.renderMarkdown("### Command was denied")
|
||||
message := fmt.Sprintf("Cline does not have permission to execute this command: `%s`", command)
|
||||
return fmt.Sprintf("\n%s\n\n%s\n", rendered, message)
|
||||
}
|
||||
|
||||
// RenderUserResponse renders user approval/rejection feedback
|
||||
func (tr *ToolRenderer) RenderUserResponse(approved bool, feedback string) string {
|
||||
var symbol, status string
|
||||
|
||||
@@ -94,6 +94,8 @@ func (h *SayHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
return h.handleHookStatus(msg, dc)
|
||||
case string(types.SayTypeHookOutputStream):
|
||||
return h.handleHookOutputStream(msg, dc)
|
||||
case string(types.SayTypeCommandPermissionDenied):
|
||||
return h.handleCommandPermissionDenied(msg, dc)
|
||||
default:
|
||||
return h.handleDefault(msg, dc)
|
||||
}
|
||||
@@ -346,6 +348,18 @@ func (h *SayHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayCon
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SayHandler) handleCommandPermissionDenied(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
if msg.Text == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use unified ToolRenderer
|
||||
rendered := dc.ToolRenderer.RenderCommandPermissionDenied(msg.Text)
|
||||
output.Print(rendered)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SayHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
var tool types.ToolMessage
|
||||
if err := json.Unmarshal([]byte(msg.Text), &tool); err != nil {
|
||||
|
||||
@@ -992,6 +992,15 @@ func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *Stre
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeCommandPermissionDenied):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
fmt.Println()
|
||||
m.displayMessage(msg, false, false, i)
|
||||
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeBrowserActionLaunch):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
|
||||
@@ -89,8 +89,9 @@ const (
|
||||
SayTypeTaskProgress SayType = "task_progress"
|
||||
// Hook status streaming from the backend.
|
||||
// These values must match the backend "say" strings emitted by the extension.
|
||||
SayTypeHookStatus SayType = "hook_status"
|
||||
SayTypeHookOutputStream SayType = "hook_output_stream"
|
||||
SayTypeHookStatus SayType = "hook_status"
|
||||
SayTypeHookOutputStream SayType = "hook_output_stream"
|
||||
SayTypeCommandPermissionDenied SayType = "command_permission_denied"
|
||||
)
|
||||
|
||||
// ToolMessage represents a tool-related message
|
||||
@@ -368,6 +369,8 @@ func convertProtoSayType(sayType cline.ClineSay) string {
|
||||
return string(SayTypeHookStatus)
|
||||
case cline.ClineSay_HOOK_OUTPUT_STREAM:
|
||||
return string(SayTypeHookOutputStream)
|
||||
case cline.ClineSay_COMMAND_PERMISSION_DENIED:
|
||||
return string(SayTypeCommandPermissionDenied)
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
+1
-1
@@ -150,7 +150,7 @@
|
||||
},
|
||||
"features/multiroot-workspace",
|
||||
"features/plan-and-act",
|
||||
"features/web-tools",
|
||||
"features/skills",
|
||||
{
|
||||
"group": "Slash Commands",
|
||||
"pages": [
|
||||
|
||||
@@ -58,59 +58,59 @@ Enable OpenTelemetry and configure an OTLP endpoint:
|
||||
|
||||
```bash
|
||||
# Enable OpenTelemetry
|
||||
export OTEL_TELEMETRY_ENABLED=1
|
||||
export CLINE_OTEL_TELEMETRY_ENABLED=true
|
||||
|
||||
# Configure metrics and logs export
|
||||
export OTEL_METRICS_EXPORTER=otlp
|
||||
export OTEL_LOGS_EXPORTER=otlp
|
||||
export CLINE_OTEL_METRICS_EXPORTER=otlp
|
||||
export CLINE_OTEL_LOGS_EXPORTER=otlp
|
||||
|
||||
# Set your OTLP endpoint
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
|
||||
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
|
||||
|
||||
# Optional: Set protocol (default is grpc)
|
||||
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||
export CLINE_OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `OTEL_TELEMETRY_ENABLED` | Enable OpenTelemetry (`1` or `true`) | Disabled |
|
||||
| `OTEL_METRICS_EXPORTER` | Metrics exporter type (`console`, `otlp`, or both) | None |
|
||||
| `OTEL_LOGS_EXPORTER` | Logs exporter type (`console`, `otlp`, or both) | None |
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | None |
|
||||
| `OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol (`grpc`, `http/protobuf`, `http/json`) | `grpc` |
|
||||
| `OTEL_EXPORTER_OTLP_INSECURE` | Allow insecure connections | `false` |
|
||||
| `OTEL_EXPORTER_OTLP_HEADERS` | Custom headers (comma-separated `key=value` pairs) | None |
|
||||
| `CLINE_OTEL_TELEMETRY_ENABLED` | Enable OpenTelemetry (`true`) | Disabled |
|
||||
| `CLINE_OTEL_METRICS_EXPORTER` | Metrics exporter type (`console`, `otlp`, or both) | None |
|
||||
| `CLINE_OTEL_LOGS_EXPORTER` | Logs exporter type (`console`, `otlp`, or both) | None |
|
||||
| `CLINE_OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint URL | None |
|
||||
| `CLINE_OTEL_EXPORTER_OTLP_PROTOCOL` | Protocol (`grpc`, `http/protobuf`, `http/json`) | `grpc` |
|
||||
| `CLINE_OTEL_EXPORTER_OTLP_INSECURE` | Allow insecure connections | `false` |
|
||||
| `CLINE_OTEL_EXPORTER_OTLP_HEADERS` | Custom headers (comma-separated `key=value` pairs) | None |
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
**Separate endpoints for metrics and logs:**
|
||||
```bash
|
||||
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics-collector:4317
|
||||
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs-collector:4317
|
||||
export CLINE_OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://metrics-collector:4317
|
||||
export CLINE_OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs-collector:4317
|
||||
```
|
||||
|
||||
**Custom headers for authentication:**
|
||||
```bash
|
||||
export OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key,x-custom-header=value"
|
||||
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="api-key=your-key,x-custom-header=value"
|
||||
```
|
||||
|
||||
**Multiple exporters (console + OTLP):**
|
||||
```bash
|
||||
export OTEL_METRICS_EXPORTER=console,otlp
|
||||
export OTEL_LOGS_EXPORTER=console,otlp
|
||||
export CLINE_OTEL_METRICS_EXPORTER=console,otlp
|
||||
export CLINE_OTEL_LOGS_EXPORTER=console,otlp
|
||||
```
|
||||
|
||||
**Export intervals:**
|
||||
```bash
|
||||
# Metrics export interval in milliseconds (default: 60000)
|
||||
export OTEL_METRIC_EXPORT_INTERVAL=30000
|
||||
export CLINE_OTEL_METRIC_EXPORT_INTERVAL=30000
|
||||
|
||||
# Logs batch size and timeout
|
||||
export OTEL_LOG_BATCH_SIZE=512
|
||||
export OTEL_LOG_BATCH_TIMEOUT=5000
|
||||
export OTEL_LOG_MAX_QUEUE_SIZE=2048
|
||||
export CLINE_OTEL_LOG_BATCH_SIZE=512
|
||||
export CLINE_OTEL_LOG_BATCH_TIMEOUT=5000
|
||||
export CLINE_OTEL_LOG_MAX_QUEUE_SIZE=2048
|
||||
```
|
||||
|
||||
## Integration Examples
|
||||
@@ -120,11 +120,11 @@ export OTEL_LOG_MAX_QUEUE_SIZE=2048
|
||||
Export to Datadog using their OTLP endpoint:
|
||||
|
||||
```bash
|
||||
export OTEL_TELEMETRY_ENABLED=1
|
||||
export OTEL_METRICS_EXPORTER=otlp
|
||||
export OTEL_LOGS_EXPORTER=otlp
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com
|
||||
export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
|
||||
export CLINE_OTEL_TELEMETRY_ENABLED=true
|
||||
export CLINE_OTEL_METRICS_EXPORTER=otlp
|
||||
export CLINE_OTEL_LOGS_EXPORTER=otlp
|
||||
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://api.datadoghq.com
|
||||
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
|
||||
```
|
||||
|
||||
### New Relic
|
||||
@@ -132,11 +132,11 @@ export OTEL_EXPORTER_OTLP_HEADERS="dd-api-key=YOUR_DD_API_KEY"
|
||||
Export to New Relic:
|
||||
|
||||
```bash
|
||||
export OTEL_TELEMETRY_ENABLED=1
|
||||
export OTEL_METRICS_EXPORTER=otlp
|
||||
export OTEL_LOGS_EXPORTER=otlp
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4317
|
||||
export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
|
||||
export CLINE_OTEL_TELEMETRY_ENABLED=true
|
||||
export CLINE_OTEL_METRICS_EXPORTER=otlp
|
||||
export CLINE_OTEL_LOGS_EXPORTER=otlp
|
||||
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.nr-data.net:4317
|
||||
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
|
||||
```
|
||||
|
||||
### Grafana Cloud
|
||||
@@ -144,11 +144,11 @@ export OTEL_EXPORTER_OTLP_HEADERS="api-key=YOUR_NEW_RELIC_LICENSE_KEY"
|
||||
Export to Grafana Cloud:
|
||||
|
||||
```bash
|
||||
export OTEL_TELEMETRY_ENABLED=1
|
||||
export OTEL_METRICS_EXPORTER=otlp
|
||||
export OTEL_LOGS_EXPORTER=otlp
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
|
||||
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic YOUR_BASE64_CREDENTIALS"
|
||||
export CLINE_OTEL_TELEMETRY_ENABLED=true
|
||||
export CLINE_OTEL_METRICS_EXPORTER=otlp
|
||||
export CLINE_OTEL_LOGS_EXPORTER=otlp
|
||||
export CLINE_OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
|
||||
export CLINE_OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic YOUR_BASE64_CREDENTIALS"
|
||||
```
|
||||
|
||||
|
||||
@@ -158,9 +158,9 @@ Test your configuration with console output before sending to a real endpoint:
|
||||
|
||||
```bash
|
||||
# Enable console output to see what data would be exported
|
||||
export OTEL_TELEMETRY_ENABLED=1
|
||||
export OTEL_METRICS_EXPORTER=console
|
||||
export OTEL_LOGS_EXPORTER=console
|
||||
export CLINE_OTEL_TELEMETRY_ENABLED=true
|
||||
export CLINE_OTEL_METRICS_EXPORTER=console
|
||||
export CLINE_OTEL_LOGS_EXPORTER=console
|
||||
```
|
||||
|
||||
Then launch Cline and check the console output for metrics and logs.
|
||||
@@ -171,20 +171,20 @@ Then launch Cline and check the console output for metrics and logs.
|
||||
|
||||
1. **Verify OpenTelemetry is enabled:**
|
||||
```bash
|
||||
echo $OTEL_TELEMETRY_ENABLED
|
||||
echo $CLINE_OTEL_TELEMETRY_ENABLED
|
||||
```
|
||||
Should output `1` or `true`
|
||||
Should output `true`
|
||||
|
||||
2. **Check exporters are configured:**
|
||||
```bash
|
||||
echo $OTEL_METRICS_EXPORTER
|
||||
echo $OTEL_LOGS_EXPORTER
|
||||
echo $CLINE_OTEL_METRICS_EXPORTER
|
||||
echo $CLINE_OTEL_LOGS_EXPORTER
|
||||
```
|
||||
|
||||
3. **Test with console exporter first:**
|
||||
```bash
|
||||
export OTEL_METRICS_EXPORTER=console
|
||||
export OTEL_LOGS_EXPORTER=console
|
||||
export CLINE_OTEL_METRICS_EXPORTER=console
|
||||
export CLINE_OTEL_LOGS_EXPORTER=console
|
||||
```
|
||||
|
||||
### Connection Errors
|
||||
@@ -196,7 +196,7 @@ Then launch Cline and check the console output for metrics and logs.
|
||||
|
||||
2. **Check if insecure mode is needed:**
|
||||
```bash
|
||||
export OTEL_EXPORTER_OTLP_INSECURE=true
|
||||
export CLINE_OTEL_EXPORTER_OTLP_INSECURE=true
|
||||
```
|
||||
|
||||
3. **Verify authentication headers:**
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
---
|
||||
title: "Skills"
|
||||
sidebarTitle: "Skills"
|
||||
description: "Extend Cline with reusable, on-demand instruction sets for specialized tasks"
|
||||
---
|
||||
|
||||
Skills are modular instruction sets that extend Cline's capabilities for specific tasks. Each skill packages detailed guidance, workflows, and optional resources that Cline loads only when relevant to your request.
|
||||
|
||||
Unlike rules (which are always active), skills load on-demand. You can install dozens of skills without affecting context or performance because Cline only sees the skill name and description until it's actually needed.
|
||||
|
||||
<Note>
|
||||
Skills is an experimental feature. Enable it in Settings → Features → Enable Skills.
|
||||
</Note>
|
||||
|
||||
## Why Skills?
|
||||
|
||||
Consider how you'd onboard a new team member: you wouldn't dump every document on them at once. You'd give them a brief overview, then point them to detailed guides when they're working on specific tasks.
|
||||
|
||||
Skills work the same way:
|
||||
- **At startup**: Cline sees only a brief description of each skill
|
||||
- **When triggered**: Cline loads the full instructions for that specific skill
|
||||
- **As needed**: Skills can bundle additional files that Cline reads only when referenced
|
||||
|
||||
This progressive loading means you can package extensive domain knowledge without burning context tokens on information that isn't relevant to the current task.
|
||||
|
||||
## Creating a Skill
|
||||
|
||||
Every skill is a directory containing a `SKILL.md` file with YAML frontmatter:
|
||||
|
||||
```
|
||||
my-skill/
|
||||
├── SKILL.md # Required: main instructions
|
||||
├── docs/ # Optional: additional documentation
|
||||
│ └── advanced.md
|
||||
└── scripts/ # Optional: utility scripts
|
||||
└── helper.sh
|
||||
```
|
||||
|
||||
The `SKILL.md` file has two parts: metadata and instructions.
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: my-skill
|
||||
description: Brief description of what this skill does and when to use it.
|
||||
---
|
||||
|
||||
# My Skill
|
||||
|
||||
Detailed instructions for Cline to follow when this skill is activated.
|
||||
|
||||
## Steps
|
||||
|
||||
1. First, do this
|
||||
2. Then do that
|
||||
3. For advanced usage, see [advanced.md](docs/advanced.md)
|
||||
```
|
||||
|
||||
**Required fields:**
|
||||
- `name`: Must exactly match the directory name
|
||||
- `description`: Tells Cline when to use this skill (max 1024 characters)
|
||||
|
||||
The description is critical because it's how Cline decides whether to activate a skill. Be specific about what the skill does and when it should be used.
|
||||
|
||||
## Where Skills Live
|
||||
|
||||
Skills can be stored in two locations:
|
||||
|
||||
**Global Skills** apply to all your projects:
|
||||
- **macOS/Linux:** `~/.cline/skills/`
|
||||
- **Windows:** `C:\Users\USERNAME\.cline\skills\`
|
||||
|
||||
**Project Skills** apply only to the current workspace:
|
||||
- `.cline/skills/` (recommended)
|
||||
- `.clinerules/skills/`
|
||||
- `.claude/skills/` (for Claude Code compatibility)
|
||||
|
||||
When a global skill and project skill have the same name, the global skill takes precedence. This lets you customize skills for your personal workflow while still using project defaults.
|
||||
|
||||
## Managing Skills
|
||||
|
||||
Click the scale icon below the chat input to open the rules and workflows panel. When skills are enabled, you'll see a Skills tab where you can:
|
||||
|
||||
- View all available skills (global and workspace)
|
||||
- Toggle individual skills on or off
|
||||
- Create new skills from a template
|
||||
- Delete skills you no longer need
|
||||
|
||||
Skills are enabled by default when discovered. Toggle them off if you want them available but not active for the current project.
|
||||
|
||||
## How Cline Uses Skills
|
||||
|
||||
When you send a message, Cline sees a list of available skills with their descriptions. If your request matches a skill's description, Cline activates it using the `use_skill` tool, which loads the full instructions.
|
||||
|
||||
For example, if you have a skill for deploying to AWS:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: aws-deploy
|
||||
description: Deploy applications to AWS using CDK. Use when deploying, updating infrastructure, or managing AWS resources.
|
||||
---
|
||||
```
|
||||
|
||||
Asking "deploy this to AWS" would trigger Cline to activate the skill, load its detailed instructions, and follow them to complete your request.
|
||||
|
||||
## Example: Data Analysis Skill
|
||||
|
||||
Here's a practical skill for data analysis tasks. Create a directory called `data-analysis/` with this `SKILL.md`:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: data-analysis
|
||||
description: Analyze data files and generate insights. Use when working with CSV, Excel, or JSON data files that need exploration, cleaning, or visualization.
|
||||
---
|
||||
```
|
||||
|
||||
Then add the instructions in the body of the file:
|
||||
|
||||
````markdown
|
||||
# Data Analysis
|
||||
|
||||
When analyzing data files, follow this workflow:
|
||||
|
||||
## 1. Understand the Data
|
||||
|
||||
- Read a sample of the file to understand its structure
|
||||
- Identify column types and data quality issues
|
||||
- Note any missing values or anomalies
|
||||
|
||||
## 2. Ask Clarifying Questions
|
||||
|
||||
Before diving in, ask the user:
|
||||
- What specific insights are they looking for?
|
||||
- Are there any known data quality issues?
|
||||
- What format do they want for the output?
|
||||
|
||||
## 3. Perform Analysis
|
||||
|
||||
Use pandas for data manipulation:
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
# Load and explore
|
||||
df = pd.read_csv("data.csv")
|
||||
print(df.head())
|
||||
print(df.describe())
|
||||
print(df.info())
|
||||
```
|
||||
|
||||
For visualization, prefer matplotlib or seaborn depending on complexity.
|
||||
|
||||
## 4. Present Findings
|
||||
|
||||
- Start with a summary of key insights
|
||||
- Support findings with specific numbers
|
||||
- Include visualizations where they add clarity
|
||||
- End with recommendations or next steps
|
||||
````
|
||||
|
||||
## Bundling Supporting Files
|
||||
|
||||
Skills can include additional files that Cline accesses only when needed:
|
||||
|
||||
```
|
||||
complex-skill/
|
||||
├── SKILL.md
|
||||
├── docs/
|
||||
│ ├── setup.md
|
||||
│ └── troubleshooting.md
|
||||
├── templates/
|
||||
│ └── config.yaml
|
||||
└── scripts/
|
||||
└── validate.py
|
||||
```
|
||||
|
||||
Reference these in your instructions:
|
||||
|
||||
````markdown
|
||||
For initial setup, follow [setup.md](docs/setup.md).
|
||||
|
||||
Use the config template at `templates/config.yaml` as a starting point.
|
||||
|
||||
Run the validation script to check your configuration:
|
||||
```bash
|
||||
python scripts/validate.py
|
||||
```
|
||||
````
|
||||
|
||||
Cline reads these files using `read_file` when the instructions reference them. Scripts can be executed directly, with only the output entering the context (not the script code itself).
|
||||
|
||||
## Ideas for Skills
|
||||
|
||||
Skills shine when you have tasks that:
|
||||
- Require detailed, multi-step workflows
|
||||
- Need domain-specific knowledge or best practices
|
||||
- Would otherwise require repeating the same instructions across conversations
|
||||
|
||||
Some possibilities:
|
||||
|
||||
- **Release management**: Version bumping, changelog generation, git tagging, and publishing
|
||||
- **Code review**: Your team's specific review checklist and quality standards
|
||||
- **Database migrations**: Safely evolving schemas with rollback procedures
|
||||
- **API integration**: Connecting to specific third-party services with proper error handling
|
||||
- **Documentation**: Your preferred structure, style guide, and tooling
|
||||
- **Debugging workflows**: Systematic approaches to diagnosing specific types of issues
|
||||
- **Infrastructure**: Terraform/CDK patterns for your cloud setup
|
||||
|
||||
The best skills encode institutional knowledge that would otherwise live only in experienced developers' heads.
|
||||
|
||||
## Skills vs Rules vs Workflows
|
||||
|
||||
| Feature | Purpose | When Active |
|
||||
|---------|---------|-------------|
|
||||
| **Rules** | Define how Cline should behave | Always (or contextually) |
|
||||
| **Workflows** | Step-by-step task automation | Invoked with `/workflow.md` |
|
||||
| **Skills** | Domain expertise loaded on-demand | Triggered by matching requests |
|
||||
|
||||
**Rules** set constraints and preferences (like "always use TypeScript" or "follow this style guide").
|
||||
|
||||
**Workflows** are explicit sequences you invoke for specific tasks (like `/release.md` for a release process).
|
||||
|
||||
**Skills** are expertise that Cline activates automatically when relevant (like data analysis knowledge when you're working with CSV files).
|
||||
|
||||
Use rules for ongoing constraints, workflows for explicit automation, and skills for domain knowledge that should be available but not always active.
|
||||
|
||||
## Related Features
|
||||
|
||||
- [Cline Rules](/features/cline-rules) for always-active project guidance
|
||||
- [Workflows](/features/slash-commands/workflows/index) for explicit task automation
|
||||
- [Hooks](/features/hooks/index) for injecting custom logic at key moments
|
||||
|
||||
@@ -120,8 +120,38 @@ Controls a built-in browser to interact with websites or local servers. Useful f
|
||||
</browser_action>
|
||||
```
|
||||
|
||||
### Leverage MCP Tools
|
||||
You can use Model Context Protocol (MCP) tools within your workflows to interact with external services like GitHub, Slack, or databases. This allows you to create powerful end-to-end automations.
|
||||
### Leveraging MCP Tools
|
||||
|
||||
MCP tools allow Cline to interact with external services like GitHub, Slack, or databases. You can reference them in your workflows using natural language or explicit XML tags for deterministic control.
|
||||
|
||||
#### Natural Language (Heuristic)
|
||||
|
||||
Most of the time, the simplest way to use an MCP tool is to describe the action you want Cline to take.
|
||||
|
||||
```markdown
|
||||
1. Fetch the latest issues from the github-repo MCP server.
|
||||
2. Summarize the critical bugs.
|
||||
3. Post the summary to the #engineering channel using the slack-notifications MCP.
|
||||
```
|
||||
|
||||
#### Explicit XML Tag (Deterministic)
|
||||
|
||||
For critical automation where you need exact control over parameters, use the `use_mcp_tool` tag.
|
||||
|
||||
```xml
|
||||
<use_mcp_tool>
|
||||
<server_name>github-repo-manager</server_name>
|
||||
<tool_name>create_issue</tool_name>
|
||||
<arguments>
|
||||
{
|
||||
"owner": "cline",
|
||||
"repo": "cline",
|
||||
"title": "Automated Bug Report",
|
||||
"body": "Found a regression in the latest build."
|
||||
}
|
||||
</arguments>
|
||||
</use_mcp_tool>
|
||||
```
|
||||
|
||||
### Manage Context Window
|
||||
Be mindful of Cline's context window. If a workflow is too long or processes too much data, it might exceed the token limit.
|
||||
|
||||
@@ -18,7 +18,8 @@ Cerebras delivers the world's fastest AI inference through their revolutionary w
|
||||
|
||||
Cline supports the following Cerebras models:
|
||||
|
||||
- `zai-glm-4.6` - Intelligent general purpose model with 1,500 tokens/s
|
||||
- `zai-glm-4.6` - Fast general-purpose model on Cerebras (up to 1,000 tokens/s). To be deprecated soon.
|
||||
- `zai-glm-4.7` - Highly capable general-purpose model on Cerebras (up to 1,000 tokens/s), competitive with leading proprietary models on coding tasks.
|
||||
- `qwen-3-235b-a22b-instruct-2507` - Advanced instruction-following model
|
||||
- `qwen-3-235b-a22b-thinking-2507` - Reasoning model with step-by-step thinking
|
||||
- `llama-3.3-70b` - Meta's Llama 3.3 model optimized for speed
|
||||
@@ -89,7 +90,7 @@ Works with any OpenAI-compatible tool—Cursor, Continue.dev, Cline, or any othe
|
||||
|
||||
- **Speed Advantage:** Cerebras excels at making reasoning models practical for real-time use. Perfect for agentic workflows that require multiple LLM calls.
|
||||
- **Free Tier:** Start with the free model to experience Cerebras speed before upgrading to paid plans.
|
||||
- **Context Windows:** Models support context windows ranging from 64K to 128K tokens for including substantial code context.
|
||||
- **Context Windows:** Models support context windows ranging from 64K to 131K tokens for including substantial code context.
|
||||
- **Rate Limits:** Generous rate limits designed for development workflows. Check your dashboard for current limits.
|
||||
- **Pricing:** Competitive pricing with significant speed advantages. Visit [Cerebras Cloud](https://cloud.cerebras.ai/) for current rates.
|
||||
- **Real-Time Applications:** Ideal for applications where AI response time matters—code generation, debugging, and interactive development.
|
||||
|
||||
Generated
+333
-4
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.47.0",
|
||||
"version": "3.49.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.47.0",
|
||||
"version": "3.49.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -89,6 +89,7 @@
|
||||
"puppeteer-core": "^23.4.0",
|
||||
"reconnecting-eventsource": "^1.6.4",
|
||||
"serialize-error": "^11.0.3",
|
||||
"shell-quote": "^1.8.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
@@ -111,10 +112,12 @@
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/diff": "^5.2.1",
|
||||
"@types/get-folder-size": "^3.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "20.x",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/proxyquire": "^1.3.31",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/should": "^11.2.0",
|
||||
"@types/sinon": "^17.0.4",
|
||||
"@types/turndown": "^5.0.5",
|
||||
@@ -4896,6 +4899,314 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz",
|
||||
"integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz",
|
||||
"integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz",
|
||||
"integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz",
|
||||
"integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz",
|
||||
"integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz",
|
||||
"integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz",
|
||||
"integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz",
|
||||
"integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz",
|
||||
"integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz",
|
||||
"integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz",
|
||||
"integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz",
|
||||
"integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz",
|
||||
"integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@sap-ai-sdk/ai-api": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@sap-ai-sdk/ai-api/-/ai-api-2.1.0.tgz",
|
||||
@@ -6473,6 +6784,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/js-yaml": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mocha": {
|
||||
"version": "10.0.7",
|
||||
"dev": true,
|
||||
@@ -6521,6 +6839,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/shell-quote": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz",
|
||||
"integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/shimmer": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
|
||||
@@ -16373,9 +16698,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.1",
|
||||
"dev": true,
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
|
||||
+6
-4
@@ -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.47.0",
|
||||
"version": "3.49.1",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -346,11 +346,10 @@
|
||||
"compile-cli": "scripts/build-cli.sh",
|
||||
"compile-cli-all-platforms": "scripts/build-cli-all-platforms.sh",
|
||||
"compile-cli-man-page": "pandoc cli/man/cline.1.md -s -t man -o cli/man/cline.1",
|
||||
"build:npm": "scripts/build-npm-package.sh",
|
||||
"test:install": "bash scripts/test-install.sh",
|
||||
"dev:cli:watch": "node scripts/dev-cli-watch.mjs",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
"postcompile-standalone-npm": "node scripts/package-standalone.mjs --target=npm",
|
||||
"postcompile-standalone-npm": "node scripts/package-npm.mjs",
|
||||
"dev": "npm run protos && npm run watch",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.mjs --watch",
|
||||
@@ -367,7 +366,7 @@
|
||||
"clean:all": "npm run clean:build && npm run clean:deps",
|
||||
"compile-tests": "node ./scripts/build-tests.js",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc -b --noEmit",
|
||||
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc --noEmit",
|
||||
"lint": "biome lint --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && npm run lint:proto",
|
||||
"lint:proto": "bash ./scripts/proto-lint.sh",
|
||||
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
|
||||
@@ -417,10 +416,12 @@
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/diff": "^5.2.1",
|
||||
"@types/get-folder-size": "^3.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "20.x",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/proxyquire": "^1.3.31",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/should": "^11.2.0",
|
||||
"@types/sinon": "^17.0.4",
|
||||
"@types/turndown": "^5.0.5",
|
||||
@@ -532,6 +533,7 @@
|
||||
"puppeteer-core": "^23.4.0",
|
||||
"reconnecting-eventsource": "^1.6.4",
|
||||
"serialize-error": "^11.0.3",
|
||||
"shell-quote": "^1.8.3",
|
||||
"simple-git": "^3.27.0",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tailwindcss": "^4.1.14",
|
||||
|
||||
@@ -81,6 +81,18 @@ service FileService {
|
||||
|
||||
// Deletes an existing hook file
|
||||
rpc deleteHook(DeleteHookRequest) returns (DeleteHookResponse);
|
||||
|
||||
// Refreshes all skill toggles (discovers skills and their enabled state)
|
||||
rpc refreshSkills(EmptyRequest) returns (RefreshedSkills);
|
||||
|
||||
// Toggles a skill on or off
|
||||
rpc toggleSkill(ToggleSkillRequest) returns (SkillsToggles);
|
||||
|
||||
// Creates a new skill from template
|
||||
rpc createSkillFile(CreateSkillRequest) returns (SkillsToggles);
|
||||
|
||||
// Deletes an existing skill directory
|
||||
rpc deleteSkillFile(DeleteSkillRequest) returns (SkillsToggles);
|
||||
}
|
||||
|
||||
// Response for refreshRules operation
|
||||
@@ -278,3 +290,45 @@ message DeleteHookRequest {
|
||||
message DeleteHookResponse {
|
||||
HooksToggles hooks_toggles = 1;
|
||||
}
|
||||
|
||||
// Skill information structure
|
||||
message SkillInfo {
|
||||
string name = 1; // Name of the skill (matches directory name)
|
||||
string description = 2; // Description from SKILL.md frontmatter
|
||||
string path = 3; // Full path to SKILL.md file
|
||||
bool enabled = 4; // Whether the skill is enabled
|
||||
}
|
||||
|
||||
// Response for refreshSkills operation
|
||||
message RefreshedSkills {
|
||||
repeated SkillInfo global_skills = 1;
|
||||
repeated SkillInfo local_skills = 2;
|
||||
}
|
||||
|
||||
// Maps from skill path to enabled/disabled status
|
||||
message SkillsToggles {
|
||||
map<string, bool> global_skills_toggles = 1;
|
||||
map<string, bool> local_skills_toggles = 2;
|
||||
}
|
||||
|
||||
// Request to toggle a skill
|
||||
message ToggleSkillRequest {
|
||||
Metadata metadata = 1;
|
||||
string skill_path = 2; // Path to the skill directory
|
||||
bool is_global = 3; // Whether this is a global or workspace skill
|
||||
bool enabled = 4; // Whether to enable or disable the skill
|
||||
}
|
||||
|
||||
// Request to create a skill
|
||||
message CreateSkillRequest {
|
||||
Metadata metadata = 1;
|
||||
string skill_name = 2; // Name of the skill to create
|
||||
bool is_global = 3; // Whether to create in global or workspace skills directory
|
||||
}
|
||||
|
||||
// Request to delete a skill
|
||||
message DeleteSkillRequest {
|
||||
Metadata metadata = 1;
|
||||
string skill_path = 2; // Path to the skill directory
|
||||
bool is_global = 3; // Whether this is a global or workspace skill
|
||||
}
|
||||
|
||||
+20
-12
@@ -49,6 +49,8 @@ service ModelsService {
|
||||
rpc refreshOcaModels(StringRequest) returns (OcaCompatibleModelInfo);
|
||||
// Fetches available models from AIhubmix
|
||||
rpc getAihubmixModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Vercel AI Gateway models
|
||||
rpc refreshVercelAiGatewayModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
@@ -381,6 +383,10 @@ message OcaModelInfo {
|
||||
string model_name = 17;
|
||||
// The API format used by this model
|
||||
optional ApiFormat api_format = 18;
|
||||
// Supports reasoning
|
||||
optional bool supports_reasoning = 19;
|
||||
// reasoning effort options
|
||||
repeated string reasoning_effort_options = 20;
|
||||
}
|
||||
|
||||
// Aggregated OCA model catalog keyed by model identifier
|
||||
@@ -604,12 +610,13 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130;
|
||||
optional string plan_mode_oca_model_id = 131;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 132;
|
||||
optional string plan_mode_hicap_model_id = 133;
|
||||
optional OpenRouterModelInfo plan_mode_hicap_model_info = 134;
|
||||
optional string plan_mode_aihubmix_model_id = 135;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 136;
|
||||
optional string plan_mode_nous_research_model_id = 137;
|
||||
optional string gemini_plan_mode_thinking_level = 138;
|
||||
optional string plan_mode_oca_reasoning_effort = 133;
|
||||
optional string plan_mode_hicap_model_id = 134;
|
||||
optional OpenRouterModelInfo plan_mode_hicap_model_info = 135;
|
||||
optional string plan_mode_aihubmix_model_id = 136;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 137;
|
||||
optional string plan_mode_nous_research_model_id = 138;
|
||||
optional string gemini_plan_mode_thinking_level = 139;
|
||||
|
||||
// Act mode configurations
|
||||
optional ApiProvider act_mode_api_provider = 200;
|
||||
@@ -645,10 +652,11 @@ message ModelsApiConfiguration {
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230;
|
||||
optional string act_mode_oca_model_id = 231;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 232;
|
||||
optional string act_mode_hicap_model_id = 233;
|
||||
optional OpenRouterModelInfo act_mode_hicap_model_info = 234;
|
||||
optional string act_mode_aihubmix_model_id = 235;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 236;
|
||||
optional string act_mode_nous_research_model_id = 237;
|
||||
optional string gemini_act_mode_thinking_level = 238;
|
||||
optional string act_mode_oca_reasoning_effort = 233;
|
||||
optional string act_mode_hicap_model_id = 234;
|
||||
optional OpenRouterModelInfo act_mode_hicap_model_info = 235;
|
||||
optional string act_mode_aihubmix_model_id = 236;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 237;
|
||||
optional string act_mode_nous_research_model_id = 238;
|
||||
optional string gemini_act_mode_thinking_level = 239;
|
||||
}
|
||||
|
||||
@@ -230,6 +230,8 @@ message Settings {
|
||||
optional bool cline_web_tools_enabled = 134;
|
||||
optional bool hooks_enabled = 135;
|
||||
optional bool azure_identity = 136;
|
||||
optional bool skills_enabled = 137;
|
||||
optional bool opt_out_of_remote_config = 138;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -371,6 +373,9 @@ message UpdateSettingsRequest {
|
||||
optional bool cline_web_tools_enabled = 34;
|
||||
optional bool enable_parallel_tool_calling = 35;
|
||||
optional bool background_edit_enabled = 36;
|
||||
optional string oca_reasoning_effort = 37;
|
||||
optional bool skills_enabled = 38;
|
||||
optional bool opt_out_of_remote_config = 39;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
|
||||
@@ -69,6 +69,7 @@ enum ClineSay {
|
||||
GENERATE_EXPLANATION = 29;
|
||||
HOOK_STATUS = 30;
|
||||
HOOK_OUTPUT_STREAM = 31;
|
||||
COMMAND_PERMISSION_DENIED = 32;
|
||||
}
|
||||
|
||||
// Enum for ClineSayTool tool types
|
||||
@@ -222,6 +223,10 @@ message ClineMessage {
|
||||
ClineModelInfo model_info = 23;
|
||||
}
|
||||
|
||||
message ShowWebviewEvent {
|
||||
bool preserve_editor_focus = 1; // When true, webview should not steal focus from editor
|
||||
}
|
||||
|
||||
// UiService provides methods for managing UI interactions
|
||||
service UiService {
|
||||
// Scrolls to a specific settings section in the settings view
|
||||
@@ -260,11 +265,8 @@ service UiService {
|
||||
// Subscribe to relinquish control events
|
||||
rpc subscribeToRelinquishControl(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to focus chat input events
|
||||
rpc subscribeToFocusChatInput(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to webview visibility change events
|
||||
rpc subscribeToDidBecomeVisible(EmptyRequest) returns (stream Empty);
|
||||
// Subscribe to show webview events
|
||||
rpc subscribeToShowWebview(EmptyRequest) returns (stream ShowWebviewEvent);
|
||||
|
||||
// Returns the HTML for the webview index page. This is only used by external clients, not by the vscode webview.
|
||||
rpc getWebviewHtml(EmptyRequest) returns (String);
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Script to build the Cline NPM package with telemetry keys injected
|
||||
# This script ensures all environment variables are properly set and builds are successful
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Required environment variables
|
||||
REQUIRED_VARS=(
|
||||
"TELEMETRY_SERVICE_API_KEY"
|
||||
"ERROR_SERVICE_API_KEY"
|
||||
)
|
||||
|
||||
# Optional but recommended environment variables
|
||||
OPTIONAL_VARS=(
|
||||
"CLINE_ENVIRONMENT"
|
||||
"POSTHOG_TELEMETRY_ENABLED"
|
||||
)
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Cline NPM Package Build Script${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 1: Verify required environment variables are set
|
||||
echo -e "${BLUE}Step 1: Verifying environment variables...${NC}"
|
||||
MISSING_VARS=()
|
||||
for VAR in "${REQUIRED_VARS[@]}"; do
|
||||
if [ -z "${!VAR}" ]; then
|
||||
MISSING_VARS+=("$VAR")
|
||||
echo -e "${RED}✗ $VAR is not set${NC}"
|
||||
else
|
||||
# Show first 10 chars for verification (don't expose full key)
|
||||
VAR_VALUE="${!VAR}"
|
||||
echo -e "${GREEN}✓ $VAR is set (${VAR_VALUE:0:10}...)${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check optional variables
|
||||
for VAR in "${OPTIONAL_VARS[@]}"; do
|
||||
if [ -z "${!VAR}" ]; then
|
||||
echo -e "${YELLOW}⚠ $VAR is not set (optional)${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✓ $VAR is set: ${!VAR}${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#MISSING_VARS[@]} -gt 0 ]; then
|
||||
echo -e "\n${RED}Error: Missing required environment variables:${NC}"
|
||||
printf '%s\n' "${MISSING_VARS[@]}"
|
||||
echo -e "\n${YELLOW}Please set these variables before running the build:${NC}"
|
||||
echo -e "export TELEMETRY_SERVICE_API_KEY=\"your_posthog_api_key\""
|
||||
echo -e "export ERROR_SERVICE_API_KEY=\"your_error_tracking_api_key\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 2: Verify Node.js can see the environment variables
|
||||
echo -e "\n${BLUE}Step 2: Verifying Node.js can access environment variables...${NC}"
|
||||
if node -e "
|
||||
const telemetryKey = process.env.TELEMETRY_SERVICE_API_KEY;
|
||||
const errorKey = process.env.ERROR_SERVICE_API_KEY;
|
||||
if (!telemetryKey || !errorKey) {
|
||||
console.error('Node.js cannot see environment variables!');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ TELEMETRY_SERVICE_API_KEY visible to Node.js');
|
||||
console.log('✓ ERROR_SERVICE_API_KEY visible to Node.js');
|
||||
"; then
|
||||
echo -e "${GREEN}✓ Node.js can access environment variables${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Node.js cannot access environment variables${NC}"
|
||||
echo -e "${YELLOW}Make sure to use 'export' when setting variables:${NC}"
|
||||
echo -e "export TELEMETRY_SERVICE_API_KEY=\"...\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 3: Clean previous builds
|
||||
echo -e "\n${BLUE}Step 3: Cleaning previous builds...${NC}"
|
||||
rm -rf dist-standalone
|
||||
echo -e "${GREEN}✓ Cleaned dist-standalone directory${NC}"
|
||||
|
||||
# Step 4: Build Go CLI binaries for all platforms
|
||||
echo -e "\n${BLUE}Step 4: Building Go CLI binaries for all platforms...${NC}"
|
||||
if npm run compile-cli-all-platforms; then
|
||||
echo -e "${GREEN}✓ Go CLI binaries built successfully${NC}"
|
||||
|
||||
# Verify binaries were created
|
||||
if ls cli/bin/cline-* 1> /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ CLI binaries verified:${NC}"
|
||||
ls -lh cli/bin/cline-* | awk '{print " " $9 " (" $5 ")"}'
|
||||
else
|
||||
echo -e "${RED}✗ No CLI binaries found in cli/bin/${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗ Failed to build Go CLI binaries${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 5: Build the standalone package with esbuild
|
||||
echo -e "\n${BLUE}Step 5: Building standalone package with esbuild...${NC}"
|
||||
if npm run compile-standalone-npm; then
|
||||
echo -e "${GREEN}✓ Standalone package built successfully${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Failed to build standalone package${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 6: Verify telemetry keys were injected
|
||||
echo -e "\n${BLUE}Step 6: Verifying telemetry keys were injected...${NC}"
|
||||
|
||||
# Check if the compiled file still has process.env references (bad)
|
||||
if grep -q "process.env.TELEMETRY_SERVICE_API_KEY" dist-standalone/cline-core.js; then
|
||||
echo -e "${RED}✗ Keys were NOT injected! Found 'process.env.TELEMETRY_SERVICE_API_KEY' in compiled code${NC}"
|
||||
echo -e "${YELLOW}This means the environment variables were not replaced during build${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if actual keys are present (good)
|
||||
if grep -q "data.cline.bot" dist-standalone/cline-core.js; then
|
||||
# Extract a snippet of the PostHog config
|
||||
POSTHOG_CONFIG=$(grep -A 3 "data.cline.bot" dist-standalone/cline-core.js | head -5)
|
||||
if echo "$POSTHOG_CONFIG" | grep -q "apiKey.*phc_"; then
|
||||
echo -e "${GREEN}✓ Telemetry keys successfully injected into compiled code${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ PostHog config found but apiKey format unclear${NC}"
|
||||
echo -e "${YELLOW}Config snippet:${NC}"
|
||||
echo "$POSTHOG_CONFIG"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Could not verify PostHog config in compiled code${NC}"
|
||||
fi
|
||||
|
||||
# Step 7: Display build summary
|
||||
echo -e "\n${BLUE}========================================${NC}"
|
||||
echo -e "${GREEN}Build completed successfully!${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}Package location:${NC} dist-standalone/"
|
||||
echo -e "${GREEN}Package version:${NC} $(node -p "require('./dist-standalone/package.json').version" 2>/dev/null || echo "unknown")"
|
||||
echo ""
|
||||
echo -e "${BLUE}Next steps:${NC}"
|
||||
echo -e "1. Test locally: ${YELLOW}cd dist-standalone && npm link${NC}"
|
||||
echo -e "2. Verify: ${YELLOW}cline version${NC}"
|
||||
echo -e "3. Publish: ${YELLOW}cd dist-standalone && npm publish${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Note: Check PostHog dashboard after running cline commands to verify telemetry${NC}"
|
||||
@@ -43,6 +43,13 @@ const PLATFORMS = [
|
||||
binaryPath: "rg",
|
||||
isZip: false,
|
||||
},
|
||||
{
|
||||
name: "linux-arm64",
|
||||
archiveName: `ripgrep-${RIPGREP_VERSION}-aarch64-unknown-linux-gnu.tar.gz`,
|
||||
url: `https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-aarch64-unknown-linux-gnu.tar.gz`,
|
||||
binaryPath: "rg",
|
||||
isZip: false,
|
||||
},
|
||||
{
|
||||
name: "win-x64",
|
||||
archiveName: `ripgrep-${RIPGREP_VERSION}-x86_64-pc-windows-msvc.zip`,
|
||||
|
||||
@@ -49,9 +49,18 @@ fi
|
||||
# Create installation directory
|
||||
mkdir -p "$INSTALL_DIR/bin"
|
||||
|
||||
# Copy standalone package first (includes node_modules, cline-core.js, etc.)
|
||||
# Copy standalone package first (cline-core.js, wasm files, etc.)
|
||||
rsync -a --exclude='bin' "$PROJECT_ROOT/dist-standalone/" "$INSTALL_DIR/"
|
||||
|
||||
# Install runtime dependencies (grpc-health-check, better-sqlite3, etc.)
|
||||
# These are external dependencies not bundled into cline-core.js
|
||||
echo -e "${CYAN}→${NC} ${DIM}Installing runtime dependencies...${NC}"
|
||||
cd "$PROJECT_ROOT/standalone/runtime-files"
|
||||
npm install --silent 2>/dev/null || npm install
|
||||
cp -r node_modules "$INSTALL_DIR/"
|
||||
cp -r vscode "$INSTALL_DIR/node_modules/"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
# Detect platform for native modules
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
arch=$(uname -m)
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* NPM Package Builder for Cline CLI
|
||||
*
|
||||
* This script builds the Cline CLI NPM package (dist-standalone/).
|
||||
* It is completely independent from package-standalone.mjs (JetBrains build).
|
||||
*
|
||||
* Usage: node scripts/package-npm.mjs
|
||||
*
|
||||
* Prerequisites:
|
||||
* - npm run protos && npm run protos-go
|
||||
* - npm run compile-cli
|
||||
* - npm run compile-cli-all-platforms
|
||||
* - npm run download-ripgrep
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process"
|
||||
import fs from "fs"
|
||||
import { cp } from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
const BUILD_DIR = "dist-standalone"
|
||||
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
|
||||
const RIPGREP_BINARIES_DIR = `${BUILD_DIR}/ripgrep-binaries`
|
||||
const CLI_BINARIES_DIR = "cli/bin"
|
||||
const IS_VERBOSE = process.argv.includes("-v") || process.argv.includes("--verbose")
|
||||
|
||||
async function main() {
|
||||
console.log("🚀 Building Cline NPM Package\n")
|
||||
|
||||
await installNodeDependencies()
|
||||
await copyCliBinaries()
|
||||
await copyRipgrepBinaries()
|
||||
await copyProtoDescriptors()
|
||||
await createNpmPackageFiles()
|
||||
await createFakeNodeModules()
|
||||
await createNpmIgnoreFile()
|
||||
await createPostinstallScript()
|
||||
|
||||
console.log("\n✅ Build complete!")
|
||||
console.log(`\n📦 NPM package ready in ${BUILD_DIR}/`)
|
||||
console.log(`To publish: cd ${BUILD_DIR} && npm publish`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Install node dependencies in the build directory
|
||||
*/
|
||||
async function installNodeDependencies() {
|
||||
// Clean modules from any previous builds
|
||||
await rmrf(path.join(BUILD_DIR, "node_modules"))
|
||||
|
||||
await cpr(RUNTIME_DEPS_DIR, BUILD_DIR)
|
||||
|
||||
console.log("Running npm install in distribution directory...")
|
||||
execSync("npm install", { stdio: "inherit", cwd: BUILD_DIR })
|
||||
|
||||
// Move the vscode directory into node_modules.
|
||||
// It can't be installed using npm because it will create a symlink which cannot be unzipped correctly on windows.
|
||||
fs.renameSync(`${BUILD_DIR}/vscode`, `${BUILD_DIR}/node_modules/vscode`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy CLI binaries (cline and cline-host) for all platforms
|
||||
* The Go binaries are cross-compiled for darwin/linux arm64/amd64
|
||||
*/
|
||||
async function copyCliBinaries() {
|
||||
console.log("Copying CLI binaries for all platforms...")
|
||||
|
||||
const platforms = [
|
||||
{ os: "darwin", arch: "arm64" },
|
||||
{ os: "darwin", arch: "amd64" },
|
||||
{ os: "linux", arch: "amd64" },
|
||||
{ os: "linux", arch: "arm64" },
|
||||
]
|
||||
|
||||
const binDir = path.join(BUILD_DIR, "bin")
|
||||
|
||||
// Create bin directory
|
||||
fs.mkdirSync(binDir, { recursive: true })
|
||||
|
||||
// Copy all platform-specific binaries
|
||||
for (const { os, arch } of platforms) {
|
||||
const platformSuffix = `${os}-${arch}`
|
||||
|
||||
// Copy cline binary
|
||||
const clineSource = path.join(CLI_BINARIES_DIR, `cline-${platformSuffix}`)
|
||||
const clineDest = path.join(binDir, `cline-${platformSuffix}`)
|
||||
|
||||
if (!fs.existsSync(clineSource)) {
|
||||
console.error(`Error: CLI binary not found at ${clineSource}`)
|
||||
console.error(`Please run: npm run compile-cli-all-platforms`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(clineSource, clineDest)
|
||||
fs.chmodSync(clineDest, 0o755)
|
||||
console.log(`✓ cline-${platformSuffix} copied`)
|
||||
|
||||
// Copy cline-host binary
|
||||
const hostSource = path.join(CLI_BINARIES_DIR, `cline-host-${platformSuffix}`)
|
||||
const hostDest = path.join(binDir, `cline-host-${platformSuffix}`)
|
||||
|
||||
if (!fs.existsSync(hostSource)) {
|
||||
console.error(`Error: CLI binary not found at ${hostSource}`)
|
||||
console.error(`Please run: npm run compile-cli-all-platforms`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(hostSource, hostDest)
|
||||
fs.chmodSync(hostDest, 0o755)
|
||||
console.log(`✓ cline-host-${platformSuffix} copied`)
|
||||
}
|
||||
|
||||
console.log(`✓ All CLI binaries copied to ${binDir}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy ripgrep binaries for ALL platforms
|
||||
* Ripgrep is needed by cline-core for file searching
|
||||
* The postinstall script will select the correct binary for the user's platform
|
||||
*/
|
||||
async function copyRipgrepBinaries() {
|
||||
console.log("Copying ripgrep binaries for all platforms...")
|
||||
|
||||
const platforms = [
|
||||
{ dir: "darwin-arm64", binary: "rg" },
|
||||
{ dir: "darwin-x64", binary: "rg" },
|
||||
{ dir: "linux-x64", binary: "rg" },
|
||||
{ dir: "linux-arm64", binary: "rg" },
|
||||
// { dir: "win-x64", binary: "rg.exe" }, // Windows not supported yet
|
||||
]
|
||||
|
||||
const ripgrepDir = path.join(BUILD_DIR, "ripgrep")
|
||||
|
||||
// Create ripgrep directory
|
||||
fs.mkdirSync(ripgrepDir, { recursive: true })
|
||||
|
||||
// Check if ripgrep binaries exist, download if missing
|
||||
const firstPlatform = platforms[0]
|
||||
const firstBinaryPath = path.join(RIPGREP_BINARIES_DIR, firstPlatform.dir, firstPlatform.binary)
|
||||
if (!fs.existsSync(firstBinaryPath)) {
|
||||
console.log(`Ripgrep binaries not found, downloading...`)
|
||||
try {
|
||||
execSync("npm run download-ripgrep", { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
console.error(`Error downloading ripgrep: ${error.message}`)
|
||||
console.error(`Please run: npm run download-ripgrep`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all platform-specific binaries
|
||||
for (const { dir, binary } of platforms) {
|
||||
const source = path.join(RIPGREP_BINARIES_DIR, dir, binary)
|
||||
const dest = path.join(ripgrepDir, `rg-${dir}`)
|
||||
|
||||
if (!fs.existsSync(source)) {
|
||||
console.error(`Error: Ripgrep binary not found at ${source}`)
|
||||
console.error(`Please run: npm run download-ripgrep`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(source, dest)
|
||||
fs.chmodSync(dest, 0o755)
|
||||
console.log(`✓ rg-${dir} copied`)
|
||||
}
|
||||
|
||||
console.log(`✓ All ripgrep binaries copied to ${ripgrepDir}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify proto descriptors exist in the build directory
|
||||
* The proto/descriptor_set.pb file is generated by build-proto.mjs to dist-standalone/proto/
|
||||
* We do NOT copy from proto/ source because that would overwrite the freshly generated descriptor
|
||||
*/
|
||||
async function copyProtoDescriptors() {
|
||||
console.log("Verifying proto descriptors...")
|
||||
|
||||
const protoDest = path.join(BUILD_DIR, "proto")
|
||||
const descriptorPath = path.join(protoDest, "descriptor_set.pb")
|
||||
|
||||
// Check if descriptor_set.pb exists in the build directory
|
||||
// It should have been generated by `npm run protos` which runs build-proto.mjs
|
||||
if (!fs.existsSync(descriptorPath)) {
|
||||
console.error(`Error: proto/descriptor_set.pb not found at ${descriptorPath}`)
|
||||
console.error(`Please run: npm run protos`)
|
||||
console.error(`Note: build-proto.mjs generates the descriptor to dist-standalone/proto/`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Verify the descriptor is recent (not stale)
|
||||
const stats = fs.statSync(descriptorPath)
|
||||
const ageMinutes = (Date.now() - stats.mtimeMs) / 1000 / 60
|
||||
if (ageMinutes > 60) {
|
||||
console.warn(`Warning: descriptor_set.pb is ${Math.round(ageMinutes)} minutes old`)
|
||||
console.warn(`Consider running: npm run protos`)
|
||||
}
|
||||
|
||||
console.log(`✓ Proto descriptors verified at ${protoDest}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy NPM package files (package.json, README.md, and man page) from cli/ directory
|
||||
*/
|
||||
async function createNpmPackageFiles() {
|
||||
console.log("Copying NPM package files...")
|
||||
|
||||
// Copy package.json from cli/ directory
|
||||
const packageJsonSource = path.join("cli", "package.json")
|
||||
const packageJsonDest = path.join(BUILD_DIR, "package.json")
|
||||
|
||||
if (!fs.existsSync(packageJsonSource)) {
|
||||
console.error(`Error: NPM package.json not found at ${packageJsonSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(packageJsonSource, packageJsonDest)
|
||||
console.log(`✓ package.json copied from ${packageJsonSource}`)
|
||||
|
||||
// Copy README.md from cli/ directory
|
||||
const readmeSource = path.join("cli", "README.md")
|
||||
const readmeDest = path.join(BUILD_DIR, "README.md")
|
||||
|
||||
if (!fs.existsSync(readmeSource)) {
|
||||
console.error(`Error: NPM README.md not found at ${readmeSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(readmeSource, readmeDest)
|
||||
console.log(`✓ README.md copied from ${readmeSource}`)
|
||||
|
||||
// Copy man page from cli/man/ directory
|
||||
const manPageSource = path.join("cli", "man", "cline.1")
|
||||
const manDir = path.join(BUILD_DIR, "man")
|
||||
const manPageDest = path.join(manDir, "cline.1")
|
||||
|
||||
if (!fs.existsSync(manPageSource)) {
|
||||
console.error(`Error: Man page not found at ${manPageSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create man directory if it doesn't exist
|
||||
fs.mkdirSync(manDir, { recursive: true })
|
||||
|
||||
await cpr(manPageSource, manPageDest)
|
||||
console.log(`✓ Man page copied from ${manPageSource}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fake_node_modules directory with vscode stub
|
||||
* This directory will be added to NODE_PATH so Node.js can find the vscode module
|
||||
* without npm interfering with the real node_modules directory
|
||||
*/
|
||||
async function createFakeNodeModules() {
|
||||
console.log("Creating fake_node_modules with vscode stub...")
|
||||
|
||||
const vscodeSource = path.join(BUILD_DIR, "node_modules", "vscode")
|
||||
const fakeNodeModulesDir = path.join(BUILD_DIR, "fake_node_modules")
|
||||
const vscodeDest = path.join(fakeNodeModulesDir, "vscode")
|
||||
|
||||
if (!fs.existsSync(vscodeSource)) {
|
||||
console.error(`Error: vscode stub module not found at ${vscodeSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create fake_node_modules directory
|
||||
fs.mkdirSync(fakeNodeModulesDir, { recursive: true })
|
||||
|
||||
// Copy vscode stub into fake_node_modules
|
||||
await cpr(vscodeSource, vscodeDest)
|
||||
|
||||
console.log(`✓ fake_node_modules/vscode created at ${vscodeDest}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create .npmignore file to ensure necessary files are included
|
||||
*/
|
||||
async function createNpmIgnoreFile() {
|
||||
console.log("Creating .npmignore file...")
|
||||
|
||||
// Create .npmignore that excludes build artifacts
|
||||
// Note: proto/ directory is NOT excluded because proto/descriptor_set.pb is needed at runtime
|
||||
const npmignoreContent = `# Exclude build artifacts and unnecessary files
|
||||
binaries/
|
||||
ripgrep-binaries/
|
||||
standalone.zip
|
||||
cline-core.js.map
|
||||
package-lock.json
|
||||
tree-sitter*.wasm
|
||||
node_modules/vscode
|
||||
`
|
||||
|
||||
const npmignorePath = path.join(BUILD_DIR, ".npmignore")
|
||||
fs.writeFileSync(npmignorePath, npmignoreContent)
|
||||
|
||||
console.log(`✓ .npmignore created`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create postinstall script for NPM package
|
||||
* This script selects the correct platform-specific binary and creates symlinks
|
||||
*/
|
||||
async function createPostinstallScript() {
|
||||
console.log("Creating postinstall script...")
|
||||
|
||||
const postinstallScript = `#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// Detect current platform and architecture
|
||||
function getPlatformInfo() {
|
||||
const platform = os.platform();
|
||||
const arch = os.arch();
|
||||
|
||||
// Map Node.js arch names to Go arch names (for CLI binaries)
|
||||
let goArch = arch;
|
||||
if (arch === 'x64') {
|
||||
goArch = 'amd64';
|
||||
}
|
||||
|
||||
// Map for ripgrep binaries (uses different naming)
|
||||
let rgArch = arch;
|
||||
if (arch === 'arm64') {
|
||||
rgArch = 'arm64';
|
||||
} else if (arch === 'x64') {
|
||||
rgArch = 'x64';
|
||||
}
|
||||
|
||||
return { platform, arch, goArch, rgArch };
|
||||
}
|
||||
|
||||
// Setup platform-specific binaries
|
||||
function setupBinaries() {
|
||||
const { platform, goArch, rgArch } = getPlatformInfo();
|
||||
const cliPlatformSuffix = \`\${platform}-\${goArch}\`;
|
||||
const rgPlatformSuffix = \`\${platform}-\${rgArch}\`;
|
||||
|
||||
console.log(\`Setting up Cline CLI for \${cliPlatformSuffix}...\`);
|
||||
|
||||
// Setup CLI binaries
|
||||
const binDir = path.join(__dirname, 'bin');
|
||||
|
||||
// Check if platform-specific binaries exist
|
||||
const clineSource = path.join(binDir, \`cline-\${cliPlatformSuffix}\`);
|
||||
const clineHostSource = path.join(binDir, \`cline-host-\${cliPlatformSuffix}\`);
|
||||
|
||||
if (!fs.existsSync(clineSource)) {
|
||||
console.error(\`Error: Binary not found for platform \${cliPlatformSuffix}\`);
|
||||
console.error(\`Expected: \${clineSource}\`);
|
||||
console.error(\`Supported platforms: darwin-arm64, darwin-amd64, linux-amd64, linux-arm64\`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(clineHostSource)) {
|
||||
console.error(\`Error: Binary not found for platform \${cliPlatformSuffix}\`);
|
||||
console.error(\`Expected: \${clineHostSource}\`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create symlinks or copies to the generic names
|
||||
const clineTarget = path.join(binDir, 'cline');
|
||||
const clineHostTarget = path.join(binDir, 'cline-host');
|
||||
|
||||
// Remove existing files if they exist
|
||||
[clineTarget, clineHostTarget].forEach(target => {
|
||||
if (fs.existsSync(target)) {
|
||||
try {
|
||||
fs.unlinkSync(target);
|
||||
} catch (e) {
|
||||
console.warn(\`Warning: Could not remove existing file \${target}: \${e.message}\`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// On Unix, create symlinks; on Windows, copy files
|
||||
if (platform === 'win32') {
|
||||
// Windows: copy files
|
||||
fs.copyFileSync(clineSource, clineTarget);
|
||||
fs.copyFileSync(clineHostSource, clineHostTarget);
|
||||
console.log('✓ Copied platform-specific CLI binaries');
|
||||
} else {
|
||||
// Unix: create symlinks
|
||||
fs.symlinkSync(path.basename(clineSource), clineTarget);
|
||||
fs.symlinkSync(path.basename(clineHostSource), clineHostTarget);
|
||||
console.log('✓ Created symlinks to platform-specific CLI binaries');
|
||||
|
||||
// Make binaries executable
|
||||
try {
|
||||
fs.chmodSync(clineSource, 0o755);
|
||||
fs.chmodSync(clineHostSource, 0o755);
|
||||
fs.chmodSync(clineTarget, 0o755);
|
||||
fs.chmodSync(clineHostTarget, 0o755);
|
||||
} catch (error) {
|
||||
console.warn(\`Warning: Could not set executable permissions: \${error.message}\`);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup ripgrep binary
|
||||
console.log(\`Setting up ripgrep for \${rgPlatformSuffix}...\`);
|
||||
|
||||
const ripgrepDir = path.join(__dirname, 'ripgrep');
|
||||
const rgSource = path.join(ripgrepDir, \`rg-\${rgPlatformSuffix}\`);
|
||||
const rgTarget = path.join(__dirname, 'rg');
|
||||
|
||||
if (!fs.existsSync(rgSource)) {
|
||||
console.error(\`Error: ripgrep binary not found for platform \${rgPlatformSuffix}\`);
|
||||
console.error(\`Expected: \${rgSource}\`);
|
||||
console.error(\`Supported platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64\`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Remove existing rg if it exists
|
||||
if (fs.existsSync(rgTarget)) {
|
||||
try {
|
||||
fs.unlinkSync(rgTarget);
|
||||
} catch (e) {
|
||||
console.warn(\`Warning: Could not remove existing ripgrep: \${e.message}\`);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy ripgrep binary to root (where cline-core expects it)
|
||||
fs.copyFileSync(rgSource, rgTarget);
|
||||
|
||||
// Make ripgrep executable (Unix only)
|
||||
if (platform !== 'win32') {
|
||||
try {
|
||||
fs.chmodSync(rgTarget, 0o755);
|
||||
} catch (error) {
|
||||
console.warn(\`Warning: Could not set ripgrep executable permissions: \${error.message}\`);
|
||||
}
|
||||
}
|
||||
console.log('✓ Copied platform-specific ripgrep binary');
|
||||
|
||||
console.log('✓ Cline CLI installation complete');
|
||||
console.log('');
|
||||
console.log('Usage:');
|
||||
console.log(' cline - Start Cline CLI');
|
||||
console.log(' cline-host - Start Cline host service');
|
||||
console.log('');
|
||||
console.log('Documentation: https://docs.cline.bot');
|
||||
}
|
||||
|
||||
try {
|
||||
setupBinaries();
|
||||
} catch (error) {
|
||||
console.error(\`Installation failed: \${error.message}\`);
|
||||
console.error('Please report this issue at: https://github.com/cline/cline/issues');
|
||||
process.exit(1);
|
||||
}
|
||||
`
|
||||
|
||||
const postinstallPath = path.join(BUILD_DIR, "postinstall.js")
|
||||
fs.writeFileSync(postinstallPath, postinstallScript)
|
||||
fs.chmodSync(postinstallPath, 0o755)
|
||||
|
||||
console.log(`✓ postinstall.js created`)
|
||||
}
|
||||
|
||||
/* cp -r */
|
||||
async function cpr(source, dest) {
|
||||
log_verbose(`Copying ${source} -> ${dest}`)
|
||||
await cp(source, dest, {
|
||||
recursive: true,
|
||||
preserveTimestamps: true,
|
||||
dereference: false, // preserve symlinks instead of following them
|
||||
})
|
||||
}
|
||||
|
||||
/* rm -rf */
|
||||
async function rmrf(dir) {
|
||||
if (fs.existsSync(dir)) {
|
||||
log_verbose(`Removing ${dir}`)
|
||||
fs.rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function log_verbose(...args) {
|
||||
if (IS_VERBOSE) {
|
||||
console.log(...args)
|
||||
}
|
||||
}
|
||||
|
||||
await main()
|
||||
@@ -13,8 +13,6 @@ import { rmrf } from "./file-utils.mjs"
|
||||
const BUILD_DIR = "dist-standalone"
|
||||
const BINARIES_DIR = `${BUILD_DIR}/binaries`
|
||||
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
|
||||
const RIPGREP_BINARIES_DIR = `${BUILD_DIR}/ripgrep-binaries`
|
||||
const CLI_BINARIES_DIR = "cli/bin"
|
||||
const IS_DEBUG_BUILD = process.env.IS_DEBUG_BUILD === "true"
|
||||
|
||||
// This should match the node version packaged with the JetBrains plugin.
|
||||
@@ -30,63 +28,15 @@ const SUPPORTED_BINARY_MODULES = ["better-sqlite3"]
|
||||
const UNIVERSAL_BUILD = !process.argv.includes("-s")
|
||||
const IS_VERBOSE = process.argv.includes("-v") || process.argv.includes("--verbose")
|
||||
|
||||
// Parse --target flag (e.g., --target=npm)
|
||||
// Default behavior is JetBrains build (no binaries)
|
||||
// Use --target=npm for npm package build (CLI binaries but no Node.js)
|
||||
const targetArg = process.argv.find((arg) => arg.startsWith("--target="))
|
||||
const BUILD_TARGET = targetArg ? targetArg.split("=")[1] : "jetbrains"
|
||||
const IS_NPM_BUILD = BUILD_TARGET === "npm"
|
||||
|
||||
// Detect current platform
|
||||
function getCurrentPlatform() {
|
||||
const platform = os.platform()
|
||||
const arch = os.arch()
|
||||
|
||||
if (platform === "darwin") {
|
||||
return arch === "arm64" ? "darwin-arm64" : "darwin-x64"
|
||||
} else if (platform === "linux") {
|
||||
return "linux-x64"
|
||||
} else if (platform === "win32") {
|
||||
return "win-x64"
|
||||
}
|
||||
throw new Error(`Unsupported platform: ${platform}-${arch}`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const buildType = IS_NPM_BUILD ? "NPM Package" : "JetBrains"
|
||||
console.log(`🚀 Building Cline ${buildType} Package\n`)
|
||||
|
||||
await installNodeDependencies()
|
||||
|
||||
if (IS_NPM_BUILD) {
|
||||
await copyCliBinaries()
|
||||
await copyRipgrepBinary()
|
||||
await copyProtoDescriptors()
|
||||
await createNpmPackageFiles()
|
||||
await createFakeNodeModules()
|
||||
await createNpmIgnoreFile()
|
||||
await createPostinstallScript()
|
||||
}
|
||||
|
||||
if (UNIVERSAL_BUILD && !IS_NPM_BUILD) {
|
||||
console.log("\nBuilding universal package for all platforms...")
|
||||
if (UNIVERSAL_BUILD) {
|
||||
console.log("Building universal package for all platforms...")
|
||||
await packageAllBinaryDeps()
|
||||
} else if (IS_NPM_BUILD) {
|
||||
console.log("\nNPM build: Keeping native modules in node_modules for npm to handle...")
|
||||
} else {
|
||||
console.log(`\nBuilding package for ${os.platform()}-${os.arch()}...`)
|
||||
}
|
||||
|
||||
if (!IS_NPM_BUILD) {
|
||||
console.log("\n📦 Creating final package...")
|
||||
await zipDistribution()
|
||||
}
|
||||
|
||||
console.log("\n✅ Build complete!")
|
||||
if (IS_NPM_BUILD) {
|
||||
console.log(`\n📦 NPM package ready in ${BUILD_DIR}/`)
|
||||
console.log(`To publish: cd ${BUILD_DIR} && npm publish`)
|
||||
console.log(`Building package for ${os.platform()}-${os.arch()}...`)
|
||||
}
|
||||
await zipDistribution()
|
||||
}
|
||||
|
||||
async function installNodeDependencies() {
|
||||
@@ -104,389 +54,6 @@ async function installNodeDependencies() {
|
||||
fs.renameSync(`${BUILD_DIR}/vscode`, `${BUILD_DIR}/node_modules/vscode`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy CLI binaries (cline and cline-host) for all platforms
|
||||
* The Go binaries are cross-compiled for darwin/linux arm64/amd64
|
||||
*/
|
||||
async function copyCliBinaries() {
|
||||
console.log("Copying CLI binaries for all platforms...")
|
||||
|
||||
const platforms = [
|
||||
{ os: "darwin", arch: "arm64" },
|
||||
{ os: "darwin", arch: "amd64" },
|
||||
{ os: "linux", arch: "amd64" },
|
||||
{ os: "linux", arch: "arm64" },
|
||||
]
|
||||
|
||||
const binDir = path.join(BUILD_DIR, "bin")
|
||||
|
||||
// Create bin directory
|
||||
fs.mkdirSync(binDir, { recursive: true })
|
||||
|
||||
// Copy all platform-specific binaries
|
||||
for (const { os, arch } of platforms) {
|
||||
const platformSuffix = `${os}-${arch}`
|
||||
|
||||
// Copy cline binary
|
||||
const clineSource = path.join(CLI_BINARIES_DIR, `cline-${platformSuffix}`)
|
||||
const clineDest = path.join(binDir, `cline-${platformSuffix}`)
|
||||
|
||||
if (!fs.existsSync(clineSource)) {
|
||||
console.error(`Error: CLI binary not found at ${clineSource}`)
|
||||
console.error(`Please run: npm run compile-cli`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(clineSource, clineDest)
|
||||
fs.chmodSync(clineDest, 0o755)
|
||||
console.log(`✓ cline-${platformSuffix} copied`)
|
||||
|
||||
// Copy cline-host binary
|
||||
const hostSource = path.join(CLI_BINARIES_DIR, `cline-host-${platformSuffix}`)
|
||||
const hostDest = path.join(binDir, `cline-host-${platformSuffix}`)
|
||||
|
||||
if (!fs.existsSync(hostSource)) {
|
||||
console.error(`Error: CLI binary not found at ${hostSource}`)
|
||||
console.error(`Please run: npm run compile-cli`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(hostSource, hostDest)
|
||||
fs.chmodSync(hostDest, 0o755)
|
||||
console.log(`✓ cline-host-${platformSuffix} copied`)
|
||||
}
|
||||
|
||||
console.log(`✓ All platform binaries copied to ${binDir}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy proto descriptors directory
|
||||
* The proto/descriptor_set.pb file is needed by cline-core for gRPC reflection
|
||||
*/
|
||||
async function copyProtoDescriptors() {
|
||||
console.log("Copying proto descriptors...")
|
||||
|
||||
const protoSource = "proto"
|
||||
const protoDest = path.join(BUILD_DIR, "proto")
|
||||
|
||||
// Check if proto directory exists
|
||||
if (!fs.existsSync(protoSource)) {
|
||||
console.error(`Error: proto directory not found at ${protoSource}`)
|
||||
console.error(`Please ensure the proto files have been generated`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Check if descriptor_set.pb exists
|
||||
const descriptorPath = path.join(protoSource, "descriptor_set.pb")
|
||||
if (!fs.existsSync(descriptorPath)) {
|
||||
console.error(`Error: proto/descriptor_set.pb not found at ${descriptorPath}`)
|
||||
console.error(`Please run: npm run protos`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Copy the entire proto directory
|
||||
await cpr(protoSource, protoDest)
|
||||
|
||||
console.log(`✓ Proto descriptors copied to ${protoDest}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy ripgrep binary for the current platform
|
||||
* Ripgrep is needed by cline-core for file searching
|
||||
*/
|
||||
async function copyRipgrepBinary() {
|
||||
const currentPlatform = getCurrentPlatform()
|
||||
const binaryName = currentPlatform.startsWith("win") ? "rg.exe" : "rg"
|
||||
const ripgrepBinarySource = path.join(RIPGREP_BINARIES_DIR, currentPlatform, binaryName)
|
||||
const ripgrepBinaryDest = path.join(BUILD_DIR, binaryName)
|
||||
|
||||
console.log(`Copying ripgrep binary for ${currentPlatform}...`)
|
||||
|
||||
// Check if ripgrep binaries exist, download if missing
|
||||
if (!fs.existsSync(ripgrepBinarySource)) {
|
||||
console.log(`Ripgrep binary not found, downloading...`)
|
||||
try {
|
||||
execSync("npm run download-ripgrep", { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
console.error(`Error downloading ripgrep: ${error.message}`)
|
||||
console.error(`Please run: npm run download-ripgrep`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Check again after download
|
||||
if (!fs.existsSync(ripgrepBinarySource)) {
|
||||
console.error(`Error: Ripgrep binary still not found at ${ripgrepBinarySource}`)
|
||||
console.error(`Download may have failed. Please run: npm run download-ripgrep`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy ripgrep binary to the root of dist-standalone (where cline-core.js is)
|
||||
await cpr(ripgrepBinarySource, ripgrepBinaryDest)
|
||||
|
||||
// Make it executable (Unix only)
|
||||
if (!currentPlatform.startsWith("win")) {
|
||||
fs.chmodSync(ripgrepBinaryDest, 0o755)
|
||||
}
|
||||
|
||||
console.log(`✓ Ripgrep binary copied to ${ripgrepBinaryDest}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a VERSION file with build metadata
|
||||
*/
|
||||
async function createVersionFile() {
|
||||
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"))
|
||||
const version = packageJson.version
|
||||
const platform = getCurrentPlatform()
|
||||
const buildDate = new Date().toISOString()
|
||||
|
||||
const versionInfo = {
|
||||
version,
|
||||
platform,
|
||||
buildDate,
|
||||
nodeVersion: TARGET_NODE_VERSION,
|
||||
}
|
||||
|
||||
const versionPath = path.join(BUILD_DIR, "VERSION.txt")
|
||||
fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2))
|
||||
|
||||
console.log(`✓ VERSION file created: ${version} (${platform})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy NPM package files (package.json, README.md, and man page) from cli/ directory
|
||||
*/
|
||||
async function createNpmPackageFiles() {
|
||||
console.log("Copying NPM package files...")
|
||||
|
||||
// Copy package.json from cli/ directory
|
||||
const packageJsonSource = path.join("cli", "package.json")
|
||||
const packageJsonDest = path.join(BUILD_DIR, "package.json")
|
||||
|
||||
if (!fs.existsSync(packageJsonSource)) {
|
||||
console.error(`Error: NPM package.json not found at ${packageJsonSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(packageJsonSource, packageJsonDest)
|
||||
console.log(`✓ package.json copied from ${packageJsonSource}`)
|
||||
|
||||
// Copy README.md from cli/ directory
|
||||
const readmeSource = path.join("cli", "README.md")
|
||||
const readmeDest = path.join(BUILD_DIR, "README.md")
|
||||
|
||||
if (!fs.existsSync(readmeSource)) {
|
||||
console.error(`Error: NPM README.md not found at ${readmeSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(readmeSource, readmeDest)
|
||||
console.log(`✓ README.md copied from ${readmeSource}`)
|
||||
|
||||
// Copy man page from cli/man/ directory
|
||||
const manPageSource = path.join("cli", "man", "cline.1")
|
||||
const manDir = path.join(BUILD_DIR, "man")
|
||||
const manPageDest = path.join(manDir, "cline.1")
|
||||
|
||||
if (!fs.existsSync(manPageSource)) {
|
||||
console.error(`Error: Man page not found at ${manPageSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create man directory if it doesn't exist
|
||||
fs.mkdirSync(manDir, { recursive: true })
|
||||
|
||||
await cpr(manPageSource, manPageDest)
|
||||
console.log(`✓ Man page copied from ${manPageSource}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fake_node_modules directory with vscode stub
|
||||
* This directory will be added to NODE_PATH so Node.js can find the vscode module
|
||||
* without npm interfering with the real node_modules directory
|
||||
*/
|
||||
async function createFakeNodeModules() {
|
||||
console.log("Creating fake_node_modules with vscode stub...")
|
||||
|
||||
const vscodeSource = path.join(BUILD_DIR, "node_modules", "vscode")
|
||||
const fakeNodeModulesDir = path.join(BUILD_DIR, "fake_node_modules")
|
||||
const vscodeDest = path.join(fakeNodeModulesDir, "vscode")
|
||||
|
||||
if (!fs.existsSync(vscodeSource)) {
|
||||
console.error(`Error: vscode stub module not found at ${vscodeSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create fake_node_modules directory
|
||||
fs.mkdirSync(fakeNodeModulesDir, { recursive: true })
|
||||
|
||||
// Copy vscode stub into fake_node_modules
|
||||
await cpr(vscodeSource, vscodeDest)
|
||||
|
||||
console.log(`✓ fake_node_modules/vscode created at ${vscodeDest}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create .npmignore file to ensure necessary files are included
|
||||
*/
|
||||
async function createNpmIgnoreFile() {
|
||||
console.log("Creating .npmignore file...")
|
||||
|
||||
// Create .npmignore that excludes build artifacts
|
||||
// Note: proto/ directory is NOT excluded because proto/descriptor_set.pb is needed at runtime
|
||||
const npmignoreContent = `# Exclude build artifacts and unnecessary files
|
||||
binaries/
|
||||
ripgrep-binaries/
|
||||
standalone.zip
|
||||
cline-core.js.map
|
||||
package-lock.json
|
||||
tree-sitter*.wasm
|
||||
node_modules/vscode
|
||||
`
|
||||
|
||||
const npmignorePath = path.join(BUILD_DIR, ".npmignore")
|
||||
fs.writeFileSync(npmignorePath, npmignoreContent)
|
||||
|
||||
console.log(`✓ .npmignore created`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create postinstall script for NPM package
|
||||
* This script selects the correct platform-specific binary and creates symlinks
|
||||
*/
|
||||
async function createPostinstallScript() {
|
||||
console.log("Creating postinstall script...")
|
||||
|
||||
const postinstallScript = `#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// Detect current platform and architecture
|
||||
function getPlatformInfo() {
|
||||
const platform = os.platform();
|
||||
const arch = os.arch();
|
||||
|
||||
// Map Node.js arch names to Go arch names
|
||||
let goArch = arch;
|
||||
if (arch === 'x64') {
|
||||
goArch = 'amd64';
|
||||
}
|
||||
|
||||
let goPlatform = platform;
|
||||
|
||||
return { platform: goPlatform, arch: goArch };
|
||||
}
|
||||
|
||||
// Setup platform-specific binaries
|
||||
function setupBinaries() {
|
||||
const { platform, arch } = getPlatformInfo();
|
||||
const platformSuffix = \`\${platform}-\${arch}\`;
|
||||
|
||||
console.log(\`Setting up Cline CLI for \${platformSuffix}...\`);
|
||||
|
||||
const binDir = path.join(__dirname, 'bin');
|
||||
|
||||
// Check if platform-specific binaries exist
|
||||
const clineSource = path.join(binDir, \`cline-\${platformSuffix}\`);
|
||||
const clineHostSource = path.join(binDir, \`cline-host-\${platformSuffix}\`);
|
||||
|
||||
if (!fs.existsSync(clineSource)) {
|
||||
console.error(\`Error: Binary not found for platform \${platformSuffix}\`);
|
||||
console.error(\`Expected: \${clineSource}\`);
|
||||
console.error(\`Supported platforms: darwin-arm64, darwin-amd64, linux-amd64, linux-arm64\`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(clineHostSource)) {
|
||||
console.error(\`Error: Binary not found for platform \${platformSuffix}\`);
|
||||
console.error(\`Expected: \${clineHostSource}\`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create symlinks or copies to the generic names
|
||||
const clineTarget = path.join(binDir, 'cline');
|
||||
const clineHostTarget = path.join(binDir, 'cline-host');
|
||||
|
||||
// Remove existing files if they exist
|
||||
[clineTarget, clineHostTarget].forEach(target => {
|
||||
if (fs.existsSync(target)) {
|
||||
try {
|
||||
fs.unlinkSync(target);
|
||||
} catch (e) {
|
||||
console.warn(\`Warning: Could not remove existing file \${target}: \${e.message}\`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// On Unix, create symlinks; on Windows, copy files
|
||||
if (platform === 'win32') {
|
||||
// Windows: copy files
|
||||
fs.copyFileSync(clineSource, clineTarget);
|
||||
fs.copyFileSync(clineHostSource, clineHostTarget);
|
||||
console.log('✓ Copied platform-specific binaries');
|
||||
} else {
|
||||
// Unix: create symlinks
|
||||
fs.symlinkSync(path.basename(clineSource), clineTarget);
|
||||
fs.symlinkSync(path.basename(clineHostSource), clineHostTarget);
|
||||
console.log('✓ Created symlinks to platform-specific binaries');
|
||||
|
||||
// Make binaries executable
|
||||
try {
|
||||
fs.chmodSync(clineSource, 0o755);
|
||||
fs.chmodSync(clineHostSource, 0o755);
|
||||
fs.chmodSync(clineTarget, 0o755);
|
||||
fs.chmodSync(clineHostTarget, 0o755);
|
||||
} catch (error) {
|
||||
console.warn(\`Warning: Could not set executable permissions: \${error.message}\`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check ripgrep binary
|
||||
const rgBinary = platform === 'win32' ? 'rg.exe' : 'rg';
|
||||
const rgPath = path.join(__dirname, rgBinary);
|
||||
|
||||
if (!fs.existsSync(rgPath)) {
|
||||
console.error(\`Error: ripgrep binary not found at \${rgPath}\`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Make ripgrep executable (Unix only)
|
||||
if (platform !== 'win32') {
|
||||
try {
|
||||
fs.chmodSync(rgPath, 0o755);
|
||||
} catch (error) {
|
||||
console.warn(\`Warning: Could not set ripgrep executable permissions: \${error.message}\`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✓ Cline CLI installation complete');
|
||||
console.log('');
|
||||
console.log('Usage:');
|
||||
console.log(' cline - Start Cline CLI');
|
||||
console.log(' cline-host - Start Cline host service');
|
||||
console.log('');
|
||||
console.log('Documentation: https://docs.cline.bot');
|
||||
}
|
||||
|
||||
try {
|
||||
setupBinaries();
|
||||
} catch (error) {
|
||||
console.error(\`Installation failed: \${error.message}\`);
|
||||
console.error('Please report this issue at: https://github.com/cline/cline/issues');
|
||||
process.exit(1);
|
||||
}
|
||||
`
|
||||
|
||||
const postinstallPath = path.join(BUILD_DIR, "postinstall.js")
|
||||
fs.writeFileSync(postinstallPath, postinstallScript)
|
||||
fs.chmodSync(postinstallPath, 0o755)
|
||||
|
||||
console.log(`✓ postinstall.js created`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads prebuilt binaries for each platform for the modules that include binaries. It uses `npx prebuild-install`
|
||||
* to download the binary.
|
||||
@@ -539,9 +106,8 @@ async function packageAllBinaryDeps() {
|
||||
}
|
||||
|
||||
async function zipDistribution() {
|
||||
// Default JetBrains build
|
||||
const zipFilename = "standalone.zip"
|
||||
const zipPath = path.join(BUILD_DIR, zipFilename)
|
||||
// Zip the build directory (excluding any pre-existing output zip).
|
||||
const zipPath = path.join(BUILD_DIR, "standalone.zip")
|
||||
const output = fs.createWriteStream(zipPath)
|
||||
const startTime = Date.now()
|
||||
const archive = archiver("zip", { zlib: { level: 6 } })
|
||||
@@ -559,31 +125,15 @@ async function zipDistribution() {
|
||||
})
|
||||
|
||||
archive.pipe(output)
|
||||
|
||||
// Build ignore lists for build directory and extension directory
|
||||
const ignorePatterns = ["standalone.zip", "standalone-cli.zip"]
|
||||
const extensionIgnores = ["dist/**"]
|
||||
|
||||
// For JetBrains builds, exclude binaries from both directories
|
||||
// JetBrains provides their own Node.js, so exclude all binaries
|
||||
ignorePatterns.push(
|
||||
"bin/**", // Exclude entire bin directory
|
||||
"node-binaries/**", // Exclude all platform-specific Node.js binaries
|
||||
)
|
||||
extensionIgnores.push(
|
||||
"cli/bin/**", // Exclude CLI binaries from extension
|
||||
"node-binaries/**", // Exclude node-binaries from extension
|
||||
)
|
||||
console.log("JetBrains build: Excluding Node.js and CLI binaries (JetBrains provides its own Node.js)")
|
||||
|
||||
// Add all the files from the standalone build dir.
|
||||
archive.glob("**/*", {
|
||||
cwd: BUILD_DIR,
|
||||
ignore: ignorePatterns,
|
||||
ignore: ["standalone.zip"],
|
||||
})
|
||||
|
||||
// Exclude the same files as the VCE vscode extension packager.
|
||||
const isIgnored = createIsIgnored(extensionIgnores)
|
||||
// Also ignore the dist directory, the build directory for the extension.
|
||||
const isIgnored = createIsIgnored(["dist/**"])
|
||||
|
||||
// Add the whole cline directory under "extension", except the for the ignored files.
|
||||
archive.directory(process.cwd(), "extension", (entry) => {
|
||||
|
||||
+2
-13
@@ -76,19 +76,8 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
|
||||
await showVersionUpdateAnnouncement(context)
|
||||
|
||||
// Initialize banner service
|
||||
BannerService.initialize(webview.controller)
|
||||
BannerService.get()
|
||||
.fetchActiveBanners()
|
||||
.then((banners) => {
|
||||
if (banners.length > 0) {
|
||||
Logger.log(`BannerService: ${banners.length} active banner(s) fetched.`)
|
||||
// Banners are now cached and can be accessed by the frontend when needed
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.error("BannerService: Failed to fetch banners on startup", error)
|
||||
})
|
||||
// Initialize banner service and fetch banners from the API.
|
||||
BannerService.initialize(webview.controller).getActiveBanners(true)
|
||||
|
||||
telemetryService.captureExtensionActivated()
|
||||
|
||||
|
||||
@@ -376,10 +376,14 @@ function createHandlerForProvider(
|
||||
return new VercelAIGatewayHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
vercelAiGatewayApiKey: options.vercelAiGatewayApiKey,
|
||||
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
|
||||
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
|
||||
openRouterModelId:
|
||||
mode === "plan" ? options.planModeVercelAiGatewayModelId : options.actModeVercelAiGatewayModelId,
|
||||
openRouterModelInfo:
|
||||
mode === "plan" ? options.planModeVercelAiGatewayModelInfo : options.actModeVercelAiGatewayModelInfo,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
geminiThinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
|
||||
})
|
||||
case "zai":
|
||||
return new ZAiHandler({
|
||||
@@ -394,6 +398,7 @@ function createHandlerForProvider(
|
||||
ocaBaseUrl: options.ocaBaseUrl,
|
||||
ocaModelId: mode === "plan" ? options.planModeOcaModelId : options.actModeOcaModelId,
|
||||
ocaModelInfo: mode === "plan" ? options.planModeOcaModelInfo : options.actModeOcaModelInfo,
|
||||
ocaReasoningEffort: mode === "plan" ? options.planModeOcaReasoningEffort : options.actModeOcaReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
ocaUsePromptCache:
|
||||
|
||||
@@ -259,7 +259,7 @@ export class AIhubmixHandler implements ApiHandler {
|
||||
const stream = await client.chat.completions.create(fixedRequestBody)
|
||||
|
||||
for await (const chunk of stream as any) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -199,7 +199,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// @ts-ignore-next-line
|
||||
let totalCost = (chunk.usage.cost || 0) + (chunk.usage.cost_details?.upstream_inference_cost || 0)
|
||||
|
||||
if (["x-ai/grok-code-fast-1", "minimax/minimax-m2.1"].includes(this.getModel().id)) {
|
||||
if (["x-ai/grok-code-fast-1", "kwaipilot/kat-coder-pro"].includes(this.getModel().id)) {
|
||||
totalCost = 0
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -67,7 +67,7 @@ export class DoubaoHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -60,7 +60,7 @@ export class FireworksHandler implements ApiHandler {
|
||||
|
||||
let reasoning: string | null = null
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (reasoning || delta?.content?.includes("<think>")) {
|
||||
reasoning = (reasoning || "") + (delta.content ?? "")
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ export class GroqHandler implements ApiHandler {
|
||||
const stream = await client.chat.completions.create(requestParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
|
||||
// Handle reasoning field if present (for reasoning models with parsed output)
|
||||
if ((delta as any)?.reasoning) {
|
||||
|
||||
@@ -66,7 +66,7 @@ export class HicapHandler implements ApiHandler {
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -86,7 +86,7 @@ export class HuaweiCloudMaaSHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
|
||||
// Handle reasoning content detection
|
||||
if (delta?.content) {
|
||||
|
||||
@@ -97,7 +97,7 @@ export class HuggingFaceHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of stream) {
|
||||
_chunkCount++
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
_totalContent += delta.content
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
} as LiteLlmChatCompletionCreateParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
|
||||
// Handle normal text content
|
||||
if (delta?.content) {
|
||||
|
||||
@@ -60,7 +60,7 @@ export class LmStudioHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const choice = chunk.choices[0]
|
||||
const choice = chunk.choices?.[0]
|
||||
const delta = choice?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
|
||||
@@ -62,7 +62,7 @@ export class MoonshotHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -57,7 +57,7 @@ export class NebiusHandler implements ApiHandler {
|
||||
})
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -55,7 +55,7 @@ export class NousResearchHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LiteLLMModelInfo, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "@shared/api"
|
||||
import { liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults, ModelInfo } from "@shared/api"
|
||||
import OpenAI, { APIError, OpenAIError } from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
@@ -9,18 +9,23 @@ import {
|
||||
} from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { OcaModelInfo } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiFormat } from "@/shared/proto/index.cline"
|
||||
import { ApiHandler, type CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { getOpenAIToolParams, ToolCallProcessor } from "../transform/tool-call-processor"
|
||||
import { handleResponsesApiStreamResponse } from "../utils/responses_api_support"
|
||||
|
||||
export interface OcaHandlerOptions extends CommonApiHandlerOptions {
|
||||
ocaBaseUrl?: string
|
||||
ocaModelId?: string
|
||||
ocaModelInfo?: LiteLLMModelInfo
|
||||
ocaModelInfo?: OcaModelInfo
|
||||
ocaReasoningEffort?: string
|
||||
thinkingBudgetTokens?: number
|
||||
ocaUsePromptCache?: boolean
|
||||
taskId?: string
|
||||
@@ -100,7 +105,7 @@ export class OcaHandler implements ApiHandler {
|
||||
return this.client
|
||||
}
|
||||
|
||||
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
|
||||
async getApiCosts(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
|
||||
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.ocaModelId || liteLlmDefaultModelId
|
||||
@@ -138,8 +143,29 @@ export class OcaHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async calculateCost(
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
_cacheWriteTokens?: number,
|
||||
_cacheReadTokens?: number,
|
||||
) {
|
||||
const inputCost = (await this.getApiCosts(1e6, 0)) || 0
|
||||
const outputCost = (await this.getApiCosts(0, 1e6)) || 0
|
||||
const totalCost = (inputCost * inputTokens) / 1e6 + (outputCost * outputTokens) / 1e6
|
||||
return totalCost
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
if (this.options.ocaModelInfo?.apiFormat == ApiFormat.OPENAI_RESPONSES) {
|
||||
yield* this.createMessageResponsesApi(systemPrompt, messages, tools)
|
||||
} else {
|
||||
yield* this.createMessageChatApi(systemPrompt, messages, tools)
|
||||
}
|
||||
}
|
||||
|
||||
async *createMessageChatApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const formattedMessages = convertToOpenAiMessages(messages)
|
||||
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
||||
@@ -193,7 +219,7 @@ export class OcaHandler implements ApiHandler {
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
const chatCompletionsParams: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: this.options.ocaModelId || liteLlmDefaultModelId,
|
||||
messages: [enhancedSystemMessage, ...enhancedMessages],
|
||||
temperature,
|
||||
@@ -206,13 +232,16 @@ export class OcaHandler implements ApiHandler {
|
||||
litellm_session_id: `cline-${this.options.taskId}`,
|
||||
...getOpenAIToolParams(tools),
|
||||
}), // Add session ID for LiteLLM tracking
|
||||
})
|
||||
}
|
||||
|
||||
const inputCost = (await this.calculateCost(1e6, 0)) || 0
|
||||
const outputCost = (await this.calculateCost(0, 1e6)) || 0
|
||||
if (this.options.ocaModelInfo?.supportsReasoningEffort) {
|
||||
chatCompletionsParams["reasoning_effort"] = this.options.ocaReasoningEffort || ("medium" as any)
|
||||
}
|
||||
|
||||
const stream = await client.chat.completions.create(chatCompletionsParams)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
|
||||
// Handle normal text content
|
||||
if (delta?.content) {
|
||||
@@ -241,8 +270,11 @@ export class OcaHandler implements ApiHandler {
|
||||
|
||||
// Handle token usage information
|
||||
if (chunk.usage) {
|
||||
const totalCost =
|
||||
(inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6
|
||||
const totalCost = await this.calculateCost(
|
||||
this.options.ocaModelInfo!,
|
||||
chunk.usage.prompt_tokens,
|
||||
chunk.usage.completion_tokens,
|
||||
)
|
||||
|
||||
// Extract cache-related information if available
|
||||
// Need to use type assertion since these properties are not in the standard OpenAI types
|
||||
@@ -270,6 +302,43 @@ export class OcaHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
async *createMessageResponsesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
|
||||
// Convert messages to Responses API input format
|
||||
const input: OpenAI.Responses.ResponseInputItem[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAIResponsesInput(messages),
|
||||
]
|
||||
|
||||
// Convert ChatCompletion tools to Responses API format if provided
|
||||
const responseTools = tools
|
||||
?.filter((tool) => tool.type === "function")
|
||||
.map((tool: any) => ({
|
||||
type: "function" as const,
|
||||
name: tool.function.name,
|
||||
description: tool.function.description,
|
||||
parameters: tool.function.parameters,
|
||||
strict: tool.function.strict ?? true, // Responses API defaults to strict mode
|
||||
}))
|
||||
|
||||
const responsesParams: OpenAI.Responses.ResponseCreateParamsStreaming = {
|
||||
model: this.options.ocaModelId || liteLlmDefaultModelId,
|
||||
input,
|
||||
stream: true,
|
||||
tools: responseTools,
|
||||
}
|
||||
|
||||
if (this.options.ocaModelInfo && this.options.ocaModelInfo.supportsReasoning) {
|
||||
responsesParams["reasoning"] = { effort: this.options.ocaReasoningEffort as any, summary: "auto" }
|
||||
}
|
||||
|
||||
// Create the response using Responses API
|
||||
const stream = await client.responses.create(responsesParams)
|
||||
|
||||
yield* handleResponsesApiStreamResponse(stream, this.options.ocaModelInfo!, this.calculateCost.bind(this))
|
||||
}
|
||||
|
||||
getModel() {
|
||||
return {
|
||||
id: this.options.ocaModelId || liteLlmDefaultModelId,
|
||||
|
||||
@@ -123,7 +123,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -132,7 +132,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -113,7 +113,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
this.lastGenerationId = chunk.id
|
||||
}
|
||||
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -122,7 +122,7 @@ export class QwenHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -99,7 +99,7 @@ export class RequestyHandler implements ApiHandler {
|
||||
let lastUsage: any
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -68,7 +68,7 @@ export class SambanovaHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -66,7 +66,7 @@ export class TogetherHandler implements ApiHandler {
|
||||
})
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "@shared/api"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
@@ -13,7 +14,9 @@ interface VercelAIGatewayHandlerOptions extends CommonApiHandlerOptions {
|
||||
vercelAiGatewayApiKey?: string
|
||||
openRouterModelId?: string
|
||||
openRouterModelInfo?: ModelInfo
|
||||
reasoningEffort?: string
|
||||
thinkingBudgetTokens?: number
|
||||
geminiThinkingLevel?: string
|
||||
}
|
||||
|
||||
export class VercelAIGatewayHandler implements ApiHandler {
|
||||
@@ -58,15 +61,18 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
systemPrompt,
|
||||
messages,
|
||||
{ id: modelId, info: modelInfo },
|
||||
this.options.reasoningEffort,
|
||||
this.options.thinkingBudgetTokens,
|
||||
tools,
|
||||
this.options.geminiThinkingLevel,
|
||||
)
|
||||
let didOutputUsage: boolean = false
|
||||
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -79,7 +85,8 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Reasoning tokens are returned separately from the content
|
||||
if ("reasoning" in delta && delta.reasoning) {
|
||||
// Skip reasoning content for models that don't support it (e.g., devstral, grok-4)
|
||||
if ("reasoning" in delta && delta.reasoning && !shouldSkipReasoningForModel(this.options.openRouterModelId)) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: typeof delta.reasoning === "string" ? delta.reasoning : JSON.stringify(delta.reasoning),
|
||||
@@ -91,7 +98,8 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
"reasoning_details" in delta &&
|
||||
delta.reasoning_details &&
|
||||
// @ts-ignore-next-line
|
||||
delta.reasoning_details.length // exists and non-0
|
||||
delta.reasoning_details.length && // exists and non-0
|
||||
!shouldSkipReasoningForModel(this.options.openRouterModelId)
|
||||
) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
|
||||
@@ -69,7 +69,7 @@ export class XAIHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -95,7 +95,7 @@ export class ZAiHandler implements ApiHandler {
|
||||
const toolCallProcessor = new ToolCallProcessor()
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
const delta = chunk.choices?.[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
|
||||
@@ -145,18 +145,13 @@ export function convertToOpenAiMessages(
|
||||
const thinkingBlock = []
|
||||
if (nonToolMessages.length > 0) {
|
||||
nonToolMessages.forEach((part) => {
|
||||
// @ts-ignore-next-line
|
||||
if (part.type === "text" && part.reasoning_details) {
|
||||
// @ts-ignore-next-line
|
||||
if (Array.isArray(part.reasoning_details)) {
|
||||
// @ts-ignore-next-line
|
||||
reasoningDetails.push(...part.reasoning_details)
|
||||
const anyPart = part as any
|
||||
if (part.type === "text" && anyPart.reasoning_details) {
|
||||
if (Array.isArray(anyPart.reasoning_details)) {
|
||||
reasoningDetails.push(...anyPart.reasoning_details)
|
||||
} else {
|
||||
// @ts-ignore-next-line
|
||||
reasoningDetails.push(part.reasoning_details)
|
||||
reasoningDetails.push(anyPart.reasoning_details)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
// delete part.reasoning_details
|
||||
}
|
||||
if (part.type === "thinking" && part.thinking) {
|
||||
// Reasoning details should have been moved to the text block
|
||||
@@ -216,7 +211,7 @@ export function convertToOpenAiMessages(
|
||||
// Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty
|
||||
tool_calls: tool_calls?.length > 0 ? tool_calls : undefined,
|
||||
// Only include reasoning_details when non-empty; sending [] can trigger provider validation issues.
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error
|
||||
reasoning_details: consolidatedReasoningDetails.length > 0 ? consolidatedReasoningDetails : undefined,
|
||||
})
|
||||
}
|
||||
@@ -404,3 +399,60 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
|
||||
|
||||
return anthropicMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes OpenAI messages for Gemini models by removing tool_calls that lack reasoning_details.
|
||||
*
|
||||
* Gemini models require thought signatures for tool calls. When switching providers mid-conversation,
|
||||
* historical tool calls may not include Gemini reasoning details, which can poison the next request.
|
||||
* This function drops tool_calls that lack reasoning_details and their paired tool messages.
|
||||
*
|
||||
* @param messages - Array of OpenAI chat completion messages
|
||||
* @param modelId - The model ID to check if sanitization is needed
|
||||
* @returns Sanitized array of messages (unchanged if not a Gemini model)
|
||||
*/
|
||||
export function sanitizeGeminiMessages(
|
||||
messages: OpenAI.Chat.ChatCompletionMessageParam[],
|
||||
modelId: string,
|
||||
): OpenAI.Chat.ChatCompletionMessageParam[] {
|
||||
if (!modelId.includes("gemini")) {
|
||||
return messages
|
||||
}
|
||||
|
||||
const droppedToolCallIds = new Set<string>()
|
||||
const sanitized: OpenAI.Chat.ChatCompletionMessageParam[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "assistant") {
|
||||
const anyMsg = msg as any
|
||||
const toolCalls = anyMsg.tool_calls
|
||||
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
||||
const reasoningDetails = anyMsg.reasoning_details
|
||||
const hasReasoningDetails = Array.isArray(reasoningDetails) && reasoningDetails.length > 0
|
||||
if (!hasReasoningDetails) {
|
||||
for (const tc of toolCalls) {
|
||||
if (tc?.id) {
|
||||
droppedToolCallIds.add(tc.id)
|
||||
}
|
||||
}
|
||||
// Keep any textual content, but drop the tool_calls themselves.
|
||||
if (anyMsg.content) {
|
||||
sanitized.push({ role: "assistant", content: anyMsg.content } as any)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.role === "tool") {
|
||||
const anyMsg = msg as any
|
||||
if (anyMsg.tool_call_id && droppedToolCallIds.has(anyMsg.tool_call_id)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sanitized.push(msg)
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
import { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { convertToOpenAiMessages } from "./openai-format"
|
||||
import { convertToOpenAiMessages, sanitizeGeminiMessages } from "./openai-format"
|
||||
import { convertToR1Format } from "./r1-format"
|
||||
import { getOpenAIToolParams } from "./tool-call-processor"
|
||||
|
||||
@@ -36,45 +36,8 @@ export async function createOpenRouterStream(
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
}
|
||||
|
||||
// Gemini models require thought signatures for tool calls. When switching providers mid-conversation,
|
||||
// historical tool calls may not include Gemini/OpenRouter reasoning details, which can poison the next request.
|
||||
// Bandaid: for Gemini only, drop tool_calls that lack reasoning_details and their paired tool messages.
|
||||
if (model.id.includes("gemini")) {
|
||||
const droppedToolCallIds = new Set<string>()
|
||||
const sanitized: OpenAI.Chat.ChatCompletionMessageParam[] = []
|
||||
|
||||
for (const msg of openAiMessages) {
|
||||
if (msg.role === "assistant") {
|
||||
const anyMsg = msg as any
|
||||
const toolCalls = anyMsg.tool_calls
|
||||
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
||||
const reasoningDetails = anyMsg.reasoning_details
|
||||
const hasReasoningDetails = Array.isArray(reasoningDetails) && reasoningDetails.length > 0
|
||||
if (!hasReasoningDetails) {
|
||||
for (const tc of toolCalls) {
|
||||
if (tc?.id) droppedToolCallIds.add(tc.id)
|
||||
}
|
||||
// Keep any textual content, but drop the tool_calls themselves.
|
||||
if (anyMsg.content) {
|
||||
sanitized.push({ role: "assistant", content: anyMsg.content } as any)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.role === "tool") {
|
||||
const anyMsg = msg as any
|
||||
if (anyMsg.tool_call_id && droppedToolCallIds.has(anyMsg.tool_call_id)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sanitized.push(msg)
|
||||
}
|
||||
|
||||
openAiMessages = sanitized
|
||||
}
|
||||
// Sanitize messages for Gemini models (removes tool_calls without reasoning_details)
|
||||
openAiMessages = sanitizeGeminiMessages(openAiMessages, model.id)
|
||||
|
||||
// prompt caching: https://openrouter.ai/docs/prompt-caching
|
||||
// this was initially specifically for claude models (some models may 'support prompt caching' automatically without this)
|
||||
@@ -114,7 +77,7 @@ export async function createOpenRouterStream(
|
||||
{
|
||||
type: "text",
|
||||
text: systemPrompt,
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
@@ -134,7 +97,7 @@ export async function createOpenRouterStream(
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
})
|
||||
@@ -217,7 +180,7 @@ export async function createOpenRouterStream(
|
||||
thinkingBudgetTokens &&
|
||||
model.info?.thinkingConfig &&
|
||||
thinkingBudgetTokens > 0 &&
|
||||
!(model.id.includes("gemini") && geminiThinkingLevel)
|
||||
!(model.id.includes("gemini-3") && geminiThinkingLevel)
|
||||
) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: thinkingBudgetTokens }
|
||||
@@ -233,7 +196,7 @@ export async function createOpenRouterStream(
|
||||
// Skip reasoning for models that don't support it (e.g., devstral, grok-4)
|
||||
const includeReasoning = !shouldSkipReasoningForModel(model.id)
|
||||
|
||||
// @ts-ignore-next-line
|
||||
// @ts-expect-error-next-line
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_tokens: maxTokens,
|
||||
@@ -249,7 +212,7 @@ export async function createOpenRouterStream(
|
||||
...(providerPreferences ? { provider: providerPreferences } : {}),
|
||||
...(isClaudeSonnet1m ? { provider: { order: ["anthropic", "google-vertex/global"], allow_fallbacks: false } } : {}),
|
||||
...getOpenAIToolParams(tools),
|
||||
...(model.id.includes("gemini") && geminiThinkingLevel
|
||||
...(model.id.includes("gemini-3") && geminiThinkingLevel
|
||||
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
|
||||
: {}),
|
||||
})
|
||||
|
||||
@@ -5,9 +5,11 @@ import {
|
||||
openRouterClaudeSonnet41mModelId,
|
||||
openRouterClaudeSonnet451mModelId,
|
||||
} from "@shared/api"
|
||||
import { shouldSkipReasoningForModel } from "@utils/model-utils"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { convertToOpenAiMessages, sanitizeGeminiMessages } from "../transform/openai-format"
|
||||
import { convertToR1Format } from "./r1-format"
|
||||
import { getOpenAIToolParams } from "./tool-call-processor"
|
||||
|
||||
export async function createVercelAIGatewayStream(
|
||||
@@ -15,77 +17,131 @@ export async function createVercelAIGatewayStream(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
model: { id: string; info: ModelInfo },
|
||||
reasoningEffort?: string,
|
||||
thinkingBudgetTokens?: number,
|
||||
tools?: OpenAITool[],
|
||||
geminiThinkingLevel?: string,
|
||||
) {
|
||||
// Convert Anthropic messages to OpenAI format
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const isClaudeSonnet1m = model.id === openRouterClaudeSonnet41mModelId || model.id === openRouterClaudeSonnet451mModelId
|
||||
if (isClaudeSonnet1m) {
|
||||
// remove the custom :1m suffix, to create the model id openrouter API expects
|
||||
// remove the custom :1m suffix, to create the model id the API expects
|
||||
model.id = model.id.slice(0, -CLAUDE_SONNET_1M_SUFFIX.length)
|
||||
}
|
||||
|
||||
// Sanitize messages for Gemini models (removes tool_calls without reasoning_details)
|
||||
openAiMessages = sanitizeGeminiMessages(openAiMessages, model.id)
|
||||
|
||||
// Prompt caching for supported models
|
||||
// This handles cache_control for Claude and MiniMax models
|
||||
const isAnthropicModel = model.id.startsWith("anthropic/")
|
||||
const isMinimaxModel = model.id.startsWith("minimax/")
|
||||
|
||||
if (isAnthropicModel || isMinimaxModel) {
|
||||
openAiMessages[0] = {
|
||||
role: "system",
|
||||
content: systemPrompt,
|
||||
// @ts-ignore-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: systemPrompt,
|
||||
// @ts-expect-error-next-line
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// Add cache_control to the last two user messages for conversation context caching
|
||||
// Add cache_control to the last two user messages
|
||||
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
|
||||
const lastTwoUserMessages = openAiMessages.filter((msg) => msg.role === "user").slice(-2)
|
||||
lastTwoUserMessages.forEach((msg) => {
|
||||
if (typeof msg.content === "string" && msg.content.length > 0) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = [{ type: "text", text: msg.content }]
|
||||
}
|
||||
if (Array.isArray(msg.content)) {
|
||||
// Find the last text part in the message content
|
||||
const lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
|
||||
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
|
||||
|
||||
if (lastTextPart && lastTextPart.text && lastTextPart.text.length > 0) {
|
||||
// @ts-ignore-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
if (!lastTextPart) {
|
||||
lastTextPart = { type: "text", text: "..." }
|
||||
msg.content.push(lastTextPart)
|
||||
}
|
||||
// @ts-expect-error-next-line
|
||||
lastTextPart["cache_control"] = { type: "ephemeral" }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Configure reasoning parameters similar to OpenRouter
|
||||
let temperature: number | undefined = 0
|
||||
// Use max tokens from model info (fetched from Vercel API)
|
||||
const maxTokens = model.info?.maxTokens || undefined
|
||||
|
||||
// Use temperature from model info, default to 0
|
||||
// Model-specific temperatures are derived in refreshVercelAiGatewayModels.ts
|
||||
let temperature: number | undefined = model.info?.temperature ?? 0
|
||||
let topP: number | undefined
|
||||
|
||||
// R1 format conversion for DeepSeek and similar reasoning models
|
||||
const requiresR1Format =
|
||||
model.id.startsWith("deepseek/deepseek-r1") ||
|
||||
model.id === "perplexity/sonar-reasoning" ||
|
||||
model.id === "qwen/qwq-32b:free" ||
|
||||
model.id === "qwen/qwq-32b"
|
||||
|
||||
if (requiresR1Format) {
|
||||
topP = 0.95
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
if (model.id.startsWith("google/gemini-3.0") || model.id === "google/gemini-3.0") {
|
||||
// Recommended value from google
|
||||
temperature = 1.0
|
||||
}
|
||||
|
||||
// Reasoning/thinking budget configuration
|
||||
let reasoning: { max_tokens: number } | undefined
|
||||
|
||||
if (isAnthropicModel) {
|
||||
const budget_tokens = thinkingBudgetTokens || 0
|
||||
const reasoningOn = budget_tokens !== 0
|
||||
if (reasoningOn) {
|
||||
// Check if it's an Anthropic Claude model that supports thinking
|
||||
const isClaudeThinkingModel = model.id.startsWith("anthropic/claude") && model.info?.thinkingConfig
|
||||
|
||||
if (isClaudeThinkingModel) {
|
||||
// For Claude models, match OpenRouter behavior: check even if thinkingBudgetTokens is 0
|
||||
const budgetTokens = thinkingBudgetTokens || 0
|
||||
if (budgetTokens !== 0) {
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: budget_tokens }
|
||||
reasoning = { max_tokens: budgetTokens }
|
||||
}
|
||||
} else if (thinkingBudgetTokens && model.info?.thinkingConfig && thinkingBudgetTokens > 0) {
|
||||
} else if (
|
||||
thinkingBudgetTokens &&
|
||||
thinkingBudgetTokens > 0 &&
|
||||
model.info?.thinkingConfig &&
|
||||
!(model.id.includes("gemini-3") && geminiThinkingLevel)
|
||||
) {
|
||||
// For other models with thinkingConfig, use the standard check
|
||||
temperature = undefined // extended thinking does not support non-1 temperature
|
||||
reasoning = { max_tokens: thinkingBudgetTokens }
|
||||
}
|
||||
|
||||
// @ts-ignore-next-line
|
||||
// Skip reasoning for models that don't support it (e.g., devstral, grok-4)
|
||||
const includeReasoning = !shouldSkipReasoningForModel(model.id)
|
||||
|
||||
// @ts-expect-error-next-line
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_tokens: model.info.maxTokens,
|
||||
max_tokens: maxTokens,
|
||||
temperature: temperature,
|
||||
top_p: topP,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
include_reasoning: true,
|
||||
include_reasoning: includeReasoning,
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...getOpenAIToolParams(tools),
|
||||
...(model.id.includes("gemini-3") && geminiThinkingLevel
|
||||
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
|
||||
: {}),
|
||||
})
|
||||
|
||||
return stream
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import OpenAI from "openai"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ModelInfo } from "@/shared/api"
|
||||
|
||||
// Type that represents the OpenAI ResponseStream with its private properties
|
||||
// The #private property issue can be resolved by using the AsyncIterable interface
|
||||
export async function* handleResponsesApiStreamResponse(
|
||||
stream: AsyncIterable<OpenAI.Responses.ResponseStreamEvent> & { _request_id?: string | null },
|
||||
modelInfo: ModelInfo,
|
||||
calculateCost: (
|
||||
modelInfo: ModelInfo,
|
||||
inputTokens: number,
|
||||
outputTokens: number,
|
||||
cacheWriteTokens: number,
|
||||
cacheReadTokens: number,
|
||||
) => Promise<number>,
|
||||
) {
|
||||
// Process the response stream
|
||||
for await (const chunk of stream) {
|
||||
// Handle different event types from Responses API
|
||||
if (chunk.type === "response.output_item.added") {
|
||||
const item = chunk.item
|
||||
if (item.type === "function_call" && item.id) {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
id: item.id,
|
||||
tool_call: {
|
||||
call_id: item.call_id,
|
||||
function: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
arguments: item.arguments,
|
||||
},
|
||||
},
|
||||
} as const
|
||||
}
|
||||
if (item.type === "reasoning" && item.encrypted_content && item.id) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: item.id,
|
||||
reasoning: "",
|
||||
redacted_data: item.encrypted_content,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
if (chunk.type === "response.output_item.done") {
|
||||
const item = chunk.item
|
||||
if (item.type === "function_call") {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
id: item.id || item.call_id,
|
||||
tool_call: {
|
||||
call_id: item.call_id,
|
||||
function: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
arguments: item.arguments,
|
||||
},
|
||||
},
|
||||
} as const
|
||||
}
|
||||
if (item.type === "reasoning") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: item.id,
|
||||
details: item.summary,
|
||||
reasoning: "",
|
||||
} as const
|
||||
}
|
||||
}
|
||||
if (chunk.type === "response.reasoning_summary_part.added") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: chunk.item_id,
|
||||
reasoning: chunk.part.text,
|
||||
} as const
|
||||
}
|
||||
if (chunk.type === "response.reasoning_summary_text.delta") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: chunk.item_id,
|
||||
reasoning: chunk.delta,
|
||||
} as const
|
||||
}
|
||||
if (chunk.type === "response.reasoning_summary_part.done") {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
id: chunk.item_id,
|
||||
details: chunk.part,
|
||||
reasoning: "",
|
||||
} as const
|
||||
}
|
||||
if (chunk.type === "response.output_text.delta") {
|
||||
// Handle text content deltas
|
||||
if (chunk.delta) {
|
||||
yield {
|
||||
id: chunk.item_id,
|
||||
type: "text",
|
||||
text: chunk.delta,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
if (chunk.type === "response.reasoning_text.delta") {
|
||||
// Handle reasoning content deltas
|
||||
if (chunk.delta) {
|
||||
yield {
|
||||
id: chunk.item_id,
|
||||
type: "reasoning",
|
||||
reasoning: chunk.delta,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
if (chunk.type === "response.function_call_arguments.delta") {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
function: {
|
||||
id: chunk.item_id,
|
||||
name: chunk.item_id,
|
||||
arguments: chunk.delta,
|
||||
},
|
||||
},
|
||||
} as const
|
||||
}
|
||||
if (chunk.type === "response.function_call_arguments.done") {
|
||||
// Handle completed function call
|
||||
if (chunk.item_id && chunk.name && chunk.arguments) {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
function: {
|
||||
id: chunk.item_id,
|
||||
name: chunk.name,
|
||||
arguments: chunk.arguments,
|
||||
},
|
||||
},
|
||||
} as const
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
chunk.type === "response.incomplete" &&
|
||||
chunk.response?.status === "incomplete" &&
|
||||
chunk.response?.incomplete_details?.reason === "max_output_tokens"
|
||||
) {
|
||||
console.log("Ran out of tokens")
|
||||
if (chunk.response?.output_text?.length > 0) {
|
||||
console.log("Partial output:", chunk.response.output_text)
|
||||
} else {
|
||||
console.log("Ran out of tokens during reasoning")
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.type === "response.completed" && chunk.response?.usage) {
|
||||
// Handle usage information when response is complete
|
||||
const usage = chunk.response.usage
|
||||
const inputTokens = usage.input_tokens || 0
|
||||
const outputTokens = usage.output_tokens || 0
|
||||
const cacheReadTokens = usage.output_tokens_details?.reasoning_tokens || 0
|
||||
const cacheWriteTokens = usage.input_tokens_details?.cached_tokens || 0
|
||||
const totalTokens = usage.total_tokens || 0
|
||||
const totalCost = await calculateCost(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
Logger.log(`Total tokens from Responses API usage: ${totalTokens}`)
|
||||
const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens)
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: nonCachedInputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
totalCost: totalCost,
|
||||
id: chunk.response.id,
|
||||
} as const
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ export const toolParamNames = [
|
||||
"input",
|
||||
"from_ref",
|
||||
"to_ref",
|
||||
"skill_name",
|
||||
] as const
|
||||
|
||||
export type ToolParamName = (typeof toolParamNames)[number]
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* Unit tests for skills utility functions
|
||||
* Tests skill discovery, override resolution, toggle filtering, and content loading
|
||||
*/
|
||||
|
||||
import { expect } from "chai"
|
||||
import * as fs from "fs"
|
||||
import { afterEach, beforeEach, describe, it } from "mocha"
|
||||
import * as path from "path"
|
||||
import * as sinon from "sinon"
|
||||
|
||||
import * as disk from "@/core/storage/disk"
|
||||
import * as fsUtils from "@/utils/fs"
|
||||
import { discoverSkills, getAvailableSkills, getSkillContent } from "../skills"
|
||||
|
||||
describe("Skills Utility Functions", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let fileExistsStub: sinon.SinonStub
|
||||
let isDirectoryStub: sinon.SinonStub
|
||||
let readdirStub: sinon.SinonStub
|
||||
let statStub: sinon.SinonStub
|
||||
let readFileStub: sinon.SinonStub
|
||||
let ensureSkillsDirStub: sinon.SinonStub
|
||||
|
||||
// Use path.join for OS-independent paths
|
||||
const TEST_CWD = path.join("/test", "project")
|
||||
const GLOBAL_SKILLS_DIR = path.join("/home", "user", ".cline", "skills")
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Stub console.warn to avoid noise in test output
|
||||
sandbox.stub(console, "warn")
|
||||
|
||||
// Stub filesystem utilities
|
||||
fileExistsStub = sandbox.stub(fsUtils, "fileExistsAtPath")
|
||||
isDirectoryStub = sandbox.stub(fsUtils, "isDirectory")
|
||||
readdirStub = sandbox.stub(fs.promises, "readdir")
|
||||
statStub = sandbox.stub(fs.promises, "stat")
|
||||
readFileStub = sandbox.stub(fs.promises, "readFile")
|
||||
ensureSkillsDirStub = sandbox.stub(disk, "ensureSkillsDirectoryExists")
|
||||
|
||||
// Default: global skills dir
|
||||
ensureSkillsDirStub.resolves(GLOBAL_SKILLS_DIR)
|
||||
|
||||
// Default: no directories exist
|
||||
fileExistsStub.resolves(false)
|
||||
isDirectoryStub.resolves(false)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe("discoverSkills", () => {
|
||||
it("should discover skills from global directory", async () => {
|
||||
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-skill")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["my-skill"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
|
||||
name: my-skill
|
||||
description: A test skill
|
||||
---
|
||||
Instructions here`)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(1)
|
||||
expect(skills[0].name).to.equal("my-skill")
|
||||
expect(skills[0].description).to.equal("A test skill")
|
||||
expect(skills[0].source).to.equal("global")
|
||||
})
|
||||
|
||||
it("should discover skills from project .clinerules/skills directory", async () => {
|
||||
const projectSkillsDir = path.join(TEST_CWD, ".clinerules", "skills")
|
||||
const skillDir = path.join(projectSkillsDir, "explaining-code")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(projectSkillsDir).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(projectSkillsDir).resolves(true)
|
||||
readdirStub.withArgs(projectSkillsDir).resolves(["explaining-code"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
|
||||
name: explaining-code
|
||||
description: Explains code with diagrams and analogies
|
||||
---
|
||||
Use analogies and ASCII diagrams when explaining code.`)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(1)
|
||||
expect(skills[0].name).to.equal("explaining-code")
|
||||
expect(skills[0].source).to.equal("project")
|
||||
})
|
||||
|
||||
it("should discover skills from project .cline/skills directory", async () => {
|
||||
const clineSkillsDir = path.join(TEST_CWD, ".cline", "skills")
|
||||
const skillDir = path.join(clineSkillsDir, "debugging")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(clineSkillsDir).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(clineSkillsDir).resolves(true)
|
||||
readdirStub.withArgs(clineSkillsDir).resolves(["debugging"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
|
||||
name: debugging
|
||||
description: Debug code systematically
|
||||
---
|
||||
Use systematic debugging approaches.`)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(1)
|
||||
expect(skills[0].name).to.equal("debugging")
|
||||
expect(skills[0].source).to.equal("project")
|
||||
})
|
||||
|
||||
it("should discover skills from project .claude/skills directory", async () => {
|
||||
const claudeSkillsDir = path.join(TEST_CWD, ".claude", "skills")
|
||||
const skillDir = path.join(claudeSkillsDir, "coding")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(claudeSkillsDir).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(claudeSkillsDir).resolves(true)
|
||||
readdirStub.withArgs(claudeSkillsDir).resolves(["coding"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
|
||||
name: coding
|
||||
description: Write clean code
|
||||
---
|
||||
Follow best practices.`)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(1)
|
||||
expect(skills[0].name).to.equal("coding")
|
||||
expect(skills[0].source).to.equal("project")
|
||||
})
|
||||
|
||||
it("should handle empty skills directories gracefully", async () => {
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves([])
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it("should skip non-directory entries in skills folder", async () => {
|
||||
const readmePath = path.join(GLOBAL_SKILLS_DIR, "README.md")
|
||||
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-skill")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["README.md", "my-skill"])
|
||||
statStub.withArgs(readmePath).resolves({ isDirectory: () => false })
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
|
||||
name: my-skill
|
||||
description: A skill
|
||||
---
|
||||
Content`)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(1)
|
||||
expect(skills[0].name).to.equal("my-skill")
|
||||
})
|
||||
|
||||
it("should skip skill directories without SKILL.md", async () => {
|
||||
const skillDir = path.join(GLOBAL_SKILLS_DIR, "incomplete-skill")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["incomplete-skill"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(false)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAvailableSkills - Override Resolution", () => {
|
||||
it("should override project skill with global skill of same name", async () => {
|
||||
const globalSkillDir = path.join(GLOBAL_SKILLS_DIR, "coding")
|
||||
const globalSkillMdPath = path.join(globalSkillDir, "SKILL.md")
|
||||
|
||||
// Setup global skill (higher priority)
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
fileExistsStub.withArgs(globalSkillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["coding"])
|
||||
statStub.withArgs(globalSkillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(globalSkillMdPath, "utf-8").resolves(`---
|
||||
name: coding
|
||||
description: Global coding skill
|
||||
---
|
||||
Global instructions`)
|
||||
|
||||
// Setup project skill with same name (lower priority)
|
||||
const projectSkillsDir = path.join(TEST_CWD, ".clinerules", "skills")
|
||||
const projectSkillDir = path.join(projectSkillsDir, "coding")
|
||||
const projectSkillMdPath = path.join(projectSkillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(projectSkillsDir).resolves(true)
|
||||
fileExistsStub.withArgs(projectSkillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(projectSkillsDir).resolves(true)
|
||||
readdirStub.withArgs(projectSkillsDir).resolves(["coding"])
|
||||
statStub.withArgs(projectSkillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(projectSkillMdPath, "utf-8").resolves(`---
|
||||
name: coding
|
||||
description: Project coding skill
|
||||
---
|
||||
Project instructions`)
|
||||
|
||||
const allSkills = await discoverSkills(TEST_CWD)
|
||||
const skills = getAvailableSkills(allSkills)
|
||||
|
||||
expect(skills).to.have.lengthOf(1)
|
||||
expect(skills[0].description).to.equal("Global coding skill")
|
||||
expect(skills[0].source).to.equal("global")
|
||||
})
|
||||
|
||||
it("should keep both skills when names are different", async () => {
|
||||
const globalSkillDir = path.join(GLOBAL_SKILLS_DIR, "global-skill")
|
||||
const globalSkillMdPath = path.join(globalSkillDir, "SKILL.md")
|
||||
|
||||
// Setup global skill
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
fileExistsStub.withArgs(globalSkillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["global-skill"])
|
||||
statStub.withArgs(globalSkillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(globalSkillMdPath, "utf-8").resolves(`---
|
||||
name: global-skill
|
||||
description: A global skill
|
||||
---
|
||||
Content`)
|
||||
|
||||
// Setup project skill with different name
|
||||
const projectSkillsDir = path.join(TEST_CWD, ".clinerules", "skills")
|
||||
const projectSkillDir = path.join(projectSkillsDir, "project-skill")
|
||||
const projectSkillMdPath = path.join(projectSkillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(projectSkillsDir).resolves(true)
|
||||
fileExistsStub.withArgs(projectSkillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(projectSkillsDir).resolves(true)
|
||||
readdirStub.withArgs(projectSkillsDir).resolves(["project-skill"])
|
||||
statStub.withArgs(projectSkillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(projectSkillMdPath, "utf-8").resolves(`---
|
||||
name: project-skill
|
||||
description: A project skill
|
||||
---
|
||||
Content`)
|
||||
|
||||
const allSkills = await discoverSkills(TEST_CWD)
|
||||
const skills = getAvailableSkills(allSkills)
|
||||
|
||||
expect(skills).to.have.lengthOf(2)
|
||||
const names = skills.map((s) => s.name)
|
||||
expect(names).to.include("global-skill")
|
||||
expect(names).to.include("project-skill")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Metadata Validation", () => {
|
||||
it("should reject skill with missing name field", async () => {
|
||||
const skillDir = path.join(GLOBAL_SKILLS_DIR, "bad-skill")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["bad-skill"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
|
||||
description: Missing name
|
||||
---
|
||||
Content`)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(0)
|
||||
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /missing required 'name' field/)
|
||||
})
|
||||
|
||||
it("should reject skill with missing description field", async () => {
|
||||
const skillDir = path.join(GLOBAL_SKILLS_DIR, "bad-skill")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["bad-skill"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
|
||||
name: bad-skill
|
||||
---
|
||||
Content`)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(0)
|
||||
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /missing required 'description' field/)
|
||||
})
|
||||
|
||||
it("should reject skill when name doesn't match directory name", async () => {
|
||||
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-dir")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["my-dir"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
|
||||
name: different-name
|
||||
description: Mismatched name
|
||||
---
|
||||
Content`)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(0)
|
||||
sinon.assert.calledWithMatch(console.warn as sinon.SinonStub, /doesn't match directory/)
|
||||
})
|
||||
|
||||
it("should handle malformed YAML frontmatter gracefully", async () => {
|
||||
const skillDir = path.join(GLOBAL_SKILLS_DIR, "bad-yaml")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["bad-yaml"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
|
||||
name: [invalid yaml
|
||||
description: broken
|
||||
---
|
||||
Content`)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(0)
|
||||
})
|
||||
|
||||
it("should handle file without frontmatter", async () => {
|
||||
const skillDir = path.join(GLOBAL_SKILLS_DIR, "no-front")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["no-front"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`Just plain markdown content without frontmatter`)
|
||||
|
||||
const skills = await discoverSkills(TEST_CWD)
|
||||
|
||||
expect(skills).to.have.lengthOf(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getSkillContent", () => {
|
||||
it("should load full skill content with instructions", async () => {
|
||||
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-skill")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["my-skill"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
|
||||
name: my-skill
|
||||
description: Test skill
|
||||
---
|
||||
These are the detailed instructions.
|
||||
|
||||
## Step 1
|
||||
Do this first.
|
||||
|
||||
## Step 2
|
||||
Then do this.`)
|
||||
|
||||
const allSkills = await discoverSkills(TEST_CWD)
|
||||
const availableSkills = getAvailableSkills(allSkills)
|
||||
const content = await getSkillContent("my-skill", availableSkills)
|
||||
|
||||
expect(content).to.not.be.null
|
||||
expect(content!.name).to.equal("my-skill")
|
||||
expect(content!.instructions).to.include("These are the detailed instructions")
|
||||
expect(content!.instructions).to.include("Step 1")
|
||||
expect(content!.instructions).to.include("Step 2")
|
||||
})
|
||||
|
||||
it("should return null for non-existent skill", async () => {
|
||||
const content = await getSkillContent("non-existent", [])
|
||||
|
||||
expect(content).to.be.null
|
||||
})
|
||||
|
||||
it("should trim whitespace from instructions", async () => {
|
||||
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-skill")
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
|
||||
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
fileExistsStub.withArgs(skillMdPath).resolves(true)
|
||||
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
|
||||
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["my-skill"])
|
||||
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
|
||||
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`---
|
||||
name: my-skill
|
||||
description: Test
|
||||
---
|
||||
|
||||
Instructions with whitespace
|
||||
|
||||
`)
|
||||
|
||||
const allSkills = await discoverSkills(TEST_CWD)
|
||||
const availableSkills = getAvailableSkills(allSkills)
|
||||
const content = await getSkillContent("my-skill", availableSkills)
|
||||
|
||||
expect(content!.instructions).to.equal("Instructions with whitespace")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -65,6 +65,7 @@ export const getLocalClineRules = async (cwd: string, toggles: ClineRulesToggles
|
||||
const rulesFilePaths = await readDirectory(clineRulesFilePath, [
|
||||
[".clinerules", "workflows"],
|
||||
[".clinerules", "hooks"],
|
||||
[".clinerules", "skills"],
|
||||
])
|
||||
|
||||
const rulesFilesTotalContent = await getRuleFilesTotalContent(rulesFilePaths, cwd, toggles)
|
||||
@@ -110,6 +111,7 @@ export async function refreshClineRulesToggles(
|
||||
const updatedLocalToggles = await synchronizeRuleToggles(localClineRulesFilePath, localClineRulesToggles, "", [
|
||||
[".clinerules", "workflows"],
|
||||
[".clinerules", "hooks"],
|
||||
[".clinerules", "skills"],
|
||||
])
|
||||
controller.stateManager.setWorkspaceState("localClineRulesToggles", updatedLocalToggles)
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { ensureSkillsDirectoryExists, GlobalFileNames } from "@core/storage/disk"
|
||||
import type { SkillContent, SkillMetadata } from "@shared/skills"
|
||||
import { fileExistsAtPath, isDirectory } from "@utils/fs"
|
||||
import * as fs from "fs/promises"
|
||||
import * as yaml from "js-yaml"
|
||||
import * as path from "path"
|
||||
|
||||
/**
|
||||
* Parse YAML frontmatter from markdown content.
|
||||
*/
|
||||
function parseFrontmatter(fileContent: string): { data: Record<string, unknown>; content: string } {
|
||||
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
|
||||
const match = fileContent.match(frontmatterRegex)
|
||||
|
||||
if (!match) {
|
||||
return { data: {}, content: fileContent }
|
||||
}
|
||||
|
||||
const [, yamlContent, body] = match
|
||||
try {
|
||||
const data = yaml.load(yamlContent) as Record<string, unknown>
|
||||
return { data: data || {}, content: body }
|
||||
} catch (error) {
|
||||
console.warn("Failed to parse YAML frontmatter:", error)
|
||||
return { data: {}, content: fileContent }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a directory for skill subdirectories containing SKILL.md files.
|
||||
*/
|
||||
async function scanSkillsDirectory(dirPath: string, source: "global" | "project"): Promise<SkillMetadata[]> {
|
||||
const skills: SkillMetadata[] = []
|
||||
|
||||
if (!(await fileExistsAtPath(dirPath)) || !(await isDirectory(dirPath))) {
|
||||
return skills
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath)
|
||||
|
||||
for (const entryName of entries) {
|
||||
const entryPath = path.join(dirPath, entryName)
|
||||
const stats = await fs.stat(entryPath).catch(() => null)
|
||||
if (!stats?.isDirectory()) continue
|
||||
|
||||
const skill = await loadSkillMetadata(entryPath, source, entryName)
|
||||
if (skill) {
|
||||
skills.push(skill)
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "EACCES") {
|
||||
console.warn(`Permission denied reading skills directory: ${dirPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
return skills
|
||||
}
|
||||
|
||||
/**
|
||||
* Load skill metadata from a skill directory.
|
||||
*/
|
||||
async function loadSkillMetadata(
|
||||
skillDir: string,
|
||||
source: "global" | "project",
|
||||
skillName: string,
|
||||
): Promise<SkillMetadata | null> {
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
if (!(await fileExistsAtPath(skillMdPath))) return null
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(skillMdPath, "utf-8")
|
||||
const { data: frontmatter } = parseFrontmatter(fileContent)
|
||||
|
||||
// Validate required fields
|
||||
if (!frontmatter.name || typeof frontmatter.name !== "string") {
|
||||
console.warn(`Skill at ${skillDir} missing required 'name' field`)
|
||||
return null
|
||||
}
|
||||
if (!frontmatter.description || typeof frontmatter.description !== "string") {
|
||||
console.warn(`Skill at ${skillDir} missing required 'description' field`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Name must match directory name per spec
|
||||
if (frontmatter.name !== skillName) {
|
||||
console.warn(`Skill name "${frontmatter.name}" doesn't match directory "${skillName}"`)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
name: skillName,
|
||||
description: frontmatter.description,
|
||||
path: skillMdPath,
|
||||
source,
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load skill at ${skillDir}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover all skills from global (~/.cline/skills) and project directories.
|
||||
* Returns skills in order: project skills first, then global skills.
|
||||
* Global skills take precedence over project skills with the same name.
|
||||
*/
|
||||
export async function discoverSkills(cwd: string): Promise<SkillMetadata[]> {
|
||||
const skills: SkillMetadata[] = []
|
||||
|
||||
const globalSkillsDir = await ensureSkillsDirectoryExists()
|
||||
const projectDirs = [
|
||||
path.join(cwd, GlobalFileNames.clineruleSkillsDir),
|
||||
path.join(cwd, GlobalFileNames.clineSkillsDir),
|
||||
path.join(cwd, GlobalFileNames.claudeSkillsDir),
|
||||
]
|
||||
|
||||
// Load project skills first (lower priority)
|
||||
for (const dir of projectDirs) {
|
||||
const projectSkills = await scanSkillsDirectory(dir, "project")
|
||||
skills.push(...projectSkills)
|
||||
}
|
||||
|
||||
// Load global skills last (~/.cline/skills) - higher priority
|
||||
const globalSkills = await scanSkillsDirectory(globalSkillsDir, "global")
|
||||
skills.push(...globalSkills)
|
||||
|
||||
return skills
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available skills with override resolution (global > project).
|
||||
*/
|
||||
export function getAvailableSkills(skills: SkillMetadata[]): SkillMetadata[] {
|
||||
const seen = new Set<string>()
|
||||
const result: SkillMetadata[] = []
|
||||
|
||||
// Iterate backwards: global skills (added last) are seen first and take precedence
|
||||
for (let i = skills.length - 1; i >= 0; i--) {
|
||||
const skill = skills[i]
|
||||
if (!seen.has(skill.name)) {
|
||||
seen.add(skill.name)
|
||||
result.unshift(skill)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full skill content including instructions.
|
||||
*/
|
||||
export async function getSkillContent(skillName: string, availableSkills: SkillMetadata[]): Promise<SkillContent | null> {
|
||||
const skill = availableSkills.find((s) => s.name === skillName)
|
||||
if (!skill) return null
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(skill.path, "utf-8")
|
||||
const { content: body } = parseFrontmatter(fileContent)
|
||||
|
||||
return {
|
||||
...skill,
|
||||
instructions: body.trim(),
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { CreateSkillRequest, SkillsToggles } from "@shared/proto/cline/file"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { ensureSkillsDirectoryExists } from "@/core/storage/disk"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { fileExistsAtPath } from "@/utils/fs"
|
||||
import { Controller } from ".."
|
||||
import { openFile } from "./openFile"
|
||||
|
||||
const SKILL_TEMPLATE = `---
|
||||
name: {{SKILL_NAME}}
|
||||
description: Brief description of what this skill does
|
||||
---
|
||||
|
||||
# {{SKILL_NAME}}
|
||||
|
||||
Instructions for the AI agent...
|
||||
|
||||
## Usage
|
||||
|
||||
Describe when and how to use this skill.
|
||||
|
||||
## Steps
|
||||
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Third step
|
||||
`
|
||||
|
||||
/**
|
||||
* Creates a new skill from template
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing skill name and isGlobal flag
|
||||
* @returns The updated skills toggles
|
||||
*/
|
||||
export async function createSkillFile(controller: Controller, request: CreateSkillRequest): Promise<SkillsToggles> {
|
||||
const { skillName, isGlobal } = request
|
||||
|
||||
if (!skillName || typeof skillName !== "string" || typeof isGlobal !== "boolean") {
|
||||
console.error("createSkillFile: Missing or invalid parameters", {
|
||||
skillName: typeof skillName === "string" ? skillName : `Invalid: ${typeof skillName}`,
|
||||
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters for createSkillFile")
|
||||
}
|
||||
|
||||
// Validate skill name (must be valid directory name)
|
||||
const sanitizedName = skillName.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase()
|
||||
if (!sanitizedName) {
|
||||
throw new Error("Invalid skill name")
|
||||
}
|
||||
|
||||
let skillDir: string
|
||||
|
||||
if (isGlobal) {
|
||||
const globalSkillsDir = await ensureSkillsDirectoryExists()
|
||||
skillDir = path.join(globalSkillsDir, sanitizedName)
|
||||
} else {
|
||||
const workspacePaths = await HostProvider.workspace.getWorkspacePaths({})
|
||||
const primaryWorkspace = workspacePaths.paths[0]
|
||||
if (!primaryWorkspace) {
|
||||
throw new Error("No workspace folder open")
|
||||
}
|
||||
// Create in .cline/skills by default
|
||||
const localSkillsDir = path.join(primaryWorkspace, ".cline", "skills")
|
||||
await fs.mkdir(localSkillsDir, { recursive: true })
|
||||
skillDir = path.join(localSkillsDir, sanitizedName)
|
||||
}
|
||||
|
||||
// Check if skill already exists
|
||||
if (await fileExistsAtPath(skillDir)) {
|
||||
await HostProvider.window.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message: `Skill "${sanitizedName}" already exists`,
|
||||
})
|
||||
// Return current toggles
|
||||
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
|
||||
const localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
|
||||
return SkillsToggles.create({
|
||||
globalSkillsToggles: globalToggles,
|
||||
localSkillsToggles: localToggles,
|
||||
})
|
||||
}
|
||||
|
||||
// Create skill directory
|
||||
await fs.mkdir(skillDir, { recursive: true })
|
||||
|
||||
// Create SKILL.md from template
|
||||
const skillMdPath = path.join(skillDir, "SKILL.md")
|
||||
const content = SKILL_TEMPLATE.replace(/\{\{SKILL_NAME\}\}/g, sanitizedName)
|
||||
await fs.writeFile(skillMdPath, content, "utf-8")
|
||||
|
||||
// Open the file for editing
|
||||
await openFile(controller, { value: skillMdPath })
|
||||
|
||||
// Return current toggles (new skill defaults to enabled)
|
||||
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
|
||||
const localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
|
||||
|
||||
return SkillsToggles.create({
|
||||
globalSkillsToggles: globalToggles,
|
||||
localSkillsToggles: localToggles,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { DeleteSkillRequest, SkillsToggles } from "@shared/proto/cline/file"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { fileExistsAtPath } from "@/utils/fs"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Deletes an existing skill directory
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing skill path and isGlobal flag
|
||||
* @returns The updated skills toggles
|
||||
*/
|
||||
export async function deleteSkillFile(controller: Controller, request: DeleteSkillRequest): Promise<SkillsToggles> {
|
||||
const { skillPath, isGlobal } = request
|
||||
|
||||
if (!skillPath || typeof skillPath !== "string" || typeof isGlobal !== "boolean") {
|
||||
console.error("deleteSkillFile: Missing or invalid parameters", {
|
||||
skillPath: typeof skillPath === "string" ? skillPath : `Invalid: ${typeof skillPath}`,
|
||||
isGlobal: typeof isGlobal === "boolean" ? isGlobal : `Invalid: ${typeof isGlobal}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters for deleteSkillFile")
|
||||
}
|
||||
|
||||
// Get the skill directory (skillPath points to SKILL.md, so get parent)
|
||||
const skillDir = path.dirname(skillPath)
|
||||
|
||||
// Verify the path exists
|
||||
if (!(await fileExistsAtPath(skillDir))) {
|
||||
console.warn(`deleteSkillFile: Skill directory not found: ${skillDir}`)
|
||||
// Return current toggles anyway
|
||||
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
|
||||
const localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
|
||||
return SkillsToggles.create({
|
||||
globalSkillsToggles: globalToggles,
|
||||
localSkillsToggles: localToggles,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete the skill directory
|
||||
await fs.rm(skillDir, { recursive: true, force: true })
|
||||
|
||||
// Remove from toggles
|
||||
let globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
|
||||
let localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
|
||||
|
||||
if (isGlobal) {
|
||||
const { [skillPath]: _, ...remaining } = globalToggles
|
||||
globalToggles = remaining
|
||||
controller.stateManager.setGlobalState("globalSkillsToggles", globalToggles)
|
||||
} else {
|
||||
const { [skillPath]: _, ...remaining } = localToggles
|
||||
localToggles = remaining
|
||||
controller.stateManager.setWorkspaceState("localSkillsToggles", localToggles)
|
||||
}
|
||||
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return SkillsToggles.create({
|
||||
globalSkillsToggles: globalToggles,
|
||||
localSkillsToggles: localToggles,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { RefreshedSkills, SkillInfo } from "@shared/proto/cline/file"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { ensureSkillsDirectoryExists } from "@/core/storage/disk"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { fileExistsAtPath, isDirectory } from "@/utils/fs"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Parse YAML frontmatter from markdown content.
|
||||
*/
|
||||
function parseFrontmatter(fileContent: string): { data: Record<string, unknown>; content: string } {
|
||||
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
|
||||
const match = fileContent.match(frontmatterRegex)
|
||||
|
||||
if (!match) {
|
||||
return { data: {}, content: fileContent }
|
||||
}
|
||||
|
||||
const [, yamlContent, body] = match
|
||||
// Simple YAML parsing for name and description
|
||||
const data: Record<string, unknown> = {}
|
||||
const lines = yamlContent.split("\n")
|
||||
for (const line of lines) {
|
||||
const colonIndex = line.indexOf(":")
|
||||
if (colonIndex > 0) {
|
||||
const key = line.slice(0, colonIndex).trim()
|
||||
const value = line
|
||||
.slice(colonIndex + 1)
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, "")
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
return { data, content: body }
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a directory for skill subdirectories containing SKILL.md files.
|
||||
*/
|
||||
async function scanSkillsDirectory(dirPath: string): Promise<SkillInfo[]> {
|
||||
const skills: SkillInfo[] = []
|
||||
|
||||
if (!(await fileExistsAtPath(dirPath)) || !(await isDirectory(dirPath))) {
|
||||
return skills
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath)
|
||||
|
||||
for (const entryName of entries) {
|
||||
const entryPath = path.join(dirPath, entryName)
|
||||
const stats = await fs.stat(entryPath).catch(() => null)
|
||||
if (!stats?.isDirectory()) continue
|
||||
|
||||
const skillMdPath = path.join(entryPath, "SKILL.md")
|
||||
if (!(await fileExistsAtPath(skillMdPath))) continue
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(skillMdPath, "utf-8")
|
||||
const { data: frontmatter } = parseFrontmatter(fileContent)
|
||||
|
||||
// Validate required fields
|
||||
if (!frontmatter.name || typeof frontmatter.name !== "string") continue
|
||||
if (!frontmatter.description || typeof frontmatter.description !== "string") continue
|
||||
if (frontmatter.name !== entryName) continue
|
||||
|
||||
skills.push(
|
||||
SkillInfo.create({
|
||||
name: entryName,
|
||||
description: frontmatter.description,
|
||||
path: skillMdPath,
|
||||
enabled: true, // Will be updated with toggle state
|
||||
}),
|
||||
)
|
||||
} catch {
|
||||
// Skip invalid skills
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Directory read error, skip
|
||||
}
|
||||
|
||||
return skills
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes all skill toggles (discovers skills and their enabled state)
|
||||
*/
|
||||
export async function refreshSkills(controller: Controller): Promise<RefreshedSkills> {
|
||||
const globalSkillsDir = await ensureSkillsDirectoryExists()
|
||||
|
||||
// Get workspace paths for local skills
|
||||
const workspacePaths = await HostProvider.workspace.getWorkspacePaths({})
|
||||
const primaryWorkspace = workspacePaths.paths[0]
|
||||
|
||||
// Scan global skills
|
||||
const globalSkills = await scanSkillsDirectory(globalSkillsDir)
|
||||
|
||||
// Get global toggles and apply them
|
||||
const globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
|
||||
for (const skill of globalSkills) {
|
||||
skill.enabled = globalToggles[skill.path] !== false
|
||||
}
|
||||
|
||||
// Scan local skills from all possible directories
|
||||
const localSkills: SkillInfo[] = []
|
||||
if (primaryWorkspace) {
|
||||
const localDirs = [
|
||||
path.join(primaryWorkspace, ".clinerules", "skills"),
|
||||
path.join(primaryWorkspace, ".cline", "skills"),
|
||||
path.join(primaryWorkspace, ".claude", "skills"),
|
||||
]
|
||||
|
||||
for (const dir of localDirs) {
|
||||
const skills = await scanSkillsDirectory(dir)
|
||||
localSkills.push(...skills)
|
||||
}
|
||||
}
|
||||
|
||||
// Get local toggles and apply them
|
||||
const localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
|
||||
for (const skill of localSkills) {
|
||||
skill.enabled = localToggles[skill.path] !== false
|
||||
}
|
||||
|
||||
return RefreshedSkills.create({
|
||||
globalSkills,
|
||||
localSkills,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { SkillsToggles, ToggleSkillRequest } from "@shared/proto/cline/file"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Toggles a skill on or off
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the skill path and enabled state
|
||||
* @returns The updated skills toggles
|
||||
*/
|
||||
export async function toggleSkill(controller: Controller, request: ToggleSkillRequest): Promise<SkillsToggles> {
|
||||
const { skillPath, isGlobal, enabled } = request
|
||||
|
||||
if (!skillPath || typeof enabled !== "boolean" || typeof isGlobal !== "boolean") {
|
||||
console.error("toggleSkill: Missing or invalid parameters", {
|
||||
skillPath,
|
||||
isGlobal,
|
||||
enabled: typeof enabled === "boolean" ? enabled : `Invalid: ${typeof enabled}`,
|
||||
})
|
||||
throw new Error("Missing or invalid parameters for toggleSkill")
|
||||
}
|
||||
|
||||
let globalToggles = controller.stateManager.getGlobalSettingsKey("globalSkillsToggles") || {}
|
||||
let localToggles = controller.stateManager.getWorkspaceStateKey("localSkillsToggles") || {}
|
||||
|
||||
if (isGlobal) {
|
||||
globalToggles = { ...globalToggles, [skillPath]: enabled }
|
||||
controller.stateManager.setGlobalState("globalSkillsToggles", globalToggles)
|
||||
} else {
|
||||
localToggles = { ...localToggles, [skillPath]: enabled }
|
||||
controller.stateManager.setWorkspaceState("localSkillsToggles", localToggles)
|
||||
}
|
||||
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return SkillsToggles.create({
|
||||
globalSkillsToggles: globalToggles,
|
||||
localSkillsToggles: localToggles,
|
||||
})
|
||||
}
|
||||
@@ -30,14 +30,15 @@ import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
import { LogoutReason } from "@/services/auth/types"
|
||||
import { BannerService } from "@/services/banner/BannerService"
|
||||
import { featureFlagsService } from "@/services/feature-flags"
|
||||
import { getDistinctId } from "@/services/logging/distinctId"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { BannerCardData } from "@/shared/cline/banner"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
import { getLatestAnnouncementId } from "@/utils/announcements"
|
||||
import { getCwd, getDesktopDir } from "@/utils/path"
|
||||
import { BannerService } from "../../services/banner/BannerService"
|
||||
import { PromptRegistry } from "../prompts/system-prompt"
|
||||
import {
|
||||
ensureCacheDirectoryExists,
|
||||
@@ -831,6 +832,8 @@ export class Controller {
|
||||
const enableCheckpointsSetting = this.stateManager.getGlobalSettingsKey("enableCheckpointsSetting")
|
||||
const globalClineRulesToggles = this.stateManager.getGlobalSettingsKey("globalClineRulesToggles")
|
||||
const globalWorkflowToggles = this.stateManager.getGlobalSettingsKey("globalWorkflowToggles")
|
||||
const globalSkillsToggles = this.stateManager.getGlobalSettingsKey("globalSkillsToggles")
|
||||
const localSkillsToggles = this.stateManager.getWorkspaceStateKey("localSkillsToggles")
|
||||
const remoteRulesToggles = this.stateManager.getGlobalStateKey("remoteRulesToggles")
|
||||
const remoteWorkflowToggles = this.stateManager.getGlobalStateKey("remoteWorkflowToggles")
|
||||
const shellIntegrationTimeout = this.stateManager.getGlobalSettingsKey("shellIntegrationTimeout")
|
||||
@@ -851,6 +854,7 @@ export class Controller {
|
||||
const lastDismissedModelBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedModelBannerVersion") || 0
|
||||
const lastDismissedCliBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedCliBannerVersion") || 0
|
||||
const subagentsEnabled = this.stateManager.getGlobalSettingsKey("subagentsEnabled")
|
||||
const skillsEnabled = this.stateManager.getGlobalSettingsKey("skillsEnabled")
|
||||
|
||||
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
const localWindsurfRulesToggles = this.stateManager.getWorkspaceStateKey("localWindsurfRulesToggles")
|
||||
@@ -874,6 +878,7 @@ export class Controller {
|
||||
const distinctId = getDistinctId()
|
||||
const version = ExtensionRegistryInfo.version
|
||||
const environment = ClineEnv.config().environment
|
||||
const banners = await this.getBanners()
|
||||
|
||||
// Set feature flag in dictation settings based on platform
|
||||
const updatedDictationSettings = {
|
||||
@@ -914,6 +919,8 @@ export class Controller {
|
||||
localAgentsRulesToggles: localAgentsRulesToggles || {},
|
||||
localWorkflowToggles: workflowToggles || {},
|
||||
globalWorkflowToggles: globalWorkflowToggles || {},
|
||||
globalSkillsToggles: globalSkillsToggles || {},
|
||||
localSkillsToggles: localSkillsToggles || {},
|
||||
remoteRulesToggles: remoteRulesToggles,
|
||||
remoteWorkflowToggles: remoteWorkflowToggles,
|
||||
shellIntegrationTimeout,
|
||||
@@ -955,6 +962,9 @@ export class Controller {
|
||||
nativeToolCallSetting: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
|
||||
backgroundEditEnabled: this.stateManager.getGlobalSettingsKey("backgroundEditEnabled"),
|
||||
skillsEnabled,
|
||||
optOutOfRemoteConfig: this.stateManager.getGlobalSettingsKey("optOutOfRemoteConfig"),
|
||||
banners,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -997,64 +1007,12 @@ export class Controller {
|
||||
return history
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the BannerService if not already initialized
|
||||
*/
|
||||
private async ensureBannerService() {
|
||||
if (!BannerService.isInitialized()) {
|
||||
try {
|
||||
BannerService.initialize(this)
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize BannerService:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches non-dismissed banners for display
|
||||
* @returns Array of banners that haven't been dismissed
|
||||
*/
|
||||
async fetchBannersForDisplay(): Promise<any[]> {
|
||||
async getBanners(): Promise<BannerCardData[]> {
|
||||
try {
|
||||
await this.ensureBannerService()
|
||||
if (BannerService.isInitialized()) {
|
||||
return await BannerService.get().getNonDismissedBanners()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch banners:", error)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses a banner and sends telemetry
|
||||
* @param bannerId The ID of the banner to dismiss
|
||||
*/
|
||||
async dismissBanner(bannerId: string): Promise<void> {
|
||||
try {
|
||||
await this.ensureBannerService()
|
||||
if (BannerService.isInitialized()) {
|
||||
await BannerService.get().dismissBanner(bannerId)
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to dismiss banner:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a banner event for telemetry tracking
|
||||
* @param bannerId The ID of the banner
|
||||
* @param eventType The type of event (seen, dismiss, click)
|
||||
*/
|
||||
async trackBannerEvent(bannerId: string, eventType: "dismiss"): Promise<void> {
|
||||
try {
|
||||
await this.ensureBannerService()
|
||||
if (BannerService.isInitialized()) {
|
||||
await BannerService.get().sendBannerEvent(bannerId, eventType)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to track banner event:", error)
|
||||
return BannerService.get().getActiveBanners()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function refreshLiteLlmModels(): Promise<Record<string, ModelInfo>>
|
||||
try {
|
||||
// Get the LiteLLM configuration
|
||||
const apiConfiguration = stateManager.getApiConfiguration()
|
||||
const baseUrl = apiConfiguration.liteLlmBaseUrl || ""
|
||||
const baseUrl = apiConfiguration.liteLlmBaseUrl || "http://localhost:4000"
|
||||
const apiKey = apiConfiguration.liteLlmApiKey
|
||||
|
||||
if (!apiKey) {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { OcaCompatibleModelInfo, OcaModelInfo } from "@shared/proto/cline/models"
|
||||
import { ApiFormat, OcaCompatibleModelInfo, OcaModelInfo } from "@shared/proto/cline/models"
|
||||
import axios from "axios"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { OcaAuthService } from "@/services/auth/oca/OcaAuthService"
|
||||
import { DEFAULT_EXTERNAL_OCA_BASE_URL, DEFAULT_INTERNAL_OCA_BASE_URL } from "@/services/auth/oca/utils/constants"
|
||||
import {
|
||||
CHAT_COMPLETIONS_API,
|
||||
DEFAULT_EXTERNAL_OCA_BASE_URL,
|
||||
DEFAULT_INTERNAL_OCA_BASE_URL,
|
||||
RESPONSES_API,
|
||||
} from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
@@ -57,6 +62,11 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
|
||||
defaultModelId = modelId
|
||||
}
|
||||
const modelInfo = model.model_info
|
||||
const supportedApiList = modelInfo.supported_api_list ?? [CHAT_COMPLETIONS_API]
|
||||
const apiFormat: ApiFormat =
|
||||
supportedApiList.includes(RESPONSES_API) && !supportedApiList.includes(CHAT_COMPLETIONS_API)
|
||||
? ApiFormat.OPENAI_RESPONSES
|
||||
: ApiFormat.OPENAI_CHAT
|
||||
models[modelId] = OcaModelInfo.create({
|
||||
maxTokens: model.litellm_params?.max_tokens || -1,
|
||||
contextWindow: modelInfo.context_window,
|
||||
@@ -73,6 +83,9 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
|
||||
temperature: modelInfo.temperature || 0,
|
||||
banner: modelInfo.banner,
|
||||
modelName: modelId,
|
||||
apiFormat: apiFormat,
|
||||
supportsReasoning: modelInfo.is_reasoning_model || false,
|
||||
reasoningEffortOptions: modelInfo.reasoning_effort_options || [],
|
||||
})
|
||||
}
|
||||
console.log("OCA models fetched", models)
|
||||
@@ -91,6 +104,25 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
|
||||
? apiConfiguration.actModeOcaModelId
|
||||
: defaultModelId!
|
||||
|
||||
let planModeOcaReasoningEffort
|
||||
let actModeOcaReasoningEffort
|
||||
if (
|
||||
models[planModeSelectedModelId].supportsReasoning &&
|
||||
models[planModeSelectedModelId].reasoningEffortOptions.length > 0
|
||||
) {
|
||||
planModeOcaReasoningEffort = apiConfiguration.planModeOcaReasoningEffort
|
||||
? apiConfiguration.planModeOcaReasoningEffort
|
||||
: models[planModeSelectedModelId].reasoningEffortOptions[0]
|
||||
}
|
||||
if (
|
||||
models[actModeSelectedModelId].supportsReasoning &&
|
||||
models[actModeSelectedModelId].reasoningEffortOptions.length > 0
|
||||
) {
|
||||
actModeOcaReasoningEffort = apiConfiguration.actModeOcaReasoningEffort
|
||||
? apiConfiguration.actModeOcaReasoningEffort
|
||||
: models[actModeSelectedModelId].reasoningEffortOptions[0]
|
||||
}
|
||||
|
||||
// Build updates object based on plan/act mode setting
|
||||
const updates: Partial<GlobalStateAndSettings> = {}
|
||||
|
||||
@@ -98,15 +130,19 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
|
||||
if (currentMode === "plan") {
|
||||
updates.planModeOcaModelId = planModeSelectedModelId
|
||||
updates.planModeOcaModelInfo = models[planModeSelectedModelId]
|
||||
updates.planModeOcaReasoningEffort = planModeOcaReasoningEffort
|
||||
} else {
|
||||
updates.actModeOcaModelId = actModeSelectedModelId
|
||||
updates.actModeOcaModelInfo = models[actModeSelectedModelId]
|
||||
updates.actModeOcaReasoningEffort = actModeOcaReasoningEffort
|
||||
}
|
||||
} else {
|
||||
updates.planModeOcaModelId = planModeSelectedModelId
|
||||
updates.planModeOcaModelInfo = models[planModeSelectedModelId]
|
||||
updates.planModeOcaReasoningEffort = planModeOcaReasoningEffort
|
||||
updates.actModeOcaModelId = actModeSelectedModelId
|
||||
updates.actModeOcaModelInfo = models[actModeSelectedModelId]
|
||||
updates.actModeOcaReasoningEffort = actModeOcaReasoningEffort
|
||||
}
|
||||
|
||||
// Update state directly using batch method
|
||||
|
||||
@@ -7,6 +7,73 @@ import path from "path"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Derives thinkingConfig from model ID and tags.
|
||||
* The Vercel API only provides a "reasoning" tag to indicate support,
|
||||
* so we derive the specific configuration based on model patterns.
|
||||
*/
|
||||
function deriveThinkingConfig(modelId: string, tags?: string[]): ModelInfo["thinkingConfig"] {
|
||||
if (!tags?.includes("reasoning")) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Anthropic Claude models
|
||||
if (modelId.startsWith("anthropic/claude")) {
|
||||
return { maxBudget: 8192 }
|
||||
}
|
||||
|
||||
// Google Gemini models
|
||||
if (modelId.includes("gemini-3")) {
|
||||
return {
|
||||
maxBudget: 32767,
|
||||
supportsThinkingLevel: true,
|
||||
geminiThinkingLevel: "high",
|
||||
}
|
||||
}
|
||||
|
||||
// DeepSeek R1 models
|
||||
if (modelId.startsWith("deepseek/deepseek-r1")) {
|
||||
return { maxBudget: 8192 }
|
||||
}
|
||||
|
||||
// OpenAI o-series reasoning models
|
||||
if (modelId.startsWith("openai/o1") || modelId.startsWith("openai/o3")) {
|
||||
return { maxBudget: 32000 }
|
||||
}
|
||||
|
||||
// Qwen QwQ models (specific IDs to match OpenRouter)
|
||||
if (modelId === "qwen/qwq-32b:free" || modelId === "qwen/qwq-32b") {
|
||||
return { maxBudget: 32000 }
|
||||
}
|
||||
|
||||
// Default for other reasoning models
|
||||
return { maxBudget: 32000 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives recommended temperature for specific model types.
|
||||
* Returns undefined to use the default (0).
|
||||
*/
|
||||
function deriveTemperature(modelId: string): number | undefined {
|
||||
// DeepSeek R1 and similar reasoning models recommend 0.7
|
||||
// Use specific model IDs to match OpenRouter behavior
|
||||
if (
|
||||
modelId.startsWith("deepseek/deepseek-r1") ||
|
||||
modelId === "perplexity/sonar-reasoning" ||
|
||||
modelId === "qwen/qwq-32b:free" ||
|
||||
modelId === "qwen/qwq-32b"
|
||||
) {
|
||||
return 0.7
|
||||
}
|
||||
|
||||
// Gemini 3.0 recommends temperature 1.0
|
||||
if (modelId.startsWith("google/gemini-3.0") || modelId === "google/gemini-3.0") {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Core function: Refreshes Vercel AI Gateway models and returns application types
|
||||
* @param _controller The controller instance (unused)
|
||||
@@ -18,7 +85,7 @@ export async function refreshVercelAiGatewayModels(_controller: Controller): Pro
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
|
||||
try {
|
||||
const response = await axios.get("https://ai-gateway.vercel.sh/v1/models", getAxiosSettings())
|
||||
const response = await axios.get("https://ai-gateway.vercel.sh/v1/models?include_mappings=true", getAxiosSettings())
|
||||
|
||||
if (response.data?.data) {
|
||||
const rawModels = response.data.data
|
||||
@@ -44,6 +111,8 @@ export async function refreshVercelAiGatewayModels(_controller: Controller): Pro
|
||||
supportsImages: true, // assume all models support images since vercel ai doesn't give this info
|
||||
supportsPromptCache: !!(rawModel.pricing?.input_cache_read && rawModel.pricing?.input_cache_write),
|
||||
description: rawModel.description ?? "",
|
||||
thinkingConfig: deriveThinkingConfig(rawModel.id, rawModel.tags),
|
||||
temperature: deriveTemperature(rawModel.id),
|
||||
}
|
||||
|
||||
models[rawModel.id] = modelInfo
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
|
||||
import { buildApiHandler } from "@/core/api"
|
||||
import { ApiHandlerOptions, ApiHandlerSecrets, ApiProvider } from "@/shared/api"
|
||||
import { ApiHandlerOptions, ApiProvider } from "@/shared/api"
|
||||
import { UpdateApiConfigurationRequestNew } from "@/shared/proto/index.cline"
|
||||
import { Secrets } from "@/shared/storage/state-keys"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
/**
|
||||
@@ -69,7 +70,7 @@ export async function updateApiConfiguration(controller: Controller, request: Up
|
||||
const { options: maskOptionsFields, secrets: maskSecretsFields } = parseFieldMask(updateMask)
|
||||
|
||||
// Process secrets based on field mask
|
||||
const secrets: Partial<ApiHandlerSecrets> = {}
|
||||
const secrets: Partial<Secrets> = {}
|
||||
|
||||
if (protoSecrets && maskSecretsFields.size > 0) {
|
||||
// Validate all masked fields exist
|
||||
@@ -81,7 +82,7 @@ export async function updateApiConfiguration(controller: Controller, request: Up
|
||||
// Process entries that are in the mask
|
||||
for (const [key, value] of Object.entries(protoSecrets)) {
|
||||
if (maskSecretsFields.has(key)) {
|
||||
secrets[key as keyof ApiHandlerSecrets] = value
|
||||
secrets[key as keyof Secrets] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { BannerService } from "@/services/banner/BannerService"
|
||||
import type { StringRequest } from "@/shared/proto/cline/common"
|
||||
import { Empty } from "@/shared/proto/cline/common"
|
||||
import type { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Dismisses a banner by ID
|
||||
* Dismisses a banner and sends telemetry
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the banner ID to dismiss
|
||||
* @returns Empty response
|
||||
@@ -11,9 +12,14 @@ import type { Controller } from ".."
|
||||
export async function dismissBanner(controller: Controller, request: StringRequest): Promise<Empty> {
|
||||
const bannerId = request.value
|
||||
|
||||
if (bannerId) {
|
||||
await controller.dismissBanner(bannerId)
|
||||
if (!bannerId) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return Empty.create()
|
||||
try {
|
||||
await BannerService.get().dismissBanner(bannerId)
|
||||
await controller.postStateToWebview()
|
||||
} catch (error) {
|
||||
console.error("Failed to dismiss banner:", error)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BannerService } from "@/services/banner/BannerService"
|
||||
import { Empty } from "@/shared/proto/cline/common"
|
||||
import type { TrackBannerEventRequest } from "@/shared/proto/cline/state"
|
||||
import type { Controller } from ".."
|
||||
@@ -8,12 +9,19 @@ import type { Controller } from ".."
|
||||
* @param request The request containing banner ID and event type
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function trackBannerEvent(controller: Controller, request: TrackBannerEventRequest): Promise<Empty> {
|
||||
export async function trackBannerEvent(_controller: Controller, request: TrackBannerEventRequest): Promise<Empty> {
|
||||
const { bannerId, eventType } = request
|
||||
|
||||
if (bannerId && eventType) {
|
||||
await controller.trackBannerEvent(bannerId, eventType as "dismiss")
|
||||
if (!bannerId) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return Empty.create()
|
||||
if (eventType !== "dismiss") {
|
||||
console.error("Unsupported event type ", eventType)
|
||||
return {}
|
||||
}
|
||||
try {
|
||||
await BannerService.get().sendBannerEvent(bannerId, eventType)
|
||||
} catch (error) {
|
||||
console.error("Failed to track banner event:", error)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-
|
||||
import { OpenaiReasoningEffort } from "@shared/storage/types"
|
||||
import { TelemetrySetting } from "@shared/TelemetrySetting"
|
||||
import { ClineEnv } from "@/config"
|
||||
import { fetchRemoteConfig } from "@/core/storage/remote-config/fetch"
|
||||
import { clearRemoteConfig } from "@/core/storage/remote-config/utils"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { McpDisplayMode } from "@/shared/McpDisplayMode"
|
||||
import { ShowMessageType } from "@/shared/proto/host/window"
|
||||
@@ -363,6 +365,10 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
controller.stateManager.setGlobalState("subagentsEnabled", !!request.subagentsEnabled)
|
||||
}
|
||||
|
||||
if (request.skillsEnabled !== undefined) {
|
||||
controller.stateManager.setGlobalState("skillsEnabled", !!request.skillsEnabled)
|
||||
}
|
||||
|
||||
if (request.nativeToolCallEnabled !== undefined) {
|
||||
controller.stateManager.setGlobalState("nativeToolCallEnabled", !!request.nativeToolCallEnabled)
|
||||
if (controller.task) {
|
||||
@@ -379,6 +385,25 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
controller.stateManager.setGlobalState("enableParallelToolCalling", !!request.enableParallelToolCalling)
|
||||
}
|
||||
|
||||
if (request.optOutOfRemoteConfig !== undefined) {
|
||||
const hadOptedOut = controller.stateManager.getGlobalSettingsKey("optOutOfRemoteConfig")
|
||||
const isOptingOut = !!request.optOutOfRemoteConfig
|
||||
const isReenablingRemoteConfig = !isOptingOut && hadOptedOut
|
||||
|
||||
// Update now so any subsequent function can access the updated value
|
||||
controller.stateManager.setGlobalState("optOutOfRemoteConfig", isOptingOut)
|
||||
|
||||
if (isOptingOut && !hadOptedOut) {
|
||||
clearRemoteConfig()
|
||||
} else if (isReenablingRemoteConfig) {
|
||||
// Fire-and-forget: We don't need to await here
|
||||
// The function catches any errors and posts the updated state to the webview
|
||||
// The immediate state update below shows the user's intent (opted-in),
|
||||
// and we apply the actual config afterwards without blocking the settings update
|
||||
fetchRemoteConfig(controller)
|
||||
}
|
||||
}
|
||||
|
||||
// Post updated state to webview
|
||||
await controller.postStateToWebview()
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import { Controller } from "../index"
|
||||
|
||||
// Keep track of active didBecomeVisible subscriptions
|
||||
const activeDidBecomeVisibleSubscriptions = new Set<StreamingResponseHandler<Empty>>()
|
||||
|
||||
/**
|
||||
* Subscribe to didBecomeVisible events
|
||||
* @param controller The controller instance
|
||||
* @param request The empty request
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request (passed by the gRPC handler)
|
||||
*/
|
||||
export async function subscribeToDidBecomeVisible(
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<Empty>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
console.log(`[DEBUG] set up didBecomeVisible subscription`)
|
||||
|
||||
// Add this subscription to the active subscriptions
|
||||
activeDidBecomeVisibleSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeDidBecomeVisibleSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "didBecomeVisible_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a didBecomeVisible event to all active subscribers
|
||||
*/
|
||||
export async function sendDidBecomeVisibleEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(activeDidBecomeVisibleSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending didBecomeVisible event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
activeDidBecomeVisibleSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
// Keep track of active focus chat input subscriptions
|
||||
const focusChatInputSubscriptions = new Set<StreamingResponseHandler<Empty>>()
|
||||
|
||||
/**
|
||||
* Subscribe to focus chat input events
|
||||
* @param controller The controller instance
|
||||
* @param request The empty request
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request
|
||||
*/
|
||||
export async function subscribeToFocusChatInput(
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<Empty>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
// Add this subscription to the active subscriptions
|
||||
focusChatInputSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
focusChatInputSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "focus_chat_input_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a focus chat input event to all active subscribers
|
||||
*/
|
||||
export async function sendFocusChatInputEvent(): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(focusChatInputSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = Empty.create({})
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending focus chat input event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
focusChatInputSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { ShowWebviewEvent } from "@shared/proto/cline/ui"
|
||||
import { getRequestRegistry, StreamingResponseHandler } from "../grpc-handler"
|
||||
import type { Controller } from "../index"
|
||||
|
||||
// Keep track of active show webview subscriptions
|
||||
const showWebviewSubscriptions = new Set<StreamingResponseHandler<ShowWebviewEvent>>()
|
||||
|
||||
/**
|
||||
* Subscribe to show webview events
|
||||
* @param controller The controller instance
|
||||
* @param request The show webview request containing preserveEditorFocus flag
|
||||
* @param responseStream The streaming response handler
|
||||
* @param requestId The ID of the request
|
||||
*/
|
||||
export async function subscribeToShowWebview(
|
||||
_controller: Controller,
|
||||
_request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler<ShowWebviewEvent>,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
// Add this subscription to the active subscriptions
|
||||
showWebviewSubscriptions.add(responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
showWebviewSubscriptions.delete(responseStream)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "show_webview_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a show webview event to all active subscribers
|
||||
* @param preserveEditorFocus When true, the webview should not steal focus from the editor
|
||||
*/
|
||||
export async function sendShowWebviewEvent(preserveEditorFocus: boolean = false): Promise<void> {
|
||||
// Send the event to all active subscribers
|
||||
const promises = Array.from(showWebviewSubscriptions).map(async (responseStream) => {
|
||||
try {
|
||||
const event = ShowWebviewEvent.create({ preserveEditorFocus })
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error sending show webview event:", error)
|
||||
// Remove the subscription if there was an error
|
||||
showWebviewSubscriptions.delete(responseStream)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,292 @@
|
||||
import { ParseEntry, parse } from "shell-quote"
|
||||
import { COMMAND_PERMISSIONS_ENV_VAR, CommandPermissionConfig, PermissionValidationResult, ShellOperatorMatch } from "./types"
|
||||
|
||||
const OPERATOR_DESCRIPTIONS: Record<string, string> = {
|
||||
";": "command chaining (semicolon)",
|
||||
"&&": "command chaining (AND)",
|
||||
"||": "command chaining (OR)",
|
||||
"|": "pipe",
|
||||
">": "output redirection",
|
||||
">>": "append redirection",
|
||||
"<": "input redirection",
|
||||
">&": "file descriptor redirection",
|
||||
"<&": "file descriptor duplication",
|
||||
"|&": "pipe with stderr",
|
||||
}
|
||||
|
||||
const LINE_SEPARATOR_REGEX = /[\n\r\u2028\u2029\u0085]/
|
||||
const LINE_SEPARATOR_DESCRIPTIONS: Record<string, ShellOperatorMatch> = {
|
||||
"\n": { operator: "\\n", description: "newline (command separator)" },
|
||||
"\r": { operator: "\\r", description: "carriage return (potential command separator)" },
|
||||
"\u2028": { operator: "U+2028", description: "unicode line separator" },
|
||||
"\u2029": { operator: "U+2029", description: "unicode paragraph separator" },
|
||||
"\u0085": { operator: "U+0085", description: "unicode next line" },
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls command execution permissions based on environment variable configuration.
|
||||
* Uses glob pattern matching to allow/deny specific commands.
|
||||
*
|
||||
* Configuration is read from the CLINE_COMMAND_PERMISSIONS environment variable.
|
||||
* Format: {"allow": ["pattern1", "pattern2"], "deny": ["pattern3"]}
|
||||
*
|
||||
* Rule evaluation:
|
||||
* 1. If shell operators are detected outside quotes → DENIED (security)
|
||||
* 2. If deny rules are defined and command matches a deny pattern → DENIED
|
||||
* 3. If allow rules are defined and command matches an allow pattern → ALLOWED
|
||||
* 4. If allow rules are defined but command doesn't match any → DENIED (deny by default)
|
||||
* 5. If no rules are defined (env var not set) → ALLOWED (backward compatibility)
|
||||
*/
|
||||
export class CommandPermissionController {
|
||||
private config: CommandPermissionConfig | null = null
|
||||
|
||||
constructor() {
|
||||
this.config = this.parseConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the CLINE_COMMAND_PERMISSIONS environment variable
|
||||
* @returns Parsed configuration or null if not set or invalid
|
||||
*/
|
||||
private parseConfig(): CommandPermissionConfig | null {
|
||||
const envValue = process.env[COMMAND_PERMISSIONS_ENV_VAR]
|
||||
if (!envValue) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(envValue)
|
||||
return {
|
||||
allow: Array.isArray(parsed.allow) ? parsed.allow : undefined,
|
||||
deny: Array.isArray(parsed.deny) ? parsed.deny : undefined,
|
||||
allowOperators: Array.isArray(parsed.allowOperators) ? parsed.allowOperators : undefined,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse ${COMMAND_PERMISSIONS_ENV_VAR}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an operator is in the allowOperators list
|
||||
* @param operator - The operator to check
|
||||
* @returns true if the operator is allowed
|
||||
*/
|
||||
private isOperatorAllowed(operator: string): boolean {
|
||||
return Boolean(this.config?.allowOperators?.includes(operator))
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a command is allowed to execute based on configured permissions
|
||||
* @param command - The command string to validate
|
||||
* @returns PermissionValidationResult indicating if command is allowed and why
|
||||
*/
|
||||
validateCommand(command: string): PermissionValidationResult {
|
||||
// No config = allow everything (backward compatibility)
|
||||
if (!this.config) {
|
||||
return { allowed: true, reason: "no_config" }
|
||||
}
|
||||
|
||||
// Check for shell operators FIRST (security check)
|
||||
const shellOperator = this.detectShellOperator(command)
|
||||
if (shellOperator) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "shell_operator_detected",
|
||||
detectedOperator: shellOperator.operator,
|
||||
}
|
||||
}
|
||||
|
||||
// Check deny rules first (deny takes precedence)
|
||||
if (this.config.deny) {
|
||||
for (const pattern of this.config.deny) {
|
||||
if (this.matchesPattern(command, pattern)) {
|
||||
return { allowed: false, matchedPattern: pattern, reason: "denied" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check allow rules
|
||||
if (this.config.allow && this.config.allow.length > 0) {
|
||||
for (const pattern of this.config.allow) {
|
||||
if (this.matchesPattern(command, pattern)) {
|
||||
return { allowed: true, matchedPattern: pattern, reason: "allowed" }
|
||||
}
|
||||
}
|
||||
// Allow rules defined but no match = deny by default
|
||||
return { allowed: false, reason: "no_match_deny_default" }
|
||||
}
|
||||
|
||||
// No allow rules defined, and no deny matched = allow
|
||||
return { allowed: true, reason: "no_config" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command matches a wildcard pattern.
|
||||
*
|
||||
* Uses simple wildcard matching where `*` matches any characters (including `/` and newlines).
|
||||
* This is different from file glob matching where `*` doesn't cross directory boundaries.
|
||||
* For command permission matching, we want `*` to match any sequence of characters
|
||||
* so that patterns like `gh pr comment *` match `gh pr comment 123 --body-file /tmp/file.txt`
|
||||
* or commands with multiline arguments like `gh pr comment 123 --body "line1\nline2"`.
|
||||
*
|
||||
* Supported patterns:
|
||||
* - `*` matches any sequence of characters (including / and newlines)
|
||||
* - `?` matches exactly one character
|
||||
*
|
||||
* @param command - The command to check
|
||||
* @param pattern - The wildcard pattern to match against
|
||||
* @returns true if command matches the pattern
|
||||
*/
|
||||
private matchesPattern(command: string, pattern: string): boolean {
|
||||
const regex = new RegExp(
|
||||
"^" +
|
||||
pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape special regex chars
|
||||
.replace(/\*/g, ".*") // * becomes .*
|
||||
.replace(/\?/g, ".") + // ? becomes .
|
||||
"$",
|
||||
"s", // s flag enables dotAll (. matches newlines)
|
||||
)
|
||||
return regex.test(command)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect shell operators using shell-quote parser.
|
||||
* This prevents command chaining/injection attacks like:
|
||||
* gh pr view 123; rm -rf /
|
||||
* gh pr view 123 && malicious_command
|
||||
* gh pr view $(malicious_command)
|
||||
*
|
||||
* Operators inside quotes are allowed (they're literal characters):
|
||||
* echo "hello; world" # OK - semicolon is inside quotes
|
||||
*
|
||||
* @param command - The command string to check
|
||||
* @returns ShellOperatorMatch if an operator is found outside quotes, null otherwise
|
||||
*/
|
||||
private detectShellOperator(command: string): ShellOperatorMatch | null {
|
||||
const dangerousCharMatch = this.detectDangerousCharsOutsideQuotes(command)
|
||||
if (dangerousCharMatch) {
|
||||
return dangerousCharMatch
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the command using shell-quote
|
||||
// shell-quote returns an array where:
|
||||
// - strings are regular arguments
|
||||
// - objects with 'op' key are shell operators
|
||||
// - objects with 'comment' key are comments
|
||||
// - objects with 'pattern' key are glob patterns (we allow these)
|
||||
const parsed = parse(command, (varName: string) => `$${varName}`)
|
||||
|
||||
// Check each parsed element for operators
|
||||
for (const entry of parsed) {
|
||||
const operatorMatch = this.checkParsedEntry(entry)
|
||||
if (operatorMatch) {
|
||||
return operatorMatch
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
} catch {
|
||||
// If parsing fails, be conservative and block the command
|
||||
// This could indicate malformed shell syntax being used for injection
|
||||
return { operator: "parse_error", description: "command parsing failed (potential injection)" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect dangerous characters outside of quoted strings.
|
||||
* This includes newlines, carriage returns, unicode line separators, and backticks.
|
||||
*
|
||||
* For newlines/carriage returns: They are safe inside ANY quotes (single or double)
|
||||
* because they become literal characters in the argument value.
|
||||
*
|
||||
* For backticks: They are only safe inside SINGLE quotes because double quotes
|
||||
* still allow command substitution.
|
||||
*
|
||||
* Examples:
|
||||
* gh pr comment 123 --body "line1\nline2" -> ALLOWED (newline in quotes)
|
||||
* gh pr comment 123\nrm -rf / -> BLOCKED (newline outside quotes)
|
||||
* echo `date` -> BLOCKED (backtick outside quotes)
|
||||
* echo "hello `date`" -> BLOCKED (backtick in double quotes - executes!)
|
||||
* echo 'hello `date`' -> ALLOWED (backtick in single quotes - literal)
|
||||
*
|
||||
* @param command - The command string to check
|
||||
* @returns ShellOperatorMatch if dangerous chars found outside appropriate quotes, null otherwise
|
||||
*/
|
||||
private detectDangerousCharsOutsideQuotes(command: string): ShellOperatorMatch | null {
|
||||
let inSingleQuote = false
|
||||
let inDoubleQuote = false
|
||||
let isEscaped = false
|
||||
|
||||
for (let i = 0; i < command.length; i++) {
|
||||
const char = command[i]
|
||||
|
||||
// If previous char was an unescaped backslash, this char is escaped
|
||||
if (isEscaped) {
|
||||
isEscaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for escape sequence (only outside single quotes)
|
||||
// In single quotes, backslashes are literal
|
||||
if (char === "\\" && !inSingleQuote) {
|
||||
isEscaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle double quotes - we track them to know when single quotes are literal
|
||||
if (char === '"' && !inSingleQuote) {
|
||||
inDoubleQuote = !inDoubleQuote
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle single quotes - only toggle when NOT inside double quotes
|
||||
// Inside double quotes, single quotes are literal characters
|
||||
if (char === "'" && !inDoubleQuote) {
|
||||
inSingleQuote = !inSingleQuote
|
||||
continue
|
||||
}
|
||||
|
||||
const inAnyQuote = inSingleQuote || inDoubleQuote
|
||||
|
||||
// Check for newlines and carriage returns outside ANY quotes
|
||||
// These are command separators when outside quotes
|
||||
if (!inAnyQuote && LINE_SEPARATOR_REGEX.test(char)) {
|
||||
return LINE_SEPARATOR_DESCRIPTIONS[char]
|
||||
}
|
||||
|
||||
// Check for backticks outside SINGLE quotes only
|
||||
// Backticks in double quotes ARE executed as command substitution in bash
|
||||
if (char === "`" && !inSingleQuote) {
|
||||
return { operator: "`", description: "command substitution (backtick)" }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a parsed entry from shell-quote for dangerous operators.
|
||||
*
|
||||
* @param entry - A parsed entry from shell-quote
|
||||
* @returns ShellOperatorMatch if dangerous operator found, null otherwise
|
||||
*/
|
||||
private checkParsedEntry(entry: ParseEntry): ShellOperatorMatch | null {
|
||||
// null entries, string entries, glob patterns, and comments are safe
|
||||
if (!entry || typeof entry === "string" || "pattern" in entry || "comment" in entry) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof entry.op === "string") {
|
||||
if (this.isOperatorAllowed(entry.op)) {
|
||||
return null
|
||||
}
|
||||
const description = OPERATOR_DESCRIPTIONS[entry.op] || `shell operator (${entry.op})`
|
||||
return { operator: entry.op, description }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { CommandPermissionController } from "./CommandPermissionController"
|
||||
export type { CommandPermissionConfig, PermissionValidationResult } from "./types"
|
||||
export { COMMAND_PERMISSIONS_ENV_VAR } from "./types"
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Configuration structure for command permissions from environment variable
|
||||
*/
|
||||
export interface CommandPermissionConfig {
|
||||
allow?: string[] // Glob patterns for allowed commands
|
||||
deny?: string[] // Glob patterns for denied commands
|
||||
allowOperators?: string[] // Shell operators to allow (e.g., [">", ">>"] to allow file writing)
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a permission validation check
|
||||
*/
|
||||
export interface PermissionValidationResult {
|
||||
allowed: boolean
|
||||
matchedPattern?: string // The pattern that matched (for error messages)
|
||||
reason: "no_config" | "allowed" | "denied" | "no_match_deny_default" | "shell_operator_detected"
|
||||
detectedOperator?: string // The shell operator that was detected (for error messages)
|
||||
}
|
||||
|
||||
/**
|
||||
* Environment variable name for command permissions
|
||||
*/
|
||||
export const COMMAND_PERMISSIONS_ENV_VAR = "CLINE_COMMAND_PERMISSIONS"
|
||||
|
||||
/**
|
||||
* Shell operators that indicate command chaining, piping, substitution, or redirection.
|
||||
* These are security-sensitive because they can be used to bypass command restrictions.
|
||||
*/
|
||||
export interface ShellOperatorMatch {
|
||||
operator: string
|
||||
description: string
|
||||
}
|
||||
@@ -25,6 +25,9 @@ export const formatResponse = {
|
||||
clineIgnoreError: (path: string) =>
|
||||
`Access to ${path} is blocked by the .clineignore file settings. You must try to continue in the task without using this file, or ask the user to update the .clineignore file.`,
|
||||
|
||||
permissionDeniedError: (reason: string) =>
|
||||
`Command execution blocked by CLINE_COMMAND_PERMISSIONS: ${reason}. You must try a different approach or ask the user to update the permission settings.`,
|
||||
|
||||
noToolsUsed: (usingNativeToolCalls: boolean) =>
|
||||
usingNativeToolCalls
|
||||
? "[ERROR] You did not use a tool in your previous response! Please retry with a tool use."
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ CAPABILITIES
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the replace_in_file tool to implement changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
@@ -618,6 +618,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
@@ -582,6 +582,7 @@ CAPABILITIES
|
||||
- You can use the list_code_definition_names tool to get an overview of source code definitions for all files at the top level of a specified directory. This can be particularly useful when you need to understand the broader context and relationships between certain parts of the code. You may need to call this tool multiple times to understand various parts of the codebase related to the task.
|
||||
- For example, when asked to make edits or improvements you might analyze the file structure in the initial environment_details to get an overview of the project, then use list_code_definition_names to get further insight using source code definitions for files located in relevant directories, then read_file to examine the contents of relevant files, analyze the code and suggest improvements or make necessary edits, then use the replace_in_file tool to implement changes. If you refactored code that could affect other parts of the codebase, you could use search_files to ensure you update other files as needed.
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
+1
@@ -542,6 +542,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
@@ -598,6 +598,7 @@ CAPABILITIES
|
||||
- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Prefer non-interactive commands when possible: use flags to disable pagers (e.g., '--no-pager'), auto-confirm prompts (e.g., '-y' when safe), provide input via flags/arguments rather than stdin, suppress interactive behavior, etc. For long-running commands, the user may keep them running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
|
||||
- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
|
||||
- For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser.
|
||||
- When the task requires or could benefit from getting up to date information on a topic (e.g. latest best practices, latest documentation, latest news, etc.), use the web_search tool to find current results, then use the web_fetch tool to retrieve and analyze the content from relevant URLs.
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
|
||||
====
|
||||
|
||||
+2
-2
@@ -265,7 +265,7 @@
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_fetch",
|
||||
"description": "Fetches and analyzes content from a specified URL.",
|
||||
"description": "Fetches and analyzes content from a specified URL. IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
@@ -295,7 +295,7 @@
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"description": "Performs a web search and returns relevant results with titles and URLs.",
|
||||
"description": "Performs a web search and returns relevant results with titles and URLs. IMPORTANT: If an MCP-provided web search tool is available, prefer using that tool instead of this one, as it may have fewer restrictions.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
|
||||
+1
-1
@@ -338,7 +338,7 @@
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "act_mode_respond",
|
||||
"description": "Provide a progress update or preamble to the user during ACT MODE execution. This tool allows you to communicate your thought process and planned actions without interrupting the execution flow. After displaying your message, execution automatically continues, allowing you to proceed with subsequent tool calls immediately. This tool is only available in ACT MODE. This tool may not be called immediately after a previous act_mode_respond call.\n\nIMPORTANT: Use this tool frequently to create a better user experience. Since it's non-blocking, there's no performance penalty for frequent use.\n\nUse this tool when:\n- After reading files and before making any edits - explain your analysis and what changes you plan to make\n- When starting a new phase of work (e.g., transitioning from backend to frontend, or from one feature to another)\n- During long sequences of operations to provide progress updates\n- When your approach or strategy changes mid-task\n- Before executing complex or potentially risky operations\n- To explain why you're choosing one approach over another\n\nDo NOT use this tool when you have completed all required actions and are ready to present the final output; in that case, use the attempt_completion tool instead.\n\nCRITICAL CONSTRAINT: You MUST NOT call this tool more than once in a row. After using act_mode_respond, your next assistant message MUST either call a different tool or perform additional work without using act_mode_respond again. If you attempt to call act_mode_respond consecutively, the tool call will fail with an explicit error.",
|
||||
"description": "Provide a progress update or preamble to the user during ACT MODE execution. This tool allows you to communicate your thought process and planned actions without interrupting the execution flow. After displaying your message, execution automatically continues, allowing you to proceed with subsequent tool calls immediately. This tool is only available in ACT MODE. This tool may not be called immediately after a previous act_mode_respond call.\n\nIMPORTANT: Use this tool when it adds value to the user experience, but always follow it with an actual tool call - never call it twice in a row.\n\nUse this tool when:\n- After reading files and before making any edits - explain your analysis and what changes you plan to make\n- When starting a new phase of work (e.g., transitioning from backend to frontend, or from one feature to another)\n- During long sequences of operations to provide progress updates\n- When your approach or strategy changes mid-task\n- Before executing complex or potentially risky operations\n- To explain why you're choosing one approach over another\n\nDo NOT use this tool when you have completed all required actions and are ready to present the final output; in that case, use the attempt_completion tool instead.\n\nCRITICAL CONSTRAINT: You MUST NOT call this tool more than once in a row. After using act_mode_respond, your next assistant message MUST either call a different tool or perform additional work without using act_mode_respond again. If you attempt to call act_mode_respond consecutively, the tool call will fail with an explicit error.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user