mirror of
https://github.com/cline/cline.git
synced 2026-09-11 16:42:40 +08:00
Compare 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 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add zai-glm-4.7 to Cerebras model list
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Adding support for responses api to OCA provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add bash command permission system to cline
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add create-pull-request skill
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Revert #8341 (0d04205dc) due to regressions in diff view/document truncation (see #8423, #8429).
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat(vercel-ai-gateway): add model refresh and improve reasoning support
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix the selection of remotely configured providers
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
show cline command permission denials in the CLI
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Harden act_mode_respond to prevent consecutive calls
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Reduce the number of network requests for the users profile
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -75,21 +75,36 @@ jobs:
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- name: Read nightly version
|
||||
- name: Generate nightly version with timestamp
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
id: version
|
||||
run: |
|
||||
# Read version from cli/package-nightly.json
|
||||
VERSION=$(node -p "require('./cli/package-nightly.json').version")
|
||||
echo "Nightly version: $VERSION"
|
||||
# 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: Setup cli/package.json for build
|
||||
- name: Update cli/package.json with nightly version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
# Copy nightly package.json to cli/package.json for build
|
||||
cp cli/package-nightly.json cli/package.json
|
||||
# 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'
|
||||
|
||||
@@ -41,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,68 +0,0 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.8-nightly.29",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "cline-core.js",
|
||||
"bin": {
|
||||
"cline": "./bin/cline",
|
||||
"cline-host": "./bin/cline-host"
|
||||
},
|
||||
"man": "./man/cline.1",
|
||||
"scripts": {
|
||||
"postinstall": "node postinstall.js"
|
||||
},
|
||||
"bundleDependencies": [
|
||||
"@grpc/grpc-js",
|
||||
"@grpc/reflection",
|
||||
"better-sqlite3",
|
||||
"grpc-health-check",
|
||||
"open",
|
||||
"vscode-uri"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
"claude",
|
||||
"dev",
|
||||
"mcp",
|
||||
"openrouter",
|
||||
"coding",
|
||||
"agent",
|
||||
"autonomous",
|
||||
"chatgpt",
|
||||
"sonnet",
|
||||
"ai",
|
||||
"llama",
|
||||
"cli"
|
||||
],
|
||||
"author": {
|
||||
"name": "Cline Bot Inc."
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cline/cline"
|
||||
},
|
||||
"homepage": "https://cline.bot",
|
||||
"bugs": {
|
||||
"url": "https://github.com/cline/cline/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.13.3",
|
||||
"@grpc/reflection": "^1.0.4",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"grpc-health-check": "^2.0.2",
|
||||
"open": "^10.1.2",
|
||||
"vscode-uri": "^3.1.0"
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux"
|
||||
],
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.8",
|
||||
"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 {
|
||||
|
||||
+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.
|
||||
|
||||
Generated
+64
-38
@@ -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",
|
||||
@@ -112,6 +112,7 @@
|
||||
"@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",
|
||||
@@ -1181,7 +1182,6 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -2644,7 +2644,6 @@
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.9.15",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.8",
|
||||
"@types/node": ">=12.12.47"
|
||||
@@ -3228,7 +3227,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
|
||||
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -3297,7 +3295,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -4913,7 +4910,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4926,7 +4924,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4939,7 +4938,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4952,7 +4952,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4965,7 +4966,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4978,7 +4980,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -4991,7 +4994,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -5004,7 +5008,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5017,7 +5022,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5030,7 +5036,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5043,7 +5050,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5056,7 +5064,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5069,7 +5078,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5082,7 +5092,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5095,7 +5106,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5108,7 +5120,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5121,7 +5134,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5134,7 +5148,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5147,7 +5162,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5160,7 +5176,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5173,7 +5190,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5186,7 +5204,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@sap-ai-sdk/ai-api": {
|
||||
"version": "2.1.0",
|
||||
@@ -6749,7 +6768,8 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/get-folder-size": {
|
||||
"version": "3.0.4",
|
||||
@@ -6764,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,
|
||||
@@ -6774,7 +6801,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
|
||||
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -7478,7 +7504,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8245,7 +8270,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -9459,8 +9483,7 @@
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1342118",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.2.0",
|
||||
@@ -12436,7 +12459,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
@@ -12670,7 +12692,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
@@ -15551,6 +15572,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -15571,6 +15593,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -16238,6 +16261,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
|
||||
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -17663,6 +17687,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
@@ -17679,6 +17704,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -18042,7 +18068,6 @@
|
||||
"version": "5.5.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -18302,6 +18327,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -18376,6 +18402,7 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -19071,7 +19098,6 @@
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
+3
-2
@@ -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"
|
||||
@@ -349,7 +349,7 @@
|
||||
"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",
|
||||
@@ -416,6 +416,7 @@
|
||||
"@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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -372,6 +374,8 @@ message UpdateSettingsRequest {
|
||||
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 {
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -241,7 +241,7 @@ export class OcaHandler implements ApiHandler {
|
||||
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) {
|
||||
@@ -303,7 +303,6 @@ export class OcaHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
async *createMessageResponsesApi(systemPrompt: string, messages: ClineStorageMessage[], tools?: OpenAITool[]): ApiStream {
|
||||
console.log("Uses Responses API")
|
||||
const client = this.ensureClient()
|
||||
|
||||
// Convert messages to Responses API input format
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -71,7 +71,7 @@ export class VercelAIGatewayHandler 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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -180,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 }
|
||||
@@ -212,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 } }
|
||||
: {}),
|
||||
})
|
||||
|
||||
@@ -139,7 +139,7 @@ export async function createVercelAIGatewayStream(
|
||||
...(model.id.startsWith("openai/o") ? { reasoning_effort: reasoningEffort || "medium" } : {}),
|
||||
...(reasoning ? { reasoning } : {}),
|
||||
...getOpenAIToolParams(tools),
|
||||
...(model.id.includes("gemini") && geminiThinkingLevel
|
||||
...(model.id.includes("gemini-3") && geminiThinkingLevel
|
||||
? { thinking_config: { thinking_level: geminiThinkingLevel, include_thoughts: true } }
|
||||
: {}),
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -63,10 +63,10 @@ export async function refreshOcaModels(controller: Controller, request: StringRe
|
||||
}
|
||||
const modelInfo = model.model_info
|
||||
const supportedApiList = modelInfo.supported_api_list ?? [CHAT_COMPLETIONS_API]
|
||||
const apiFormat: ApiFormat = supportedApiList.includes(RESPONSES_API)
|
||||
? ApiFormat.OPENAI_RESPONSES
|
||||
: ApiFormat.OPENAI_CHAT
|
||||
console.log(modelId, supportedApiList)
|
||||
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,
|
||||
|
||||
@@ -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
-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",
|
||||
|
||||
@@ -296,7 +296,7 @@
|
||||
},
|
||||
{
|
||||
"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.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getFeedbackSection } from "./feedback"
|
||||
import { getMcp } from "./mcp"
|
||||
import { getObjectiveSection } from "./objective"
|
||||
import { getRulesSection } from "./rules"
|
||||
import { getSkillsSection } from "./skills"
|
||||
import { getSystemInfo } from "./system_info"
|
||||
import { getUpdatingTaskProgress } from "./task_progress"
|
||||
import { getToolUseSection } from "./tool_use"
|
||||
@@ -36,6 +37,7 @@ export function getSystemPromptComponents() {
|
||||
id: SystemPromptSection.CAPABILITIES,
|
||||
fn: getCapabilitiesSection,
|
||||
},
|
||||
{ id: SystemPromptSection.SKILLS, fn: getSkillsSection },
|
||||
{ id: SystemPromptSection.RULES, fn: getRulesSection },
|
||||
{ id: SystemPromptSection.OBJECTIVE, fn: getObjectiveSection },
|
||||
{
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { PromptVariant, SystemPromptContext } from "../types"
|
||||
|
||||
/**
|
||||
* Generate the skills section for the system prompt.
|
||||
*/
|
||||
export async function getSkillsSection(_variant: PromptVariant, context: SystemPromptContext): Promise<string | undefined> {
|
||||
const skills = context.skills
|
||||
if (!skills || skills.length === 0) return undefined
|
||||
|
||||
const skillsList = skills.map((skill) => ` - "${skill.name}": ${skill.description}`).join("\n")
|
||||
|
||||
return `SKILLS
|
||||
|
||||
The following skills provide specialized instructions for specific tasks. When a user's request matches a skill description, use the use_skill tool to load and activate the skill.
|
||||
|
||||
Available skills:
|
||||
${skillsList}
|
||||
|
||||
To use a skill:
|
||||
1. Match the user's request to a skill based on its description
|
||||
2. Call use_skill with the skill_name parameter set to the exact skill name
|
||||
3. Follow the instructions returned by the tool`
|
||||
}
|
||||
@@ -8,6 +8,7 @@ export enum SystemPromptSection {
|
||||
CLI_SUBAGENTS = "CLI_SUBAGENTS_SECTION",
|
||||
TODO = "TODO_SECTION",
|
||||
CAPABILITIES = "CAPABILITIES_SECTION",
|
||||
SKILLS = "SKILLS_SECTION",
|
||||
RULES = "RULES_SECTION",
|
||||
SYSTEM_INFO = "SYSTEM_INFO_SECTION",
|
||||
OBJECTIVE = "OBJECTIVE_SECTION",
|
||||
|
||||
@@ -28,7 +28,7 @@ const NATIVE_GPT_5: ClineToolSpec = {
|
||||
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.
|
||||
|
||||
IMPORTANT: Use this tool frequently to create a better user experience. Since it's non-blocking, there's no performance penalty for frequent use.
|
||||
IMPORTANT: 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.
|
||||
|
||||
Use this tool when:
|
||||
- After reading files and before making any edits - explain your analysis and what changes you plan to make
|
||||
|
||||
@@ -16,6 +16,7 @@ export * from "./read_file"
|
||||
export * from "./replace_in_file"
|
||||
export * from "./search_files"
|
||||
export * from "./use_mcp_tool"
|
||||
export * from "./use_skill"
|
||||
export * from "./web_fetch"
|
||||
export * from "./web_search"
|
||||
export * from "./write_to_file"
|
||||
|
||||
@@ -18,6 +18,7 @@ import { read_file_variants } from "./read_file"
|
||||
import { replace_in_file_variants } from "./replace_in_file"
|
||||
import { search_files_variants } from "./search_files"
|
||||
import { use_mcp_tool_variants } from "./use_mcp_tool"
|
||||
import { use_skill_variants } from "./use_skill"
|
||||
import { web_fetch_variants } from "./web_fetch"
|
||||
import { web_search_variants } from "./web_search"
|
||||
import { write_to_file_variants } from "./write_to_file"
|
||||
@@ -47,6 +48,7 @@ export function registerClineToolSets(): void {
|
||||
...replace_in_file_variants,
|
||||
...search_files_variants,
|
||||
...use_mcp_tool_variants,
|
||||
...use_skill_variants,
|
||||
...web_fetch_variants,
|
||||
...web_search_variants,
|
||||
...write_to_file_variants,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ModelFamily } from "@/shared/prompts"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ClineToolSpec } from "../spec"
|
||||
|
||||
const id = ClineDefaultTool.USE_SKILL
|
||||
|
||||
const generic: ClineToolSpec = {
|
||||
id,
|
||||
variant: ModelFamily.GENERIC,
|
||||
name: "use_skill",
|
||||
description:
|
||||
"Load and activate a skill by name. Skills provide specialized instructions for specific tasks. Use this tool ONCE when a user's request matches one of the available skill descriptions shown in the SKILLS section of your system prompt. After activation, follow the skill's instructions directly - do not call use_skill again.",
|
||||
contextRequirements: (context) => context.skills !== undefined && context.skills.length > 0,
|
||||
parameters: [
|
||||
{
|
||||
name: "skill_name",
|
||||
required: true,
|
||||
instruction: "The name of the skill to activate (must match exactly one of the available skill names)",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const use_skill_variants = [generic]
|
||||
@@ -7,6 +7,7 @@ import type { McpHub } from "@/services/mcp/McpHub"
|
||||
import type { BrowserSettings } from "@/shared/BrowserSettings"
|
||||
import type { FocusChainSettings } from "@/shared/FocusChainSettings"
|
||||
import { ModelFamily } from "@/shared/prompts"
|
||||
import type { SkillMetadata } from "@/shared/skills"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ClineToolSpec } from "./spec"
|
||||
import { SystemPromptSection } from "./templates/placeholders"
|
||||
@@ -96,6 +97,7 @@ export interface SystemPromptContext {
|
||||
readonly ide: string
|
||||
readonly supportsBrowserUse?: boolean
|
||||
readonly mcpHub?: McpHub
|
||||
readonly skills?: SkillMetadata[]
|
||||
readonly focusChainSettings?: FocusChainSettings
|
||||
readonly globalClineRulesFileInstructions?: string
|
||||
readonly localClineRulesFileInstructions?: string
|
||||
|
||||
@@ -32,6 +32,7 @@ export const config = createVariant(ModelFamily.DEVSTRAL)
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
SystemPromptSection.OBJECTIVE,
|
||||
SystemPromptSection.USER_INSTRUCTIONS,
|
||||
SystemPromptSection.SKILLS,
|
||||
)
|
||||
.tools(
|
||||
ClineDefaultTool.BASH,
|
||||
@@ -51,6 +52,7 @@ export const config = createVariant(ModelFamily.DEVSTRAL)
|
||||
ClineDefaultTool.PLAN_MODE,
|
||||
ClineDefaultTool.MCP_DOCS,
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: "devstral",
|
||||
|
||||
@@ -30,6 +30,10 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.SKILLS}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.FEEDBACK}}}
|
||||
|
||||
====
|
||||
|
||||
@@ -38,11 +38,11 @@ export const config = createVariant(ModelFamily.GEMINI_3)
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
SystemPromptSection.TODO,
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.TASK_PROGRESS,
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
SystemPromptSection.OBJECTIVE,
|
||||
SystemPromptSection.USER_INSTRUCTIONS,
|
||||
SystemPromptSection.SKILLS,
|
||||
)
|
||||
.tools(
|
||||
ClineDefaultTool.BASH,
|
||||
@@ -64,6 +64,7 @@ export const config = createVariant(ModelFamily.GEMINI_3)
|
||||
ClineDefaultTool.MCP_DOCS,
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.GEMINI_3,
|
||||
|
||||
@@ -22,6 +22,10 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.SKILLS}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.EDITING_FILES}}}
|
||||
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export const config = createVariant(ModelFamily.GENERIC)
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
SystemPromptSection.OBJECTIVE,
|
||||
SystemPromptSection.USER_INSTRUCTIONS,
|
||||
SystemPromptSection.SKILLS,
|
||||
)
|
||||
.tools(
|
||||
ClineDefaultTool.BASH,
|
||||
@@ -63,6 +64,7 @@ export const config = createVariant(ModelFamily.GENERIC)
|
||||
ClineDefaultTool.MCP_DOCS,
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: "generic",
|
||||
|
||||
@@ -30,6 +30,10 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.SKILLS}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.FEEDBACK}}}
|
||||
|
||||
====
|
||||
|
||||
@@ -33,6 +33,7 @@ export const config = createVariant(ModelFamily.GLM)
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
SystemPromptSection.OBJECTIVE,
|
||||
SystemPromptSection.USER_INSTRUCTIONS,
|
||||
SystemPromptSection.SKILLS,
|
||||
)
|
||||
.tools(
|
||||
ClineDefaultTool.BASH,
|
||||
@@ -51,6 +52,7 @@ export const config = createVariant(ModelFamily.GLM)
|
||||
ClineDefaultTool.MCP_DOCS,
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.GLM,
|
||||
|
||||
@@ -14,6 +14,8 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
## {{${SystemPromptSection.CAPABILITIES}}}
|
||||
|
||||
## {{${SystemPromptSection.SKILLS}}}
|
||||
|
||||
## {{${SystemPromptSection.EDITING_FILES}}}
|
||||
|
||||
## {{${SystemPromptSection.TODO}}}
|
||||
|
||||
@@ -42,6 +42,7 @@ export const config = createVariant(ModelFamily.GPT_5)
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
SystemPromptSection.OBJECTIVE,
|
||||
SystemPromptSection.USER_INSTRUCTIONS,
|
||||
SystemPromptSection.SKILLS,
|
||||
)
|
||||
.tools(
|
||||
ClineDefaultTool.BASH,
|
||||
@@ -62,6 +63,7 @@ export const config = createVariant(ModelFamily.GPT_5)
|
||||
ClineDefaultTool.MCP_DOCS,
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.GPT_5,
|
||||
|
||||
@@ -35,6 +35,10 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.SKILLS}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.FEEDBACK}}}
|
||||
|
||||
====
|
||||
|
||||
@@ -34,6 +34,7 @@ export const config = createVariant(ModelFamily.HERMES)
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
SystemPromptSection.OBJECTIVE,
|
||||
SystemPromptSection.USER_INSTRUCTIONS,
|
||||
SystemPromptSection.SKILLS,
|
||||
)
|
||||
.tools(
|
||||
ClineDefaultTool.BASH,
|
||||
@@ -53,6 +54,7 @@ export const config = createVariant(ModelFamily.HERMES)
|
||||
ClineDefaultTool.MCP_DOCS,
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: "hermes",
|
||||
|
||||
@@ -12,6 +12,8 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
## {{${SystemPromptSection.CAPABILITIES}}}
|
||||
|
||||
## {{${SystemPromptSection.SKILLS}}}
|
||||
|
||||
## {{${SystemPromptSection.EDITING_FILES}}}
|
||||
|
||||
## {{${SystemPromptSection.TODO}}}
|
||||
|
||||
@@ -48,6 +48,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
SystemPromptSection.OBJECTIVE,
|
||||
SystemPromptSection.USER_INSTRUCTIONS,
|
||||
SystemPromptSection.SKILLS,
|
||||
)
|
||||
.tools(
|
||||
ClineDefaultTool.BASH,
|
||||
@@ -69,6 +70,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
|
||||
ClineDefaultTool.MCP_DOCS,
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.NATIVE_GPT_5_1,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user