mirror of
https://github.com/cline/cline.git
synced 2026-09-02 07:42:19 +08:00
Compare commits
135 Commits
pr-8264
...
cli-windows
| Author | SHA1 | Date | |
|---|---|---|---|
| 570003eaa6 | |||
| d4733706d8 | |||
| a727d5c347 | |||
| ae306adc42 | |||
| 2f3002d215 | |||
| 41404e1a13 | |||
| d6244efda1 | |||
| be410c2927 | |||
| 6c67a0c8fc | |||
| 31e3c082ec | |||
| a75f4a63c8 | |||
| 0b059cf36f | |||
| 5d7f0f04d3 | |||
| 13cf28d8f4 | |||
| 6238fab366 | |||
| 7c26a7d16b | |||
| 7885c75a4f | |||
| 8994d125be | |||
| 79d88f7708 | |||
| 4d092bfba6 | |||
| dc5c6f916b | |||
| 50332ec46a | |||
| 5ccd062839 | |||
| ae9eceb113 | |||
| dd35448a9d | |||
| 32aa16612d | |||
| bffe5c4d2a | |||
| b18d7012aa | |||
| e0965821fc | |||
| 9e46e9fd22 | |||
| 890a1f7ac8 | |||
| b89a73c193 | |||
| 51927dba33 | |||
| 7634f22104 | |||
| fbf784f78b | |||
| 62bf50a659 | |||
| d850fbc0ad | |||
| 4dd6c6dcc7 | |||
| 8813f8252c | |||
| 3210c4bc4b | |||
| 9f3daa4151 | |||
| ac2db41815 | |||
| df1d33c751 | |||
| 361494d18f | |||
| dca0a8fa3e | |||
| 6d7213dc6a | |||
| 97a35d3868 | |||
| 4ec9155c46 | |||
| bffca989a1 | |||
| 0133b5d030 | |||
| a94c4be438 | |||
| d70792e539 | |||
| e4ddaac627 | |||
| 9478b600aa | |||
| d194e47bf6 | |||
| c9ff9cf1d5 | |||
| d7fa6b33c1 | |||
| 8f521e7ea3 | |||
| 3cb8d0fbcf | |||
| 5f2bf6329f | |||
| 963abc190e | |||
| 631a7d6566 | |||
| e43ab0ea7a | |||
| 242e3321a2 | |||
| ea6cb4b29e | |||
| 7699c52abf | |||
| 24281d98e1 | |||
| 0af7d0911d | |||
| 83af73a5f9 | |||
| 9d875e6222 | |||
| 125e2ad01f | |||
| 2c75285566 | |||
| 8279f2e145 | |||
| 5b94ba3ef9 | |||
| 703146182a | |||
| 9603643b77 | |||
| c6dce7fb17 | |||
| cc0d4ae6cb | |||
| a9365e30e9 | |||
| efe468d9b1 | |||
| 1b6202604d | |||
| ea1dbd8bea | |||
| e43517519e | |||
| 520d08c5f2 | |||
| 31a55ac87e | |||
| a442983742 | |||
| 7b71eff294 | |||
| 6d1890f8bb | |||
| 11d17fc17e | |||
| 42a3dc6150 | |||
| 4032e51e8d | |||
| 1bbc90487c | |||
| d422ebbb27 | |||
| 7470d234ef | |||
| 21b81f1844 | |||
| 362429a317 | |||
| 09cb9ac9ac | |||
| 94160faeef | |||
| f526f70e3a | |||
| d9b47378c6 | |||
| 0671c59e6d | |||
| bf87887501 | |||
| 66a81a6efa | |||
| eee64c5204 | |||
| 748ba99c1c | |||
| 82b1a01644 | |||
| 1c6307e8ad | |||
| 58d9c0af18 | |||
| bf213c24ea | |||
| 80cceaa3ae | |||
| 359e088eb6 | |||
| 47a464defe | |||
| 38f619cfd9 | |||
| 050773ac31 | |||
| 6d67ff0b94 | |||
| 18f4ef8b49 | |||
| 085e69d142 | |||
| 46aa66ed9d | |||
| 2ebbe954d9 | |||
| 067f5eea09 | |||
| 2a48bad28c | |||
| fd46c97825 | |||
| c6f4584f7d | |||
| a17b31070f | |||
| cad82d518d | |||
| b4d7ec187f | |||
| 42af8414e4 | |||
| 8f6b9e8362 | |||
| f1430359db | |||
| 932695f70b | |||
| 489ee936c2 | |||
| dff7f61175 | |||
| 7426513245 | |||
| f49a856ee4 | |||
| 24568fed0d |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add zai-glm-4.7 to Cerebras model list
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add claude 4.5 haiku
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Adding support for responses api to OCA provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Expose --version in cline cli command
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Log Persistence errors to PostHog
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix two bugs in DiffViewProvider file editing:
|
||||
|
||||
1. **Line boundary validation**: Add `safelyTruncateDocument()` to prevent out-of-bounds line errors on JetBrains hosts (fixes #8423, #8429). The gRPC protocol strictly validates line numbers, causing "truncateDocument INTERNAL: Wrong line" errors when `truncateDocument()` was called with a line number >= document line count.
|
||||
|
||||
2. **Content concatenation on final update**: When replacing content without a trailing newline, the old content at line N+1 was concatenated to the new content. Fixed by extending the replacement range to cover the entire document on final update.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
OpenAI GPT-5 Codex models are now using Apply Patch tool for diff edits.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix issue where tool call ids are invalid when switching between models using the chat completion format and the responses api format.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add bash command permission system to cline
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: add chat output on skill use
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding telemetry for background exec terminal
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: removes retry message from UI after retry succeeds
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add claude 4.5 opus into sap provider.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Limite Vertex and LiteLLM options when they're remote configured
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix crash when the Context Menu has a type but no options
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Throttle the remote config fetch
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Improve history view filter menu
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Add git worktree management UI for running parallel Cline sessions
|
||||
@@ -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
|
||||
@@ -0,0 +1,193 @@
|
||||
This file is the secret sauce for working effectively in this codebase. It captures tribal knowledge—the nuanced, non-obvious patterns that make the difference between a quick fix and hours of back-and-forth & human intervention.
|
||||
|
||||
**When to add to this file:**
|
||||
- User had to intervene, correct, or hand-hold
|
||||
- Multiple back-and-forth attempts were needed to get something working
|
||||
- You discovered something that required reading many files to understand
|
||||
- A change touched files you wouldn't have guessed
|
||||
- Something worked differently than you expected
|
||||
- User explicitly asks to "add this to CLAUDE.md"
|
||||
|
||||
**Proactively suggest additions** when any of the above happen—don't wait to be asked.
|
||||
|
||||
**What NOT to add:** Stuff you can figure out from reading a few files, obvious patterns, or standard practices. This file should be high-signal, not comprehensive.
|
||||
|
||||
## Miscellaneous
|
||||
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
|
||||
- When creating PRs, if the change is user-facing and significant enough to warrant a changelog entry, run `npm run changeset` and create a patch changeset. Never create minor or major version bumps. Skip changesets for trivial fixes, internal refactors, or minor UI tweaks that users wouldn't notice.
|
||||
- When adding new feature flags, see this PR as a reference https://github.com/cline/cline/pull/7566
|
||||
|
||||
## gRPC/Protobuf Communication
|
||||
The extension and webview communicate via gRPC-like protocol over VS Code message passing.
|
||||
|
||||
**Proto files live in `proto/`** (e.g., `proto/cline/task.proto`, `proto/cline/ui.proto`)
|
||||
- Each feature domain has its own `.proto` file
|
||||
- For simple data, use shared types in `proto/cline/common.proto` (`StringRequest`, `Empty`, `Int64Request`)
|
||||
- For complex data, define custom messages in the feature's `.proto` file
|
||||
- Naming: Services `PascalCaseService`, RPCs `camelCase`, Messages `PascalCase`
|
||||
- For streaming responses, use `stream` keyword (see `subscribeToAuthCallback` in `account.proto`)
|
||||
|
||||
**Run `npm run protos`** after any proto changes—generates types in:
|
||||
- `src/shared/proto/` - Shared type definitions
|
||||
- `src/generated/grpc-js/` - Service implementations
|
||||
- `src/generated/nice-grpc/` - Promise-based clients
|
||||
- `src/generated/hosts/` - Generated handlers
|
||||
|
||||
**Adding new enum values** (like a new `ClineSay` type) requires updating conversion mappings in `src/shared/proto-conversions/cline-message.ts`
|
||||
|
||||
**Adding new RPC methods** requires:
|
||||
- Handler in `src/core/controller/<domain>/`
|
||||
- Call from webview via generated client: `UiServiceClient.scrollToSettings(StringRequest.create({ value: "browser" }))`
|
||||
|
||||
**Example—the `explain-changes` feature touched:**
|
||||
- `proto/cline/task.proto` - Added `ExplainChangesRequest` message and `explainChanges` RPC
|
||||
- `proto/cline/ui.proto` - Added `GENERATE_EXPLANATION = 29` to `ClineSay` enum
|
||||
- `src/shared/ExtensionMessage.ts` - Added `ClineSayGenerateExplanation` type
|
||||
- `src/shared/proto-conversions/cline-message.ts` - Added mapping for new say type
|
||||
- `src/core/controller/task/explainChanges.ts` - Handler implementation
|
||||
- `webview-ui/src/components/chat/ChatRow.tsx` - UI rendering
|
||||
|
||||
## Adding a New API Provider
|
||||
When adding a new provider (e.g., "openai-codex"), you must update the proto conversion layer in THREE places or the provider will silently reset to Anthropic:
|
||||
|
||||
1. `proto/cline/models.proto` - Add to the `ApiProvider` enum (e.g., `OPENAI_CODEX = 40;`)
|
||||
2. `convertApiProviderToProto()` in `src/shared/proto-conversions/models/api-configuration-conversion.ts` - Add case mapping string to proto enum
|
||||
3. `convertProtoToApiProvider()` in the same file - Add case mapping proto enum back to string
|
||||
|
||||
**Why this matters:** Without these, the provider string hits the `default` case and returns `ANTHROPIC`. The webview, provider list, and handler all work fine, but the state silently resets when it round-trips through proto serialization. No error is thrown.
|
||||
|
||||
**Other files to update when adding a provider:**
|
||||
- `src/shared/api.ts` - Add to `ApiProvider` union type, define models
|
||||
- `src/shared/providers/providers.json` - Add to provider list for dropdown
|
||||
- `src/core/api/index.ts` - Register handler in `createHandlerForProvider()`
|
||||
- `webview-ui/src/components/settings/utils/providerUtils.ts` - Add cases in `getModelsForProvider()` and `normalizeApiConfiguration()`
|
||||
- `webview-ui/src/utils/validate.ts` - Add validation case
|
||||
- `webview-ui/src/components/settings/ApiOptions.tsx` - Render provider component
|
||||
|
||||
## Responses API Providers (OpenAI Codex, OpenAI Native)
|
||||
Providers using OpenAI's Responses API require native tool calling. XML tools don't work with the Responses API.
|
||||
|
||||
**Symptoms of broken native tool calling:**
|
||||
- Tools get called multiple times (e.g., `ask_followup_question` asks the same question twice)
|
||||
- Tool arguments get duplicated or malformed
|
||||
- The model responds but tools aren't recognized
|
||||
|
||||
**Root causes to check:**
|
||||
1. **Provider missing from `isNextGenModelProvider()`** in `src/utils/model-utils.ts`. The native variant matchers (e.g., `native-gpt-5/config.ts`) call this function. If your provider isn't in the list, the matcher returns false and falls back to XML tools.
|
||||
|
||||
2. **Model missing `apiFormat: ApiFormat.OPENAI_RESPONSES`** in its model info (`src/shared/api.ts`). This property signals that the model requires native tool calling. The task runner in `src/core/task/index.ts` checks this and forces `enableNativeToolCalls: true` regardless of user settings.
|
||||
|
||||
**When adding a new Responses API provider:**
|
||||
1. Add provider to `isNextGenModelProvider()` list in `src/utils/model-utils.ts`
|
||||
2. Set `apiFormat: ApiFormat.OPENAI_RESPONSES` on all models that use the Responses API
|
||||
3. The variant matcher and task runner will handle the rest automatically
|
||||
|
||||
## Adding Tools to System Prompt
|
||||
This is tricky—multiple prompt variants and configs. **Always search for existing similar tools first and follow their pattern.** Look at the full chain from prompt definition → variant configs → handler → UI before implementing.
|
||||
|
||||
1. **Add to `ClineDefaultTool` enum** in `src/shared/tools.ts`
|
||||
2. **Tool definition** in `src/core/prompts/system-prompt/tools/` (create file like `generate_explanation.ts`)
|
||||
- Define variants for each `ModelFamily` (generic, next-gen, xs, etc.)
|
||||
- Export variants array (e.g., `export const my_tool_variants = [GENERIC, NATIVE_NEXT_GEN, XS]`)
|
||||
- **Fallback behavior**: If a variant isn't defined for a model family, `ClineToolSet.getToolByNameWithFallback()` automatically falls back to GENERIC. So you only need to export `[GENERIC]` unless the tool needs model-specific behavior.
|
||||
3. **Register in `src/core/prompts/system-prompt/tools/init.ts`** - Import and spread into `allToolVariants`
|
||||
4. **Add to variant configs** - Each model family has its own config in `src/core/prompts/system-prompt/variants/*/config.ts`. Add your tool's enum to the `.tools()` list:
|
||||
- `generic/config.ts`, `next-gen/config.ts`, `gpt-5/config.ts`, `native-gpt-5/config.ts`, `native-gpt-5-1/config.ts`, `native-next-gen/config.ts`, `gemini-3/config.ts`, `glm/config.ts`, `hermes/config.ts`, `xs/config.ts`
|
||||
- **Important**: If you add to a variant's config, make sure the tool spec exports a variant for that ModelFamily (or relies on GENERIC fallback)
|
||||
5. **Create handler** in `src/core/task/tools/handlers/`
|
||||
6. **Wire up in `ToolExecutor.ts`** if needed for execution flow
|
||||
7. **Add to tool parsing** in `src/core/assistant-message/index.ts` if needed
|
||||
8. **If tool has UI feedback**: add `ClineSay` enum in proto, update `src/shared/ExtensionMessage.ts`, update `src/shared/proto-conversions/cline-message.ts`, update `webview-ui/src/components/chat/ChatRow.tsx`
|
||||
|
||||
## Modifying System Prompt
|
||||
**Read these first:** `src/core/prompts/system-prompt/README.md`, `tools/README.md`, `__tests__/README.md`
|
||||
|
||||
System prompt is modular: **components** (reusable sections) + **variants** (model-specific configs) + **templates** (with `{{PLACEHOLDER}}` resolution).
|
||||
|
||||
**Key directories:**
|
||||
- `components/` - Shared sections: `rules.ts`, `capabilities.ts`, `editing_files.ts`, etc.
|
||||
- `variants/` - Model-specific: `generic/`, `next-gen/`, `xs/`, `gpt-5/`, `gemini-3/`, `hermes/`, `glm/`, etc.
|
||||
- `templates/` - Template engine and placeholder definitions
|
||||
|
||||
**Variant tiers (ask user which to modify):**
|
||||
- **Next-gen** (Claude 4, GPT-5, Gemini 2.5): `next-gen/`, `native-next-gen/`, `native-gpt-5/`, `native-gpt-5-1/`, `gemini-3/`, `gpt-5/`
|
||||
- **Standard** (default fallback): `generic/`
|
||||
- **Local/small models**: `xs/`, `hermes/`, `glm/`
|
||||
|
||||
**How overrides work:** Variants can override components via `componentOverrides` in their `config.ts`, or provide a custom template in `template.ts` (e.g., `next-gen/template.ts` exports `rules_template`). If no override, the shared component from `components/` is used.
|
||||
|
||||
**Example: Adding a rule to RULES section**
|
||||
1. Check if variant overrides rules: look for `rules_template` in `variants/*/template.ts` or `componentOverrides.RULES` in `config.ts`
|
||||
2. If shared: modify `components/rules.ts`
|
||||
3. If overridden: modify that variant's template
|
||||
4. XS variant is special—has heavily condensed inline content in `template.ts`
|
||||
|
||||
**After any changes, regenerate snapshots:**
|
||||
```bash
|
||||
UPDATE_SNAPSHOTS=true npm run test:unit
|
||||
```
|
||||
Snapshots live in `__tests__/__snapshots__/`. Tests validate across model families and context variations (browser, MCP, focus chain).
|
||||
|
||||
## Modifying Default Slash Commands
|
||||
Three places need updates:
|
||||
- `src/core/slash-commands/index.ts` - Command definitions
|
||||
- `src/core/prompts/commands.ts` - System prompt integration
|
||||
- `webview-ui/src/utils/slash-commands.ts` - Webview autocomplete
|
||||
|
||||
## Adding New Global State Keys
|
||||
Adding a new key to global state requires updates in multiple places. Missing any step causes silent failures.
|
||||
|
||||
Required steps:
|
||||
1. Type definition in `src/shared/storage/state-keys.ts` - Add to `GlobalState` or `Settings` interface
|
||||
2. Read from globalState in `src/core/storage/utils/state-helpers.ts`:
|
||||
- Add `const myKey = context.globalState.get<GlobalStateAndSettings["myKey"]>("myKey")` in `readGlobalStateFromDisk()`
|
||||
- Add to the return object: `myKey: myKey ?? defaultValue,`
|
||||
3. StateManager handles read/write via `setGlobalState()`/`getGlobalStateKey()` after initialization
|
||||
|
||||
Common mistake: Adding only the return value without the `context.globalState.get()` call. This compiles but the value is always `undefined` on load.
|
||||
|
||||
## StateManager Cache vs Direct globalState Access
|
||||
StateManager uses an in-memory cache populated during `StateManager.initialize(context)` in `common.ts`. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
|
||||
|
||||
Exception: State needed immediately at extension startup (before cache is ready)
|
||||
|
||||
When Window A sets state and immediately opens Window B, the new window's StateManager cache is populated from `context.globalState` during initialization. If you need to read state in Window B right at startup (e.g., in `common.ts` during `initialize()`), read directly from `context.globalState.get()` instead of StateManager's cache.
|
||||
|
||||
Example pattern (see `lastShownAnnouncementId` and `worktreeAutoOpenPath`):
|
||||
```typescript
|
||||
// Writing (normal pattern)
|
||||
controller.stateManager.setGlobalState("myKey", value)
|
||||
|
||||
// Reading at startup in common.ts (bypass cache)
|
||||
const value = context.globalState.get<string>("myKey")
|
||||
```
|
||||
|
||||
This is only needed for cross-window state read during the brief startup window before StateManager cache is fully usable. Normal state access after initialization should use StateManager.
|
||||
|
||||
## ChatRow Cancelled/Interrupted States
|
||||
When a ChatRow displays a loading/in-progress state (spinner), you must handle what happens when the task is cancelled. This is non-obvious because cancellation doesn't update the message content—you have to infer it from context.
|
||||
|
||||
**The pattern:**
|
||||
1. A message has a `status` field (e.g., `"generating"`, `"complete"`, `"error"`) stored in `message.text` as JSON
|
||||
2. When cancelled mid-operation, the status stays `"generating"` forever—no one updates it
|
||||
3. To detect cancellation, check TWO conditions:
|
||||
- `!isLast` — if this message is no longer the last message, something else happened after it (interrupted)
|
||||
- `lastModifiedMessage?.ask === "resume_task" || "resume_completed_task"` — task was just cancelled and is waiting to resume
|
||||
|
||||
**Example from `generate_explanation`:**
|
||||
```tsx
|
||||
const wasCancelled =
|
||||
explanationInfo.status === "generating" &&
|
||||
(!isLast ||
|
||||
lastModifiedMessage?.ask === "resume_task" ||
|
||||
lastModifiedMessage?.ask === "resume_completed_task")
|
||||
const isGenerating = explanationInfo.status === "generating" && !wasCancelled
|
||||
```
|
||||
|
||||
**Why both checks?**
|
||||
- `!isLast` catches: cancelled → resumed → did other stuff → this old message is stale
|
||||
- `lastModifiedMessage?.ask === "resume_task"` catches: just cancelled, hasn't resumed yet, this message is still technically "last"
|
||||
|
||||
**See also:** `BrowserSessionRow.tsx` uses similar pattern with `isLastApiReqInterrupted` and `isLastMessageResume`.
|
||||
|
||||
**Backend side:** When streaming is cancelled, clean up properly (close tabs, clear comments, etc.) by checking `taskState.abort` after the streaming function returns.
|
||||
+3
-3
@@ -72,12 +72,12 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
|
||||
# Example configurations:
|
||||
#
|
||||
# Console debugging (logs only):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_TELEMETRY_ENABLED=true
|
||||
# OTEL_LOGS_EXPORTER=console
|
||||
# TEL_DEBUG_DIAGNOSTICS=true
|
||||
#
|
||||
# OTLP with gRPC (insecure, for local testing):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_TELEMETRY_ENABLED=true
|
||||
# OTEL_LOGS_EXPORTER=otlp
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317
|
||||
@@ -85,7 +85,7 @@ POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: tru
|
||||
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer your-token
|
||||
#
|
||||
# OTLP with HTTP/JSON (production):
|
||||
# OTEL_TELEMETRY_ENABLED=1
|
||||
# OTEL_TELEMETRY_ENABLED=true
|
||||
# OTEL_LOGS_EXPORTER=otlp
|
||||
# OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
||||
# OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/docs/
|
||||
/.github/ @saoudrizwan @garoth @sjf
|
||||
/README.md @saoudrizwan @nickbaumann98
|
||||
/src/core/storage/ @celestial-vault @abeatrix
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
name: Cline PR Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
[opened, ready_for_review]
|
||||
# Manual trigger for backfilling existing PRs. Run from terminal:
|
||||
# gh workflow run cline-pr-review.yml -f pr_number=1234
|
||||
# Or batch process open PRs:
|
||||
# gh pr list --state open --limit 10 --json number --jq '.[].number' | while read num; do
|
||||
# gh workflow run cline-pr-review.yml -f pr_number=$num
|
||||
# sleep 60
|
||||
# done
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: "PR number to review"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: pr-review-${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cline-pr-review:
|
||||
# Runs on PR opened/ready_for_review (skips drafts) or manual trigger for backfilling
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
# SECURITY: These permissions are intentionally restrictive.
|
||||
# - contents: read -> cline can read the codebase but CANNOT write/push any code
|
||||
# - pull-requests: write -> cline can post reviews and inline suggestions
|
||||
# - issues: read -> cline can search for related issues
|
||||
# NOTE: Even with pull-requests: write, cline CANNOT merge PRs because branch protection
|
||||
# requires 1 approval from a Code Owner. The GITHUB_TOKEN cannot bypass this.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "npm"
|
||||
|
||||
- name: Install and Verify Cline CLI
|
||||
run: |
|
||||
npx cline version # verify installation
|
||||
|
||||
- name: Configure Cline with Anthropic
|
||||
run: |
|
||||
npx cline auth --provider anthropic \
|
||||
--apikey "${{ secrets.ANTHROPIC_API_KEY }}" \
|
||||
--modelid claude-opus-4-5-20251101
|
||||
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Review PR with Cline
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.pr.outputs.number }}
|
||||
GITHUB_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CLINE_COMMAND_PERMISSIONS: |
|
||||
{
|
||||
"allow": [
|
||||
"gh pr diff *",
|
||||
"gh pr view *",
|
||||
"gh pr checks *",
|
||||
"gh pr list *",
|
||||
"gh label list *",
|
||||
"gh issue list *",
|
||||
"gh issue view *",
|
||||
"git log *",
|
||||
"gh pr comment ${{ steps.pr.outputs.number }} *",
|
||||
"gh pr edit ${{ steps.pr.outputs.number }} *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/comments *",
|
||||
"gh api repos/${{ github.repository }}/pulls/${{ steps.pr.outputs.number }}/reviews *"
|
||||
]
|
||||
}
|
||||
run: |
|
||||
npx cline --yolo 'You'\''re a GitHub PR reviewer for the open source Cline repository. Your goal is to give the PR author helpful feedback and give maintainers the context they need to review efficiently.
|
||||
|
||||
PR: #'"${PR_NUMBER}"'
|
||||
|
||||
## Gather context
|
||||
|
||||
```bash
|
||||
# Get full PR details
|
||||
gh pr view '"${PR_NUMBER}"' --json number,title,body,author,createdAt,updatedAt,isDraft,labels,commits,files,additions,deletions,changedFiles,baseRefName,headRefName,mergeable,reviewDecision
|
||||
|
||||
# Get the diff
|
||||
gh pr diff '"${PR_NUMBER}"'
|
||||
|
||||
# Check CI status
|
||||
gh pr checks '"${PR_NUMBER}"'
|
||||
|
||||
# Get existing review comments (to understand context and your previous feedback)
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/comments --jq '\''.[] | {user: .user.login, body: .body, path: .path, created_at: .created_at}'\''
|
||||
|
||||
# Get conversation comments
|
||||
gh pr view '"${PR_NUMBER}"' --comments
|
||||
```
|
||||
|
||||
If this is a re-review (workflow_dispatch event):
|
||||
Read your previous comments carefully. Understand what you asked for before.
|
||||
Check if new commits or comments address your previous feedback.
|
||||
|
||||
## Check contributing guidelines
|
||||
|
||||
Flag (but don'\''t block) if:
|
||||
- Missing changeset - For user-facing changes, check if there'\''s a `.changeset/` file:
|
||||
```bash
|
||||
gh pr diff '"${PR_NUMBER}"' --name-only | grep '\''.changeset/'\'' || echo '\''No changeset found'\''
|
||||
```
|
||||
If missing, ask them to run `npm run changeset`
|
||||
- Missing tests - New features should have tests
|
||||
|
||||
## Find related issues and PRs
|
||||
|
||||
Search thoroughly for context that might help with the review:
|
||||
|
||||
```bash
|
||||
# Find related issues for context
|
||||
gh issue list --search '\''<keywords from the PR>'\'' --state all --limit 30
|
||||
gh issue list --search '\''<error messages or feature names>'\'' --state all --limit 20
|
||||
|
||||
# Find similar PRs for reference
|
||||
gh pr list --search '\''<keywords>'\'' --state all --limit 30
|
||||
```
|
||||
|
||||
For each relevant issue or PR you find, read it including comments:
|
||||
```bash
|
||||
gh issue view <number> --comments
|
||||
gh pr view <number> --comments
|
||||
```
|
||||
|
||||
Look for:
|
||||
- Open issues this PR might fix that weren'\''t linked in the description
|
||||
- Similar PRs that went through review - what feedback did they get? What patterns did they follow?
|
||||
- Context from maintainer discussions that could inform your review
|
||||
|
||||
## Find subject matter experts
|
||||
|
||||
For files changed in this PR, find who knows the code best:
|
||||
```bash
|
||||
# Get files changed
|
||||
gh pr diff '"${PR_NUMBER}"' --name-only
|
||||
|
||||
# For each relevant path, find contributors
|
||||
git log --since='\''6 months ago'\'' --format='\''%an'\'' -- <path> | sort | uniq -c | sort -rn | head -5
|
||||
```
|
||||
|
||||
Cross-reference git authors with GitHub usernames. Include an SME table in your response:
|
||||
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username1 | Authored PR #X which modified this area |
|
||||
| @username2 | Primary contributor to affected file (15 commits in 6 months) |
|
||||
| @username3 | Reviewed similar PR #Y with extensive feedback |
|
||||
|
||||
## Bash command usage
|
||||
|
||||
Don'\''t use operators like `|`, `&&`, or `;` - run each command separately and analyze the output.
|
||||
|
||||
When referencing command outputs, quote them properly to avoid formatting issues.
|
||||
|
||||
## Deep code review
|
||||
|
||||
This is the most important part. Don'\''t just look for syntax issues - understand what the PR is trying to achieve and whether the implementation is the right approach.
|
||||
|
||||
Step 1: Understand the intent
|
||||
Read the PR description and understand what the author is trying to accomplish. What problem are they solving? What feature are they adding?
|
||||
|
||||
Step 2: Form your own opinion first
|
||||
Before analyzing their code, think about how YOU would implement this feature or fix. What files would you touch? What patterns would you follow? What edge cases would you handle?
|
||||
|
||||
Step 3: Compare approaches
|
||||
Now look at their implementation. How does it compare to what you would have done?
|
||||
- Is their approach better in some ways? Note what they did well.
|
||||
- Is their approach missing something? Be specific about what and why.
|
||||
- Are there edge cases they haven'\''t considered?
|
||||
- Does it follow the patterns established in similar parts of the codebase?
|
||||
|
||||
Step 4: Look at the bigger picture
|
||||
- What other files or systems does this change interact with?
|
||||
- Could this break anything else?
|
||||
- Is there additional work needed beyond this PR to complete the feature?
|
||||
- Does this fit well with the overall architecture?
|
||||
|
||||
Step 5: Find reference implementations
|
||||
Look for similar changes in the codebase:
|
||||
```bash
|
||||
git log --oneline --all --grep='\''<relevant keywords>'\'' | head -20
|
||||
git log --oneline -- <similar files> | head -20
|
||||
```
|
||||
|
||||
If this is adding a new API provider, look at how other providers are implemented.
|
||||
If this is adding a new feature, look at how similar features were added.
|
||||
Note where their implementation aligns with or diverges from established patterns.
|
||||
|
||||
Step 6: Standard code review checks
|
||||
- DRY: Is there duplicated code that could be extracted?
|
||||
- Error handling: Are errors handled appropriately?
|
||||
- Security: Any injection risks, credential exposure, unsafe dependencies?
|
||||
- Performance: Any obvious inefficiencies, memory leaks, N+1 patterns?
|
||||
- Types: Is TypeScript used correctly? Any unsafe type assertions?
|
||||
- Naming: Are variables and functions named clearly?
|
||||
- Comments: Is complex logic explained? Are there outdated comments?
|
||||
|
||||
## Inline code suggestions
|
||||
|
||||
For specific code improvements, use GitHub'\''s suggestion syntax via `gh api`.
|
||||
This creates suggestions the author can commit with one click.
|
||||
|
||||
Single-line suggestion:
|
||||
```bash
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
|
||||
-f event='\''COMMENT'\'' \
|
||||
-f body='\'''\'' \
|
||||
-F comments='\''[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"line": 42,
|
||||
"body": "Consider simplifying:\n\n```suggestion\nconst result = items.filter(Boolean);\n```"
|
||||
}
|
||||
]'\''
|
||||
```
|
||||
|
||||
Multi-line suggestion (replacing lines 40-45):
|
||||
```bash
|
||||
gh api repos/'"${GITHUB_REPO}"'/pulls/'"${PR_NUMBER}"'/reviews \
|
||||
-X POST \
|
||||
-f commit_id="$(gh pr view '"${PR_NUMBER}"' --json headRefOid -q .headRefOid)" \
|
||||
-f event='\''COMMENT'\'' \
|
||||
-f body='\'''\'' \
|
||||
-F comments='\''[
|
||||
{
|
||||
"path": "src/example.ts",
|
||||
"start_line": 40,
|
||||
"line": 45,
|
||||
"body": "This can be simplified:\n\n```suggestion\nconst simplified = doThing();\n```"
|
||||
}
|
||||
]'\''
|
||||
```
|
||||
|
||||
Use inline suggestions for concrete improvements. Use regular comments for questions or broader feedback.
|
||||
|
||||
## Post your review
|
||||
|
||||
After your investigation, post a single helpful comment that helps the author and gives maintainers context.
|
||||
|
||||
Start with a warm thank you for their contribution. Be conversational, not robotic.
|
||||
|
||||
Include what'\''s relevant:
|
||||
- In-depth explanation of what the PR does - Be comprehensive. A maintainer should be able to read this section and fully understand the author'\''s intent, why they made the changes, how they implemented it, and what files/systems are affected. Don'\''t just summarize - explain.
|
||||
- Related issues/PRs you found that provide useful context (link to them)
|
||||
- Your review findings (issues to address, suggestions, etc.)
|
||||
- Clear next steps for the author
|
||||
|
||||
Include a '\''For Maintainers'\'' section with:
|
||||
- Anything else useful to help the maintainer resolve this PR
|
||||
- Related issues/PRs with context on why they'\''re relevant
|
||||
- Open issues this PR might fix that weren'\''t linked in the description
|
||||
- Your recommendation: merge as-is, needs changes, needs discussion, close, etc.
|
||||
- SME table - who should review this and why
|
||||
|
||||
For the SME table:
|
||||
| SME | Reason |
|
||||
|-----|--------|
|
||||
| @username | Primary contributor to affected files |
|
||||
|
||||
## Update labels
|
||||
|
||||
Add appropriate labels based on your analysis:
|
||||
```bash
|
||||
gh label list --json name,description --limit 100
|
||||
gh pr edit '"${PR_NUMBER}"' --add-label '\''label1,label2'\''
|
||||
```
|
||||
|
||||
When done, add the reviewed label:
|
||||
```bash
|
||||
gh pr edit '"${PR_NUMBER}"' --add-label '\''Bot Reviewed'\''
|
||||
```
|
||||
|
||||
## Remember
|
||||
|
||||
- This is a one-time automated response - you will NOT see their reply or respond again. Never say things like '\''let me know if you have questions'\'', '\''I can help you with'\'', or '\''feel free to ask'\'' - you won'\''t be there to follow up. Frame any questions for the maintainers who will follow up.
|
||||
- Be helpful and welcoming - Many contributors are new to the project
|
||||
- Be specific - Point to exact lines and suggest fixes, don'\''t give vague feedback
|
||||
- Think deeply - Don'\''t just surface-level review, understand the intent and evaluate the approach
|
||||
- Use inline suggestions - Make it easy for authors to accept changes
|
||||
- You'\''re a first-pass reviewer - A human maintainer will do final approval'
|
||||
@@ -0,0 +1,130 @@
|
||||
name: Publish NPM Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirm_publish:
|
||||
description: 'Type "publish" to confirm you want to publish to NPM'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish-npm-release:
|
||||
needs: test
|
||||
name: Publish Cline CLI to NPM
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && github.event.inputs.confirm_publish == 'publish'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
cache-dependency-path: cli/go.sum
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- name: Read release version
|
||||
id: version
|
||||
run: |
|
||||
# Read version from cli/package.json (stable version)
|
||||
VERSION=$(node -p "require('./cli/package.json').version")
|
||||
echo "Release version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
run: npm run download-ripgrep
|
||||
|
||||
- name: Clean previous builds
|
||||
run: rm -rf dist-standalone
|
||||
|
||||
- name: Generate Protos (First Pass)
|
||||
run: npm run protos && npm run protos-go
|
||||
|
||||
- name: Compile CLI
|
||||
run: npm run compile-cli
|
||||
|
||||
- name: Compile CLI for all platforms
|
||||
run: npm run compile-cli-all-platforms
|
||||
|
||||
- name: Build standalone NPM package
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
OTEL_TELEMETRY_ENABLED: "1"
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: npm run compile-standalone-npm
|
||||
|
||||
- name: Generate Protos (Second Pass - Bug Workaround)
|
||||
run: npm run protos && npm run protos-go
|
||||
|
||||
- name: Verify build output
|
||||
run: |
|
||||
echo "Checking dist-standalone directory..."
|
||||
ls -la dist-standalone/
|
||||
|
||||
echo "Verifying CLI binaries..."
|
||||
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
|
||||
|
||||
echo "Checking package.json in dist-standalone..."
|
||||
cat dist-standalone/package.json | grep version
|
||||
|
||||
- name: Publish to NPM with latest tag
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'latest'..."
|
||||
cd dist-standalone
|
||||
npm publish --tag latest --access public
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'latest'"
|
||||
echo ""
|
||||
echo "📦 Install with: npm install -g cline"
|
||||
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
|
||||
@@ -0,0 +1,175 @@
|
||||
name: Publish NPM Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write # Required by test workflow
|
||||
pull-requests: write # Required by test workflow
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
|
||||
publish-npm-nightly:
|
||||
needs: test
|
||||
name: Publish Cline CLI (Nightly) to NPM
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for recent commits
|
||||
id: check_commits
|
||||
run: |
|
||||
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
|
||||
echo "No commits in last 24 hours, skipping publish"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Found recent commits, proceeding with publish"
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup Go
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
cache-dependency-path: cli/go.sum
|
||||
|
||||
# Cache root dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache root dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
|
||||
- name: Install root dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.root-cache.outputs.cache-hit != 'true'
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
if: steps.check_commits.outputs.skip != 'true' && steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- name: Generate nightly version with timestamp
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
id: version
|
||||
run: |
|
||||
# Read base version from cli/package.json (e.g., "1.0.9")
|
||||
BASE_VERSION=$(node -p "require('./cli/package.json').version")
|
||||
|
||||
# Generate timestamp (Unix epoch seconds)
|
||||
TIMESTAMP=$(date +%s)
|
||||
|
||||
# Create unique nightly version: 1.0.9-nightly.1736365200
|
||||
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
|
||||
|
||||
echo "Base version: $BASE_VERSION"
|
||||
echo "Generated nightly version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update cli/package.json with nightly version
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
# Update version with timestamp-based nightly version
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('cli/package.json', 'utf8'));
|
||||
pkg.version = '${{ steps.version.outputs.version }}';
|
||||
fs.writeFileSync('cli/package.json', JSON.stringify(pkg, null, '\t'));
|
||||
"
|
||||
|
||||
echo "Using version ${{ steps.version.outputs.version }} for build"
|
||||
cat cli/package.json | grep '"version"'
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run download-ripgrep
|
||||
|
||||
- name: Clean previous builds
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: rm -rf dist-standalone
|
||||
|
||||
- name: Generate Protos (First Pass)
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run protos && npm run protos-go
|
||||
|
||||
- name: Compile CLI
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run compile-cli
|
||||
|
||||
- name: Compile CLI for all platforms
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run compile-cli-all-platforms
|
||||
|
||||
- name: Build standalone NPM package
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
OTEL_TELEMETRY_ENABLED: "1"
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
POSTHOG_TELEMETRY_ENABLED: "true"
|
||||
run: npm run compile-standalone-npm
|
||||
|
||||
- name: Generate Protos (Second Pass - Bug Workaround)
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: npm run protos && npm run protos-go
|
||||
|
||||
- name: Verify build output
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "Checking dist-standalone directory..."
|
||||
ls -la dist-standalone/
|
||||
|
||||
echo "Verifying CLI binaries..."
|
||||
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
|
||||
|
||||
echo "Checking package.json in dist-standalone..."
|
||||
cat dist-standalone/package.json | grep version
|
||||
|
||||
- name: Publish to NPM with nightly tag
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
|
||||
cd dist-standalone
|
||||
npm publish --tag nightly --access public
|
||||
|
||||
- name: Summary
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: |
|
||||
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'nightly'"
|
||||
echo ""
|
||||
echo "📦 Install with: npm install -g cline@nightly"
|
||||
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
|
||||
@@ -5,6 +5,7 @@ node_modules
|
||||
tmp
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
/pkg
|
||||
|
||||
.DS_Store
|
||||
.idea
|
||||
@@ -41,3 +42,8 @@ webview-ui/src/services/grpc-client.ts
|
||||
|
||||
# E2E Tests
|
||||
test-results
|
||||
|
||||
/.github/act
|
||||
/pkg
|
||||
.secrets
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Default
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
.worktrees/**
|
||||
CLAUDE.local.md
|
||||
out/
|
||||
dist-standalone/
|
||||
node_modules/
|
||||
|
||||
+58
-1
@@ -1,8 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
## [3.51.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Adding OpenAI gpt-5.2-codex model to the model picker
|
||||
|
||||
## [3.50.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add gpt-5.2-codex OpenAI model support
|
||||
- Add create-pull-request skill
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix the selection of remotely configured providers
|
||||
- Fix act_mode_respond to prevent consecutive calls
|
||||
- Fix invalid tool call IDs when switching between model formats
|
||||
|
||||
## [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 +1737,4 @@ Add Opus 4.1 through Claude Code
|
||||
|
||||
## [0.0.6]
|
||||
|
||||
- Initial release
|
||||
- Initial release
|
||||
@@ -1,129 +1,2 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file is the secret sauce for working effectively in this codebase. It captures tribal knowledge—the nuanced, non-obvious patterns that make the difference between a quick fix and hours of back-and-forth & human intervention.
|
||||
|
||||
**When to add to this file:**
|
||||
- User had to intervene, correct, or hand-hold
|
||||
- Multiple back-and-forth attempts were needed to get something working
|
||||
- You discovered something that required reading many files to understand
|
||||
- A change touched files you wouldn't have guessed
|
||||
- Something worked differently than you expected
|
||||
- User explicitly asks to "add this to CLAUDE.md"
|
||||
|
||||
**Proactively suggest additions** when any of the above happen—don't wait to be asked.
|
||||
|
||||
**What NOT to add:** Stuff you can figure out from reading a few files, obvious patterns, or standard practices. This file should be high-signal, not comprehensive.
|
||||
|
||||
## Miscellaneous
|
||||
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
|
||||
- When creating PRs, if the change is user-facing and significant enough to warrant a changelog entry, run `npm run changeset` and create a patch changeset. Never create minor or major version bumps. Skip changesets for trivial fixes, internal refactors, or minor UI tweaks that users wouldn't notice.
|
||||
|
||||
## gRPC/Protobuf Communication
|
||||
The extension and webview communicate via gRPC-like protocol over VS Code message passing.
|
||||
|
||||
**Proto files live in `proto/`** (e.g., `proto/cline/task.proto`, `proto/cline/ui.proto`)
|
||||
- Each feature domain has its own `.proto` file
|
||||
- For simple data, use shared types in `proto/cline/common.proto` (`StringRequest`, `Empty`, `Int64Request`)
|
||||
- For complex data, define custom messages in the feature's `.proto` file
|
||||
- Naming: Services `PascalCaseService`, RPCs `camelCase`, Messages `PascalCase`
|
||||
- For streaming responses, use `stream` keyword (see `subscribeToAuthCallback` in `account.proto`)
|
||||
|
||||
**Run `npm run protos`** after any proto changes—generates types in:
|
||||
- `src/shared/proto/` - Shared type definitions
|
||||
- `src/generated/grpc-js/` - Service implementations
|
||||
- `src/generated/nice-grpc/` - Promise-based clients
|
||||
- `src/generated/hosts/` - Generated handlers
|
||||
|
||||
**Adding new enum values** (like a new `ClineSay` type) requires updating conversion mappings in `src/shared/proto-conversions/cline-message.ts`
|
||||
|
||||
**Adding new RPC methods** requires:
|
||||
- Handler in `src/core/controller/<domain>/`
|
||||
- Call from webview via generated client: `UiServiceClient.scrollToSettings(StringRequest.create({ value: "browser" }))`
|
||||
|
||||
**Example—the `explain-changes` feature touched:**
|
||||
- `proto/cline/task.proto` - Added `ExplainChangesRequest` message and `explainChanges` RPC
|
||||
- `proto/cline/ui.proto` - Added `GENERATE_EXPLANATION = 29` to `ClineSay` enum
|
||||
- `src/shared/ExtensionMessage.ts` - Added `ClineSayGenerateExplanation` type
|
||||
- `src/shared/proto-conversions/cline-message.ts` - Added mapping for new say type
|
||||
- `src/core/controller/task/explainChanges.ts` - Handler implementation
|
||||
- `webview-ui/src/components/chat/ChatRow.tsx` - UI rendering
|
||||
|
||||
## Adding Tools to System Prompt
|
||||
This is tricky—multiple prompt variants and configs. **Always search for existing similar tools first and follow their pattern.** Look at the full chain from prompt definition → variant configs → handler → UI before implementing.
|
||||
|
||||
1. **Add to `ClineDefaultTool` enum** in `src/shared/tools.ts`
|
||||
2. **Tool definition** in `src/core/prompts/system-prompt/tools/` (create file like `generate_explanation.ts`)
|
||||
- Define variants for each `ModelFamily` (generic, next-gen, xs, etc.)
|
||||
- Export variants array (e.g., `export const my_tool_variants = [GENERIC, NATIVE_NEXT_GEN, XS]`)
|
||||
- **Fallback behavior**: If a variant isn't defined for a model family, `ClineToolSet.getToolByNameWithFallback()` automatically falls back to GENERIC. So you only need to export `[GENERIC]` unless the tool needs model-specific behavior.
|
||||
3. **Register in `src/core/prompts/system-prompt/tools/init.ts`** - Import and spread into `allToolVariants`
|
||||
4. **Add to variant configs** - Each model family has its own config in `src/core/prompts/system-prompt/variants/*/config.ts`. Add your tool's enum to the `.tools()` list:
|
||||
- `generic/config.ts`, `next-gen/config.ts`, `gpt-5/config.ts`, `native-gpt-5/config.ts`, `native-gpt-5-1/config.ts`, `native-next-gen/config.ts`, `gemini-3/config.ts`, `glm/config.ts`, `hermes/config.ts`, `xs/config.ts`
|
||||
- **Important**: If you add to a variant's config, make sure the tool spec exports a variant for that ModelFamily (or relies on GENERIC fallback)
|
||||
5. **Create handler** in `src/core/task/tools/handlers/`
|
||||
6. **Wire up in `ToolExecutor.ts`** if needed for execution flow
|
||||
7. **Add to tool parsing** in `src/core/assistant-message/index.ts` if needed
|
||||
8. **If tool has UI feedback**: add `ClineSay` enum in proto, update `src/shared/ExtensionMessage.ts`, update `src/shared/proto-conversions/cline-message.ts`, update `webview-ui/src/components/chat/ChatRow.tsx`
|
||||
|
||||
## Modifying System Prompt
|
||||
**Read these first:** `src/core/prompts/system-prompt/README.md`, `tools/README.md`, `__tests__/README.md`
|
||||
|
||||
System prompt is modular: **components** (reusable sections) + **variants** (model-specific configs) + **templates** (with `{{PLACEHOLDER}}` resolution).
|
||||
|
||||
**Key directories:**
|
||||
- `components/` - Shared sections: `rules.ts`, `capabilities.ts`, `editing_files.ts`, etc.
|
||||
- `variants/` - Model-specific: `generic/`, `next-gen/`, `xs/`, `gpt-5/`, `gemini-3/`, `hermes/`, `glm/`, etc.
|
||||
- `templates/` - Template engine and placeholder definitions
|
||||
|
||||
**Variant tiers (ask user which to modify):**
|
||||
- **Next-gen** (Claude 4, GPT-5, Gemini 2.5): `next-gen/`, `native-next-gen/`, `native-gpt-5/`, `native-gpt-5-1/`, `gemini-3/`, `gpt-5/`
|
||||
- **Standard** (default fallback): `generic/`
|
||||
- **Local/small models**: `xs/`, `hermes/`, `glm/`
|
||||
|
||||
**How overrides work:** Variants can override components via `componentOverrides` in their `config.ts`, or provide a custom template in `template.ts` (e.g., `next-gen/template.ts` exports `rules_template`). If no override, the shared component from `components/` is used.
|
||||
|
||||
**Example: Adding a rule to RULES section**
|
||||
1. Check if variant overrides rules: look for `rules_template` in `variants/*/template.ts` or `componentOverrides.RULES` in `config.ts`
|
||||
2. If shared: modify `components/rules.ts`
|
||||
3. If overridden: modify that variant's template
|
||||
4. XS variant is special—has heavily condensed inline content in `template.ts`
|
||||
|
||||
**After any changes, regenerate snapshots:**
|
||||
```bash
|
||||
UPDATE_SNAPSHOTS=true npm run test:unit
|
||||
```
|
||||
Snapshots live in `__tests__/__snapshots__/`. Tests validate across model families and context variations (browser, MCP, focus chain).
|
||||
|
||||
## Modifying Default Slash Commands
|
||||
Three places need updates:
|
||||
- `src/core/slash-commands/index.ts` - Command definitions
|
||||
- `src/core/prompts/commands.ts` - System prompt integration
|
||||
- `webview-ui/src/utils/slash-commands.ts` - Webview autocomplete
|
||||
|
||||
## ChatRow Cancelled/Interrupted States
|
||||
When a ChatRow displays a loading/in-progress state (spinner), you must handle what happens when the task is cancelled. This is non-obvious because cancellation doesn't update the message content—you have to infer it from context.
|
||||
|
||||
**The pattern:**
|
||||
1. A message has a `status` field (e.g., `"generating"`, `"complete"`, `"error"`) stored in `message.text` as JSON
|
||||
2. When cancelled mid-operation, the status stays `"generating"` forever—no one updates it
|
||||
3. To detect cancellation, check TWO conditions:
|
||||
- `!isLast` — if this message is no longer the last message, something else happened after it (interrupted)
|
||||
- `lastModifiedMessage?.ask === "resume_task" || "resume_completed_task"` — task was just cancelled and is waiting to resume
|
||||
|
||||
**Example from `generate_explanation`:**
|
||||
```tsx
|
||||
const wasCancelled =
|
||||
explanationInfo.status === "generating" &&
|
||||
(!isLast ||
|
||||
lastModifiedMessage?.ask === "resume_task" ||
|
||||
lastModifiedMessage?.ask === "resume_completed_task")
|
||||
const isGenerating = explanationInfo.status === "generating" && !wasCancelled
|
||||
```
|
||||
|
||||
**Why both checks?**
|
||||
- `!isLast` catches: cancelled → resumed → did other stuff → this old message is stale
|
||||
- `lastModifiedMessage?.ask === "resume_task"` catches: just cancelled, hasn't resumed yet, this message is still technically "last"
|
||||
|
||||
**See also:** `BrowserSessionRow.tsx` uses similar pattern with `isLastApiReqInterrupted` and `isLastMessageResume`.
|
||||
|
||||
**Backend side:** When streaming is cancelled, clean up properly (close tabs, clear comments, etc.) by checking `taskState.abort` after the streaming function returns.
|
||||
@.clinerules/general.md
|
||||
@.clinerules/network.md
|
||||
|
||||
+7
-1
@@ -70,7 +70,8 @@
|
||||
"noControlCharactersInRegex": "off",
|
||||
"noShadowRestrictedNames": "off",
|
||||
"noArrayIndexKey": "info",
|
||||
"noAssignInExpressions": "info"
|
||||
"noAssignInExpressions": "info",
|
||||
"useIterableCallbackReturn": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noUselessConstructor": "off",
|
||||
@@ -111,6 +112,11 @@
|
||||
"expand": "always"
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"includes": [
|
||||
"**",
|
||||
|
||||
@@ -37,8 +37,9 @@ var (
|
||||
|
||||
func main() {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "cline [prompt]",
|
||||
Short: "Cline CLI - AI-powered coding assistant",
|
||||
Use: "cline [prompt]",
|
||||
Short: "Cline CLI - AI-powered coding assistant",
|
||||
Version: global.CliVersion,
|
||||
Long: `A command-line interface for interacting with Cline AI coding assistant.
|
||||
|
||||
Start a new task by providing a prompt:
|
||||
@@ -177,6 +178,8 @@ see the manual page: man cline`,
|
||||
},
|
||||
}
|
||||
|
||||
rootCmd.SetVersionTemplate(cli.VersionString())
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&coreAddress, "address", fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT), "Cline Core gRPC address")
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "F", "rich", "output format (rich|json|plain)")
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.9",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "cline-core.js",
|
||||
"bin": {
|
||||
@@ -59,7 +59,8 @@
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux"
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64",
|
||||
|
||||
@@ -358,6 +358,9 @@ func AddProviderPartial(ctx context.Context, manager *task.Manager, provider cli
|
||||
if openRouterInfo, ok := modelInfo.(*cline.OpenRouterModelInfo); ok {
|
||||
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
|
||||
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
|
||||
} else if ocaInfo, ok := modelInfo.(*cline.OcaModelInfo); ok {
|
||||
apiConfig.PlanModeOcaModelInfo = ocaInfo
|
||||
apiConfig.ActModeOcaModelInfo = ocaInfo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +429,9 @@ func UpdateProviderPartial(ctx context.Context, manager *task.Manager, provider
|
||||
if openRouterInfo, ok := updates.ModelInfo.(*cline.OpenRouterModelInfo); ok {
|
||||
apiConfig.PlanModeOpenRouterModelInfo = openRouterInfo
|
||||
apiConfig.ActModeOpenRouterModelInfo = openRouterInfo
|
||||
} else if ocaInfo, ok := updates.ModelInfo.(*cline.OcaModelInfo); ok {
|
||||
apiConfig.PlanModeOcaModelInfo = ocaInfo
|
||||
apiConfig.ActModeOcaModelInfo = ocaInfo
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
|
||||
}
|
||||
|
||||
// Step 3: Select model
|
||||
modelID, _, err := pw.selectModel(cline.ApiProvider_OCA, "")
|
||||
modelID, modelInfo, err := pw.selectModel(cline.ApiProvider_OCA, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("model selection failed: %w", err)
|
||||
}
|
||||
@@ -198,7 +198,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
|
||||
// Step 4: Apply the OCA model configuration and set as active
|
||||
updates := ProviderUpdatesPartial{
|
||||
ModelID: &modelID,
|
||||
ModelInfo: nil,
|
||||
ModelInfo: modelInfo,
|
||||
}
|
||||
|
||||
if err := UpdateProviderPartial(pw.ctx, pw.manager, cline.ApiProvider_OCA, updates, true); err != nil {
|
||||
|
||||
@@ -339,6 +339,13 @@ func (tr *ToolRenderer) RenderCommandOutput(output string) string {
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func (tr *ToolRenderer) RenderCommandPermissionDenied(command string) string {
|
||||
command = strings.TrimSpace(command)
|
||||
rendered := tr.renderMarkdown("### Command was denied")
|
||||
message := fmt.Sprintf("Cline does not have permission to execute this command: `%s`", command)
|
||||
return fmt.Sprintf("\n%s\n\n%s\n", rendered, message)
|
||||
}
|
||||
|
||||
// RenderUserResponse renders user approval/rejection feedback
|
||||
func (tr *ToolRenderer) RenderUserResponse(approved bool, feedback string) string {
|
||||
var symbol, status string
|
||||
|
||||
@@ -3,15 +3,9 @@ package global
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cline/cli/pkg/common"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
// ClineClients manages Cline instances using the new registry system
|
||||
@@ -242,269 +236,3 @@ func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address stri
|
||||
return fmt.Errorf("cannot start remote instance at %s", normalized)
|
||||
}
|
||||
|
||||
func startClineHost(hostPort int, workspaces []string) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-host on port %d\n", hostPort)
|
||||
}
|
||||
|
||||
// Get the directory where the cline binary is located
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
binDir := path.Dir(execPath)
|
||||
clineHostPath := path.Join(binDir, "cline-host")
|
||||
|
||||
// Build command arguments
|
||||
args := []string{
|
||||
"--verbose",
|
||||
"--port", fmt.Sprintf("%d", hostPort),
|
||||
}
|
||||
|
||||
for _, ws := range workspaces {
|
||||
args = append(args, "--workspace", ws)
|
||||
}
|
||||
|
||||
// Start the cline-host process
|
||||
cmd := exec.Command(clineHostPath, args...)
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := path.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-host-%s-localhost-%d.log", timestamp, hostPort)
|
||||
logFilePath := path.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-host: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-host (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-host output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// KillInstanceByAddress kills a Cline instance by its address
|
||||
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, address string) error {
|
||||
// Check if the instance exists in the registry
|
||||
_, err := registry.GetInstance(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("instance %s not found in registry", address)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Killing instance: %s\n", address)
|
||||
}
|
||||
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get process info for instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Terminating process PID %d...\n", pid)
|
||||
}
|
||||
|
||||
// Kill the process
|
||||
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
||||
return fmt.Errorf("failed to kill process %d: %w", pid, err)
|
||||
}
|
||||
|
||||
// Wait for the instance to remove itself from registry
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Waiting for instance to clean up registry entry...\n")
|
||||
}
|
||||
for range 5 {
|
||||
time.Sleep(1 * time.Second)
|
||||
if !registry.HasInstanceAtAddress(address) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Instance %s successfully killed and removed from registry.\n", address)
|
||||
}
|
||||
|
||||
// Update default instance if needed
|
||||
instances, err := registry.ListInstancesCleaned(ctx)
|
||||
if err == nil && len(instances) > 0 {
|
||||
// ensureDefaultInstance logic will handle setting a new default
|
||||
defaultInstance := registry.GetDefaultInstance()
|
||||
if defaultInstance == address || defaultInstance == "" {
|
||||
if len(instances) > 0 {
|
||||
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("instance killed but failed to remove itself from registry within 5 seconds")
|
||||
}
|
||||
|
||||
func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-core on port %d (with hostbridge on %d)\n", corePort, hostPort)
|
||||
}
|
||||
|
||||
// Get the executable path and resolve symlinks (for npm global installs)
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Resolve symlinks to get the real path
|
||||
// For npm global installs, execPath might be a symlink like:
|
||||
// /opt/homebrew/bin/cline -> /opt/homebrew/lib/node_modules/cline/bin/cline
|
||||
realPath, err := filepath.EvalSymlinks(execPath)
|
||||
if err != nil {
|
||||
// If we can't resolve symlinks, fall back to the original path
|
||||
realPath = execPath
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Warning: Could not resolve symlinks for %s: %v\n", execPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
binDir := path.Dir(realPath)
|
||||
installDir := path.Dir(binDir)
|
||||
clineCorePath := path.Join(installDir, "cline-core.js")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Executable path: %s\n", execPath)
|
||||
if realPath != execPath {
|
||||
fmt.Printf("Real path (after resolving symlinks): %s\n", realPath)
|
||||
}
|
||||
fmt.Printf("Bin directory: %s\n", binDir)
|
||||
fmt.Printf("Install directory: %s\n", installDir)
|
||||
fmt.Printf("Looking for cline-core.js at: %s\n", clineCorePath)
|
||||
}
|
||||
|
||||
// Check if cline-core.js exists at the primary location
|
||||
var finalClineCorePath string
|
||||
var finalInstallDir string
|
||||
if _, err := os.Stat(clineCorePath); os.IsNotExist(err) {
|
||||
// Development mode: Try ../../dist-standalone/cline-core.js
|
||||
// This handles the case where we're running from cli/bin/cline
|
||||
devClineCorePath := path.Join(binDir, "..", "..", "dist-standalone", "cline-core.js")
|
||||
devInstallDir := path.Join(binDir, "..", "..", "dist-standalone")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Primary location not found, trying development path: %s\n", devClineCorePath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(devClineCorePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("cline-core.js not found at '%s' or '%s'. Please ensure you're running from the correct location or reinstall with 'npm install -g cline'", clineCorePath, devClineCorePath)
|
||||
}
|
||||
|
||||
finalClineCorePath = devClineCorePath
|
||||
finalInstallDir = devInstallDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using development mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
} else {
|
||||
finalClineCorePath = clineCorePath
|
||||
finalInstallDir = installDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using production mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := path.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-core-%s-localhost-%d.log", timestamp, corePort)
|
||||
logFilePath := path.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Start the cline-core process with --config flag using system node
|
||||
args := []string{finalClineCorePath,
|
||||
"--port", fmt.Sprintf("%d", corePort),
|
||||
"--host-bridge-port", fmt.Sprintf("%d", hostPort),
|
||||
"--config", Config.ConfigPath}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using system node\n")
|
||||
}
|
||||
|
||||
cmd := exec.Command("node", args...)
|
||||
|
||||
// Set working directory to installation root
|
||||
cmd.Dir = finalInstallDir
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
// Set environment variables with NODE_PATH for both real and fake node_modules
|
||||
// The fake node_modules contains the vscode stub that can't be in the real node_modules
|
||||
env := os.Environ()
|
||||
realNodeModules := path.Join(finalInstallDir, "node_modules")
|
||||
fakeNodeModules := path.Join(finalInstallDir, "fake_node_modules")
|
||||
nodePath := fmt.Sprintf("%s%c%s", realNodeModules, os.PathListSeparator, fakeNodeModules)
|
||||
|
||||
env = append(env,
|
||||
fmt.Sprintf("NODE_PATH=%s", nodePath),
|
||||
// These control gRPC debug logging
|
||||
//"GRPC_TRACE=all",
|
||||
//"GRPC_VERBOSITY=DEBUG",
|
||||
"NODE_ENV=development",
|
||||
)
|
||||
cmd.Env = env
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("NODE_PATH set to: %s\n", nodePath)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-core: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-core (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-core output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
@@ -37,11 +37,16 @@ var (
|
||||
|
||||
func InitializeGlobalConfig(cfg *GlobalConfig) error {
|
||||
if cfg.ConfigPath == "" {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home directory: %w", err)
|
||||
// Check CLINE_DIR environment variable first
|
||||
if clineDir := os.Getenv("CLINE_DIR"); clineDir != "" {
|
||||
cfg.ConfigPath = clineDir
|
||||
} else {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get home directory: %w", err)
|
||||
}
|
||||
cfg.ConfigPath = filepath.Join(homeDir, ".cline")
|
||||
}
|
||||
cfg.ConfigPath = filepath.Join(homeDir, ".cline")
|
||||
}
|
||||
|
||||
// Ensure .cline directory exists
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
//go:build !windows
|
||||
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
func startClineHost(hostPort int, workspaces []string) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-host on port %d\n", hostPort)
|
||||
}
|
||||
|
||||
// Get the directory where the cline binary is located
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
binDir := filepath.Dir(execPath)
|
||||
clineHostPath := filepath.Join(binDir, "cline-host")
|
||||
|
||||
// Build command arguments
|
||||
args := []string{
|
||||
"--verbose",
|
||||
"--port", fmt.Sprintf("%d", hostPort),
|
||||
}
|
||||
|
||||
for _, ws := range workspaces {
|
||||
args = append(args, "--workspace", ws)
|
||||
}
|
||||
|
||||
// Start the cline-host process
|
||||
cmd := exec.Command(clineHostPath, args...)
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := filepath.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-host-%s-localhost-%d.log", timestamp, hostPort)
|
||||
logFilePath := filepath.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-host: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-host (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-host output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// KillInstanceByAddress kills a Cline instance by its address
|
||||
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, address string) error {
|
||||
// Check if the instance exists in the registry
|
||||
_, err := registry.GetInstance(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("instance %s not found in registry", address)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Killing instance: %s\n", address)
|
||||
}
|
||||
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get process info for instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Terminating process PID %d...\n", pid)
|
||||
}
|
||||
|
||||
// Kill the process
|
||||
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
||||
return fmt.Errorf("failed to kill process %d: %w", pid, err)
|
||||
}
|
||||
|
||||
// Wait for the instance to remove itself from registry
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Waiting for instance to clean up registry entry...\n")
|
||||
}
|
||||
for range 5 {
|
||||
time.Sleep(1 * time.Second)
|
||||
if !registry.HasInstanceAtAddress(address) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Instance %s successfully killed and removed from registry.\n", address)
|
||||
}
|
||||
|
||||
// Update default instance if needed
|
||||
instances, err := registry.ListInstancesCleaned(ctx)
|
||||
if err == nil && len(instances) > 0 {
|
||||
// ensureDefaultInstance logic will handle setting a new default
|
||||
defaultInstance := registry.GetDefaultInstance()
|
||||
if defaultInstance == address || defaultInstance == "" {
|
||||
if len(instances) > 0 {
|
||||
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("instance killed but failed to remove itself from registry within 5 seconds")
|
||||
}
|
||||
|
||||
func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-core on port %d (with hostbridge on %d)\n", corePort, hostPort)
|
||||
}
|
||||
|
||||
// Get the executable path and resolve symlinks (for npm global installs)
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Resolve symlinks to get the real path
|
||||
// For npm global installs, execPath might be a symlink like:
|
||||
// /opt/homebrew/bin/cline -> /opt/homebrew/lib/node_modules/cline/bin/cline
|
||||
realPath, err := filepath.EvalSymlinks(execPath)
|
||||
if err != nil {
|
||||
// If we can't resolve symlinks, fall back to the original path
|
||||
realPath = execPath
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Warning: Could not resolve symlinks for %s: %v\n", execPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
binDir := filepath.Dir(realPath)
|
||||
installDir := filepath.Dir(binDir)
|
||||
clineCorePath := filepath.Join(installDir, "cline-core.js")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Executable path: %s\n", execPath)
|
||||
if realPath != execPath {
|
||||
fmt.Printf("Real path (after resolving symlinks): %s\n", realPath)
|
||||
}
|
||||
fmt.Printf("Bin directory: %s\n", binDir)
|
||||
fmt.Printf("Install directory: %s\n", installDir)
|
||||
fmt.Printf("Looking for cline-core.js at: %s\n", clineCorePath)
|
||||
}
|
||||
|
||||
// Check if cline-core.js exists at the primary location
|
||||
var finalClineCorePath string
|
||||
var finalInstallDir string
|
||||
if _, err := os.Stat(clineCorePath); os.IsNotExist(err) {
|
||||
// Development mode: Try ../../dist-standalone/cline-core.js
|
||||
// This handles the case where we're running from cli/bin/cline
|
||||
devClineCorePath := filepath.Join(binDir, "..", "dist-standalone", "cline-core.js")
|
||||
devInstallDir := filepath.Join(binDir, "..", "dist-standalone")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Primary location not found, trying development path: %s\n", devClineCorePath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(devClineCorePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("cline-core.js not found at '%s' or '%s'. Please ensure you're running from the correct location or reinstall with 'npm install -g cline'", clineCorePath, devClineCorePath)
|
||||
}
|
||||
|
||||
finalClineCorePath = devClineCorePath
|
||||
finalInstallDir = devInstallDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using development mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
} else {
|
||||
finalClineCorePath = clineCorePath
|
||||
finalInstallDir = installDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using production mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := filepath.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-core-%s-localhost-%d.log", timestamp, corePort)
|
||||
logFilePath := filepath.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Start the cline-core process with --config flag using system node
|
||||
args := []string{finalClineCorePath,
|
||||
"--port", fmt.Sprintf("%d", corePort),
|
||||
"--host-bridge-port", fmt.Sprintf("%d", hostPort),
|
||||
"--config", Config.ConfigPath}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using system node\n")
|
||||
}
|
||||
|
||||
cmd := exec.Command("node", args...)
|
||||
|
||||
// Set working directory to installation root
|
||||
cmd.Dir = finalInstallDir
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
// Set environment variables with NODE_PATH for both real and fake node_modules
|
||||
// The fake node_modules contains the vscode stub that can't be in the real node_modules
|
||||
env := os.Environ()
|
||||
realNodeModules := filepath.Join(finalInstallDir, "node_modules")
|
||||
fakeNodeModules := filepath.Join(finalInstallDir, "fake_node_modules")
|
||||
nodePath := fmt.Sprintf("%s%c%s", realNodeModules, os.PathListSeparator, fakeNodeModules)
|
||||
|
||||
env = append(env,
|
||||
fmt.Sprintf("NODE_PATH=%s", nodePath),
|
||||
// These control gRPC debug logging
|
||||
//"GRPC_TRACE=all",
|
||||
//"GRPC_VERBOSITY=DEBUG",
|
||||
"NODE_ENV=development",
|
||||
)
|
||||
cmd.Env = env
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("NODE_PATH set to: %s\n", nodePath)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-core: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-core (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-core output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
//go:build windows
|
||||
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
func startClineHost(hostPort int, workspaces []string) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-host on port %d\n", hostPort)
|
||||
}
|
||||
|
||||
// Get the directory where the cline binary is located
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
binDir := filepath.Dir(execPath)
|
||||
clineHostPath := filepath.Join(binDir, "cline-host.exe")
|
||||
|
||||
// Build command arguments
|
||||
args := []string{
|
||||
"--verbose",
|
||||
"--port", fmt.Sprintf("%d", hostPort),
|
||||
}
|
||||
|
||||
for _, ws := range workspaces {
|
||||
args = append(args, "--workspace", ws)
|
||||
}
|
||||
|
||||
// Start the cline-host process
|
||||
cmd := exec.Command(clineHostPath, args...)
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := filepath.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-host-%s-localhost-%d.log", timestamp, hostPort)
|
||||
logFilePath := filepath.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-host: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-host (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-host output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// KillInstanceByAddress kills a Cline instance by its address
|
||||
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, address string) error {
|
||||
// Check if the instance exists in the registry
|
||||
_, err := registry.GetInstance(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("instance %s not found in registry", address)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Killing instance: %s\n", address)
|
||||
}
|
||||
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get process info for instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Terminating process PID %d...\n", pid)
|
||||
}
|
||||
|
||||
// Find and kill the process using os.Process
|
||||
// On Windows, os.Process.Kill() properly calls TerminateProcess with the correct handle
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find process %d: %w", pid, err)
|
||||
}
|
||||
|
||||
if err := process.Kill(); err != nil {
|
||||
return fmt.Errorf("failed to kill process %d: %w", pid, err)
|
||||
}
|
||||
|
||||
// Wait for the instance to remove itself from registry
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Waiting for instance to clean up registry entry...\n")
|
||||
}
|
||||
for range 5 {
|
||||
time.Sleep(1 * time.Second)
|
||||
if !registry.HasInstanceAtAddress(address) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Instance %s successfully killed and removed from registry.\n", address)
|
||||
}
|
||||
|
||||
// Update default instance if needed
|
||||
instances, err := registry.ListInstancesCleaned(ctx)
|
||||
if err == nil && len(instances) > 0 {
|
||||
// ensureDefaultInstance logic will handle setting a new default
|
||||
defaultInstance := registry.GetDefaultInstance()
|
||||
if defaultInstance == address || defaultInstance == "" {
|
||||
if len(instances) > 0 {
|
||||
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("instance killed but failed to remove itself from registry within 5 seconds")
|
||||
}
|
||||
|
||||
func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-core on port %d (with hostbridge on %d)\n", corePort, hostPort)
|
||||
}
|
||||
|
||||
// Get the executable path and resolve symlinks (for npm global installs)
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Resolve symlinks to get the real path
|
||||
// For npm global installs, execPath might be a symlink like:
|
||||
// /opt/homebrew/bin/cline -> /opt/homebrew/lib/node_modules/cline/bin/cline
|
||||
realPath, err := filepath.EvalSymlinks(execPath)
|
||||
if err != nil {
|
||||
// If we can't resolve symlinks, fall back to the original path
|
||||
realPath = execPath
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Warning: Could not resolve symlinks for %s: %v\n", execPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
binDir := filepath.Dir(realPath)
|
||||
installDir := filepath.Dir(binDir)
|
||||
clineCorePath := filepath.Join(installDir, "cline-core.js")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Executable path: %s\n", execPath)
|
||||
if realPath != execPath {
|
||||
fmt.Printf("Real path (after resolving symlinks): %s\n", realPath)
|
||||
}
|
||||
fmt.Printf("Bin directory: %s\n", binDir)
|
||||
fmt.Printf("Install directory: %s\n", installDir)
|
||||
fmt.Printf("Looking for cline-core.js at: %s\n", clineCorePath)
|
||||
}
|
||||
|
||||
// Check if cline-core.js exists at the primary location
|
||||
var finalClineCorePath string
|
||||
var finalInstallDir string
|
||||
if _, err := os.Stat(clineCorePath); os.IsNotExist(err) {
|
||||
// Development mode: Try ../../dist-standalone/cline-core.js
|
||||
// This handles the case where we're running from cli/bin/cline
|
||||
devClineCorePath := filepath.Join(binDir, "..", "dist-standalone", "cline-core.js")
|
||||
devInstallDir := filepath.Join(binDir, "..", "dist-standalone")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Primary location not found, trying development path: %s\n", devClineCorePath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(devClineCorePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("cline-core.js not found at '%s' or '%s'. Please ensure you're running from the correct location or reinstall with 'npm install -g cline'", clineCorePath, devClineCorePath)
|
||||
}
|
||||
|
||||
finalClineCorePath = devClineCorePath
|
||||
finalInstallDir = devInstallDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using development mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
} else {
|
||||
finalClineCorePath = clineCorePath
|
||||
finalInstallDir = installDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using production mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := filepath.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-core-%s-localhost-%d.log", timestamp, corePort)
|
||||
logFilePath := filepath.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Start the cline-core process with --config flag using system node
|
||||
args := []string{finalClineCorePath,
|
||||
"--port", fmt.Sprintf("%d", corePort),
|
||||
"--host-bridge-port", fmt.Sprintf("%d", hostPort),
|
||||
"--config", Config.ConfigPath}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using system node\n")
|
||||
}
|
||||
|
||||
cmd := exec.Command("node", args...)
|
||||
|
||||
// Set working directory to installation root
|
||||
cmd.Dir = finalInstallDir
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
}
|
||||
|
||||
// Set environment variables with NODE_PATH for both real and fake node_modules
|
||||
// The fake node_modules contains the vscode stub that can't be in the real node_modules
|
||||
env := os.Environ()
|
||||
realNodeModules := filepath.Join(finalInstallDir, "node_modules")
|
||||
fakeNodeModules := filepath.Join(finalInstallDir, "fake_node_modules")
|
||||
nodePath := fmt.Sprintf("%s%c%s", realNodeModules, os.PathListSeparator, fakeNodeModules)
|
||||
|
||||
env = append(env,
|
||||
fmt.Sprintf("NODE_PATH=%s", nodePath),
|
||||
// These control gRPC debug logging
|
||||
//"GRPC_TRACE=all",
|
||||
//"GRPC_VERBOSITY=DEBUG",
|
||||
"NODE_ENV=development",
|
||||
)
|
||||
cmd.Env = env
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("NODE_PATH set to: %s\n", nodePath)
|
||||
fmt.Printf("Attempting to run command: %s\n", cmd)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-core: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-core (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-core output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
@@ -94,6 +94,8 @@ func (h *SayHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
return h.handleHookStatus(msg, dc)
|
||||
case string(types.SayTypeHookOutputStream):
|
||||
return h.handleHookOutputStream(msg, dc)
|
||||
case string(types.SayTypeCommandPermissionDenied):
|
||||
return h.handleCommandPermissionDenied(msg, dc)
|
||||
default:
|
||||
return h.handleDefault(msg, dc)
|
||||
}
|
||||
@@ -346,6 +348,18 @@ func (h *SayHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayCon
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SayHandler) handleCommandPermissionDenied(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
if msg.Text == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use unified ToolRenderer
|
||||
rendered := dc.ToolRenderer.RenderCommandPermissionDenied(msg.Text)
|
||||
output.Print(rendered)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SayHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext) error {
|
||||
var tool types.ToolMessage
|
||||
if err := json.Unmarshal([]byte(msg.Text), &tool); err != nil {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
@@ -247,28 +246,6 @@ type killResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func killInstanceProcess(ctx context.Context, registry *global.ClientRegistry, address string) killResult {
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
|
||||
// Kill the process
|
||||
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
||||
return killResult{address: address, pid: pid, err: err}
|
||||
}
|
||||
|
||||
return killResult{address: address, pid: pid, err: nil}
|
||||
}
|
||||
|
||||
func newInstanceListCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build !windows
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"syscall"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
func killInstanceProcess(ctx context.Context, registry *global.ClientRegistry, address string) killResult {
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
|
||||
// Kill the process
|
||||
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
||||
return killResult{address: address, pid: pid, err: err}
|
||||
}
|
||||
|
||||
return killResult{address: address, pid: pid, err: nil}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//go:build windows
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
func killInstanceProcess(ctx context.Context, registry *global.ClientRegistry, address string) killResult {
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
|
||||
// Find the process by PID
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
// Process may already be dead
|
||||
return killResult{address: address, pid: pid, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
// Kill the process - on Windows, os.Process.Kill() calls TerminateProcess internally
|
||||
if err := process.Kill(); err != nil {
|
||||
return killResult{address: address, pid: pid, err: fmt.Errorf("failed to terminate process: %w", err)}
|
||||
}
|
||||
|
||||
return killResult{address: address, pid: pid, err: nil}
|
||||
}
|
||||
@@ -992,6 +992,15 @@ func (m *Manager) processStateUpdate(stateUpdate *cline.State, coordinator *Stre
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeCommandPermissionDenied):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
fmt.Println()
|
||||
m.displayMessage(msg, false, false, i)
|
||||
|
||||
coordinator.MarkProcessedInCurrentTurn(msgKey)
|
||||
}
|
||||
|
||||
case msg.Say == string(types.SayTypeBrowserActionLaunch):
|
||||
msgKey := fmt.Sprintf("%d", msg.Timestamp)
|
||||
if !coordinator.IsProcessedInCurrentTurn(msgKey) {
|
||||
|
||||
@@ -89,8 +89,9 @@ const (
|
||||
SayTypeTaskProgress SayType = "task_progress"
|
||||
// Hook status streaming from the backend.
|
||||
// These values must match the backend "say" strings emitted by the extension.
|
||||
SayTypeHookStatus SayType = "hook_status"
|
||||
SayTypeHookOutputStream SayType = "hook_output_stream"
|
||||
SayTypeHookStatus SayType = "hook_status"
|
||||
SayTypeHookOutputStream SayType = "hook_output_stream"
|
||||
SayTypeCommandPermissionDenied SayType = "command_permission_denied"
|
||||
)
|
||||
|
||||
// ToolMessage represents a tool-related message
|
||||
@@ -368,6 +369,8 @@ func convertProtoSayType(sayType cline.ClineSay) string {
|
||||
return string(SayTypeHookStatus)
|
||||
case cline.ClineSay_HOOK_OUTPUT_STREAM:
|
||||
return string(SayTypeHookOutputStream)
|
||||
case cline.ClineSay_COMMAND_PERMISSION_DENIED:
|
||||
return string(SayTypeCommandPermissionDenied)
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
+14
-11
@@ -8,6 +8,19 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// VersionString returns the full version information string
|
||||
func VersionString() string {
|
||||
return fmt.Sprintf(`Cline CLI
|
||||
Cline CLI Version: %s
|
||||
Cline Core Version: %s
|
||||
Commit: %s
|
||||
Built: %s
|
||||
Built by: %s
|
||||
Go version: %s
|
||||
OS/Arch: %s/%s
|
||||
`, global.CliVersion, global.Version, global.Commit, global.Date, global.BuiltBy, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
// NewVersionCommand creates the version command
|
||||
func NewVersionCommand() *cobra.Command {
|
||||
var short bool
|
||||
@@ -18,21 +31,11 @@ func NewVersionCommand() *cobra.Command {
|
||||
Short: "Show version information",
|
||||
Long: `Display version information for the Cline CLI.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Versions are injected at build time via ldflags
|
||||
if short {
|
||||
fmt.Println(global.CliVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Cline CLI\n")
|
||||
fmt.Printf("Cline CLI Version: %s\n", global.CliVersion)
|
||||
fmt.Printf("Cline Core Version: %s\n", global.Version)
|
||||
fmt.Printf("Commit: %s\n", global.Commit)
|
||||
fmt.Printf("Built: %s\n", global.Date)
|
||||
fmt.Printf("Built by: %s\n", global.BuiltBy)
|
||||
fmt.Printf("Go version: %s\n", runtime.Version())
|
||||
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
fmt.Print(VersionString())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -47,7 +47,10 @@ func (s *DiffService) generateDiffID() string {
|
||||
return fmt.Sprintf("diff_%d_%d", os.Getpid(), id)
|
||||
}
|
||||
|
||||
// splitLines splits content into lines, preserving line ending information
|
||||
// splitLines splits content into lines, preserving trailing newlines.
|
||||
// This matches the behavior of JavaScript's String.split("\n"):
|
||||
// - "hello\nworld\n" -> ["hello", "world", ""]
|
||||
// - "hello\nworld" -> ["hello", "world"]
|
||||
func splitLines(content string) []string {
|
||||
if content == "" {
|
||||
return []string{}
|
||||
@@ -65,10 +68,9 @@ func splitLines(content string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last line if it doesn't end with newline
|
||||
if current != "" {
|
||||
lines = append(lines, current)
|
||||
}
|
||||
// Always add the last segment - if content ends with newline, this will be
|
||||
// an empty string which preserves the trailing newline when joined back
|
||||
lines = append(lines, current)
|
||||
|
||||
return lines
|
||||
}
|
||||
@@ -176,9 +178,19 @@ func (s *DiffService) ReplaceText(ctx context.Context, req *proto.ReplaceTextReq
|
||||
endLine = startLine
|
||||
}
|
||||
|
||||
// Check if we're replacing to the end of the document
|
||||
replacingToEnd := endLine >= len(session.lines)
|
||||
|
||||
// Split new content into lines
|
||||
newLines := splitLines(newContent)
|
||||
|
||||
// Remove trailing empty line for proper splicing, BUT only when NOT replacing
|
||||
// to the end of the document. When replacing to the end, keep the trailing
|
||||
// empty string to preserve trailing newlines from the content.
|
||||
if !replacingToEnd && len(newLines) > 0 && newLines[len(newLines)-1] == "" {
|
||||
newLines = newLines[:len(newLines)-1]
|
||||
}
|
||||
|
||||
// Ensure we have enough lines in the current content
|
||||
for len(session.lines) < endLine {
|
||||
session.lines = append(session.lines, "")
|
||||
|
||||
+2
-1
@@ -150,7 +150,7 @@
|
||||
},
|
||||
"features/multiroot-workspace",
|
||||
"features/plan-and-act",
|
||||
"features/web-tools",
|
||||
"features/skills",
|
||||
{
|
||||
"group": "Slash Commands",
|
||||
"pages": [
|
||||
@@ -177,6 +177,7 @@
|
||||
"features/tasks/task-management"
|
||||
]
|
||||
},
|
||||
"features/worktrees",
|
||||
"features/yolo-mode"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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:**
|
||||
|
||||
@@ -48,7 +48,7 @@ These labels match what you see in the Auto Approve menu.
|
||||
| Execute all commands | Run commands marked as requiring approval | Requires “Execute safe commands” |
|
||||
| Use the browser | Allows use of the browser tool for web fetching and searching | Proxy issues can apply |
|
||||
| Use MCP servers | Use MCP tools and access MCP resources | Some servers also have per-tool auto-approve |
|
||||
| Enable notifications | Notifies you about long-running auto-approved commands | Helpful for terminal work |
|
||||
| Enable notifications | Notifies you about long-running auto-approved commands | Accessible directly in the Auto Approve menu |
|
||||
|
||||
<Warning>
|
||||
“Read all files” and “Edit all files” only matter if their base toggle is enabled. They extend access outside your workspace.
|
||||
@@ -92,6 +92,9 @@ These are examples, not guarantees.
|
||||
|
||||
Auto-approved actions can run for a while, especially long terminal commands. If you enable notifications, Cline can notify you when an auto-approved command has been running for a while and may need attention.
|
||||
|
||||
The **Enable notifications** toggle is located at the bottom of the Auto Approve menu, below a separator line. This puts the notification setting right where you manage your auto-approval permissions, making it easy to discover and adjust.
|
||||
|
||||
|
||||
## Recommendations
|
||||
|
||||
A good default setup is:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
---
|
||||
title: "Worktrees"
|
||||
sidebarTitle: "Worktrees"
|
||||
---
|
||||
|
||||
Worktrees let you work on multiple branches simultaneously, each in its own folder. This enables Cline to work on tasks in parallel across separate VS Code windows, or lets Cline work independently while you continue coding in your main workspace.
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/worktrees-overview.png"
|
||||
alt="Worktrees view showing multiple linked worktrees"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
## What Are Git Worktrees?
|
||||
|
||||
A Git worktree is a linked copy of your repository in a separate folder, checked out to a specific branch. All worktrees share the same Git history and `.git` directory, but each has its own working directory with different code checked out.
|
||||
|
||||
Key concepts:
|
||||
- **Main worktree**: Your original repository folder where the `.git` directory lives
|
||||
- **Linked worktrees**: Additional folders you create, each checked out to a different branch
|
||||
- **Shared history**: All worktrees share commits, branches, and Git configuration
|
||||
|
||||
<Tip>
|
||||
Unlike regular branch switching, worktrees let you have multiple branches checked out at the same time in different folders. This means you can have VS Code windows open for different features simultaneously.
|
||||
</Tip>
|
||||
|
||||
## Why Use Worktrees with Cline?
|
||||
|
||||
Worktrees solve a common problem: **Cline takes over your VS Code window while working on a task**. With worktrees, you can:
|
||||
|
||||
1. **Run Cline in parallel** - Have Cline work on multiple tasks simultaneously, each in its own worktree and VS Code window
|
||||
2. **Keep working while Cline works** - Let Cline handle a task in a separate worktree while you continue coding in your main workspace
|
||||
3. **Isolate experimental changes** - Test risky changes in a worktree without affecting your main branch
|
||||
4. **Quick context switching** - Jump between features without stashing or committing incomplete work
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Quick Launch (Recommended)
|
||||
|
||||
The fastest way to start using worktrees is the **New Worktree Window** button on Cline's home screen:
|
||||
|
||||
1. Click **New Worktree Window** on the home screen
|
||||
2. Enter a branch name and folder path (defaults are auto-filled)
|
||||
3. Click **Create & Open**
|
||||
|
||||
A new VS Code window opens with your worktree, and Cline automatically opens ready to work.
|
||||
|
||||
<Tip>
|
||||
The home screen also shows your current branch and worktree path. Click it to open the full Worktrees view.
|
||||
</Tip>
|
||||
|
||||
### Full Worktrees View
|
||||
|
||||
For more control, open the full Worktrees view by clicking the **Worktrees** button in the Cline sidebar header, or by clicking your current branch info on the home screen:
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a New Worktree">
|
||||
Click **New Worktree** at the bottom of the view. Enter a branch name and path (defaults are auto-filled).
|
||||
</Step>
|
||||
<Step title="Open in New Window">
|
||||
Once created, click the **Open in new window** button to open the worktree in a separate VS Code window. Cline will automatically open in the new window.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Typical Workflow
|
||||
|
||||
Here's how a typical worktree session looks:
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a new worktree">
|
||||
Click **New Worktree Window** on the home screen or use the Worktrees view. A new VS Code window opens with Cline ready to go.
|
||||
</Step>
|
||||
<Step title="Do your work">
|
||||
Work on your feature or let Cline handle a task. Make commits as you go.
|
||||
</Step>
|
||||
<Step title="Close the worktree window">
|
||||
When you're done, close the worktree's VS Code window.
|
||||
</Step>
|
||||
<Step title="Merge from your primary worktree">
|
||||
Back in your main VS Code window, open the Worktrees view and click the **merge button** on the worktree you just worked in. This merges the branch and optionally deletes the worktree.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Managing Worktrees
|
||||
|
||||
### Viewing Worktrees
|
||||
|
||||
The Worktrees view shows all worktrees for your repository:
|
||||
|
||||
- **Current**: The worktree you're currently in (highlighted)
|
||||
- **Main**: The primary worktree where your `.git` directory lives (cannot be deleted)
|
||||
- **Locked**: Worktrees that are locked to prevent accidental deletion
|
||||
|
||||
### Opening Worktrees
|
||||
|
||||
Each worktree has two open options:
|
||||
- **Open in current window**: Replace your current workspace with the worktree
|
||||
- **Open in new window**: Open the worktree in a separate VS Code window (recommended for parallel Cline sessions)
|
||||
|
||||
Either way, Cline automatically opens in the new workspace, ready to start a task.
|
||||
|
||||
### Deleting Worktrees
|
||||
|
||||
Click the trash icon on any linked worktree to delete it. A confirmation dialog will show you exactly what will be deleted:
|
||||
- The branch itself
|
||||
- All project files in the worktree folder
|
||||
|
||||
<Warning>
|
||||
Deleting a worktree permanently removes the branch and all files in that folder. Make sure any important changes are committed and pushed first.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
You cannot delete the main worktree. It's the primary repository where your `.git` directory lives.
|
||||
</Note>
|
||||
|
||||
### Merging Worktrees
|
||||
|
||||
When you're done working in a worktree and ready to merge your changes back to the main branch:
|
||||
|
||||
1. Click the **merge icon** (git merge symbol) on any linked worktree
|
||||
2. Review the merge details in the confirmation modal
|
||||
3. Choose whether to delete the worktree after merging
|
||||
4. Click **Merge**
|
||||
|
||||
<Frame>
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/docs/assets/worktrees-merge.png"
|
||||
alt="Merge worktree modal"
|
||||
/>
|
||||
</Frame>
|
||||
|
||||
#### Handling Merge Conflicts
|
||||
|
||||
If your branch has conflicts with the main branch, Cline will detect them and show you the conflicting files. You have two options:
|
||||
|
||||
1. **Ask Cline to Resolve & Merge** - Creates a new Cline task with a prompt asking Cline to resolve the conflicts, complete the merge, and clean up the worktree
|
||||
2. **Resolve Manually** - Close the modal and resolve conflicts yourself using your preferred Git tools
|
||||
|
||||
<Tip>
|
||||
The "Ask Cline to Resolve" option is particularly useful for complex conflicts. Cline will analyze the conflicting files and attempt to merge them intelligently based on the intent of both branches.
|
||||
</Tip>
|
||||
|
||||
## .worktreeinclude: Automatic File Copying
|
||||
|
||||
When you create a new worktree, it starts with a fresh checkout—no `node_modules`, no build artifacts, no IDE settings. This means you'd normally need to run `npm install` or similar setup commands.
|
||||
|
||||
The `.worktreeinclude` file solves this by automatically copying specified files to new worktrees.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Create a `.worktreeinclude` file in your repository root
|
||||
2. Add glob patterns for files you want copied (using `.gitignore` syntax)
|
||||
3. When Cline creates a new worktree, files matching **both** `.worktreeinclude` **and** `.gitignore` are copied automatically
|
||||
|
||||
<Note>
|
||||
Only files that are both matched by `.worktreeinclude` AND listed in `.gitignore` are copied. This prevents accidentally duplicating tracked files.
|
||||
</Note>
|
||||
|
||||
### Example `.worktreeinclude`
|
||||
|
||||
```gitignore
|
||||
# Copy node_modules to avoid npm install
|
||||
node_modules/
|
||||
|
||||
# Copy IDE settings
|
||||
.vscode/
|
||||
|
||||
# Copy build cache
|
||||
.next/
|
||||
dist/
|
||||
|
||||
# Copy environment files (if gitignored)
|
||||
.env.local
|
||||
```
|
||||
|
||||
### Creating a `.worktreeinclude` File
|
||||
|
||||
The Worktrees view will show a tip if you don't have a `.worktreeinclude` file. If you have a `.gitignore`, you can click **Create from .gitignore** to create one pre-filled with your gitignore contents. Then edit it to keep only the patterns you want copied.
|
||||
|
||||
<Tip>
|
||||
For most JavaScript/TypeScript projects, just including `node_modules/` in your `.worktreeinclude` saves significant setup time for each new worktree.
|
||||
</Tip>
|
||||
|
||||
### Pro Tip: Symlink to .gitignore
|
||||
|
||||
Since `.gitignore` usually contains most of the files you'd want copied to new worktrees (dependencies, environment files, build caches, etc.), you can create a symlink so they stay in sync automatically:
|
||||
|
||||
```bash
|
||||
# In your repository root
|
||||
ln -s .gitignore .worktreeinclude
|
||||
```
|
||||
|
||||
Now whenever you update your `.gitignore`, your `.worktreeinclude` will have the same patterns. This is especially useful for projects where gitignored files are exactly what you want copied—no need to maintain two separate files.
|
||||
|
||||
<Note>
|
||||
If you need different patterns than your `.gitignore`, create a regular `.worktreeinclude` file instead of a symlink.
|
||||
</Note>
|
||||
|
||||
## Best Practices
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="For Parallel Cline Sessions">
|
||||
1. **Create purpose-specific worktrees** - Name branches clearly (e.g., `cline/refactor-auth`, `cline/add-tests`)
|
||||
2. **Open in new windows** - Always use "Open in new window" for true parallelism
|
||||
3. **Use .worktreeinclude** - Set up automatic file copying to reduce setup time
|
||||
</Accordion>
|
||||
<Accordion title="For Solo Development">
|
||||
1. **Keep your main branch clean** - Use worktrees for experimental or risky changes
|
||||
2. **Quick feature switches** - Instead of stashing, create a worktree for interruptions
|
||||
3. **Review in isolation** - Create worktrees to review PRs without disrupting your work
|
||||
</Accordion>
|
||||
<Accordion title="Worktree Hygiene">
|
||||
1. **Delete unused worktrees** - Remove worktrees when their branches are merged
|
||||
2. **Use meaningful names** - Branch names should indicate the worktree's purpose
|
||||
3. **Check for stale worktrees** - Periodically review and clean up old worktrees
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Limitations
|
||||
|
||||
Worktrees are not available in certain workspace configurations:
|
||||
|
||||
- **Multi-root workspaces**: If you have multiple folders open in VS Code, worktrees are disabled. Open a single repository folder instead.
|
||||
- **Subfolder of a repository**: If you've opened a subfolder within a Git repository (not the root), worktrees are disabled. Open the repository root folder instead.
|
||||
|
||||
The Worktrees view will display a message explaining the limitation if either of these applies to your workspace.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Branch already exists error">
|
||||
Git doesn't allow the same branch to be checked out in multiple worktrees. Either:
|
||||
- Use a different branch name
|
||||
- Delete the existing worktree using that branch
|
||||
</Accordion>
|
||||
<Accordion title="Worktree folder already exists">
|
||||
The path you specified already contains files. Choose a different path or delete the existing folder first.
|
||||
</Accordion>
|
||||
<Accordion title="Can't delete worktree">
|
||||
If a worktree is locked, you'll need to unlock it first using `git worktree unlock <path>` in the terminal. If the worktree has uncommitted changes, you may need to use force delete.
|
||||
</Accordion>
|
||||
<Accordion title=".worktreeinclude files not copying">
|
||||
Make sure the files you want copied are:
|
||||
1. Listed in your `.worktreeinclude` file
|
||||
2. Also listed in your `.gitignore` (only gitignored files are copied)
|
||||
3. Actually exist in your current worktree
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Technical Details
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="How Worktrees Work Internally">
|
||||
- Worktrees are a native Git feature (`git worktree` command)
|
||||
- All worktrees share the same `.git` directory and object database
|
||||
- Each worktree has its own index, working directory, and HEAD
|
||||
- Worktree list is stored in `.git/worktrees/`
|
||||
</Accordion>
|
||||
<Accordion title="Storage Considerations">
|
||||
- Each worktree contains a full checkout of the repository
|
||||
- `.worktreeinclude` can significantly increase worktree size (e.g., copying `node_modules`)
|
||||
- Consider your disk space when creating many worktrees
|
||||
</Accordion>
|
||||
<Accordion title="Relationship with Checkpoints">
|
||||
Worktrees are separate from Cline's [checkpoint system](/features/checkpoints). Each worktree has its own checkpoint history. Checkpoints track changes within a single worktree, while worktrees let you work across multiple branches simultaneously.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
Worktrees unlock true parallel development with Cline. Create a worktree, open it in a new window, and let Cline work independently while you continue coding!
|
||||
@@ -16,20 +16,17 @@ Cline supports accessing models directly through the official OpenAI API.
|
||||
|
||||
### Supported Models
|
||||
|
||||
Cline is compatible with a variety of OpenAI models, including but not limited to:
|
||||
Cline is compatible with a variety of OpenAI models, including common choices from OpenAI's featured/frontier lists:
|
||||
|
||||
- 'o3'
|
||||
- `o3-mini` (medium reasoning effort)
|
||||
- 'o4-mini'
|
||||
- `o3-mini-high` (high reasoning effort)
|
||||
- `o3-mini-low` (low reasoning effort)
|
||||
- `o1`
|
||||
- `o1-preview`
|
||||
- `o1-mini`
|
||||
- `gpt-5.2`
|
||||
- `gpt-5.2-codex`
|
||||
- `gpt-5-mini`
|
||||
- `gpt-5-nano`
|
||||
- `gpt-4.1`
|
||||
- `gpt-4o`
|
||||
- `gpt-4o-mini`
|
||||
- 'gpt-4.1'
|
||||
- 'gpt-4.1-mini'
|
||||
- `o3`
|
||||
- `o4-mini`
|
||||
|
||||
For the most current list of available models and their capabilities, please refer to the official [OpenAI Models documentation](https://platform.openai.com/docs/models).
|
||||
|
||||
|
||||
Generated
+6138
-3552
File diff suppressed because it is too large
Load Diff
+27
-6
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.47.0",
|
||||
"version": "3.51.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -339,18 +339,33 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:all-surfaces",
|
||||
"build:vscode": "npx tsx scripts/build.ts --surface=vscode",
|
||||
"build:vscode:prod": "npx tsx scripts/build.ts --surface=vscode --prod",
|
||||
"build:jetbrains": "npx tsx scripts/build.ts --surface=jetbrains",
|
||||
"build:jetbrains:prod": "npx tsx scripts/build.ts --surface=jetbrains --prod",
|
||||
"build:cli:unix": "npx tsx scripts/build.ts --surface=cli --platform=unix",
|
||||
"build:cli:windows": "npx tsx scripts/build.ts --surface=cli --platform=windows",
|
||||
"build:cli:all-platforms": "npx tsx scripts/build.ts --surface=cli --platform=all",
|
||||
"build:cli:unix:prod": "npx tsx scripts/build.ts --surface=cli --platform=unix --prod",
|
||||
"build:cli:windows:prod": "npx tsx scripts/build.ts --surface=cli --platform=windows --prod",
|
||||
"build:cli:all-platforms:prod": "npx tsx scripts/build.ts --surface=cli --platform=all --prod",
|
||||
"build:all-surfaces": "npx tsx scripts/build.ts --surface=all",
|
||||
"build:all-surfaces:prod": "npx tsx scripts/build.ts --surface=all --prod",
|
||||
"build:all-surfaces:all-platforms": "npx tsx scripts/build.ts --surface=all --platform=all",
|
||||
"build:all-surfaces:all-platforms:prod": "npx tsx scripts/build.ts --surface=all --platform=all --prod",
|
||||
"build:all-surfaces:all-platforms:all-stages": "npx tsx scripts/build.ts --surface=all --platform=all --all-stages",
|
||||
"build:npm": "npx tsx scripts/build.ts --surface=npm",
|
||||
"build:npm:prod": "npx tsx scripts/build.ts --surface=npm --prod",
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.mjs",
|
||||
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
|
||||
"compile-standalone-npm": "npm run protos && npm run protos-go && npm run check-types && npm run lint && node esbuild.mjs --standalone",
|
||||
"compile-cli": "scripts/build-cli.sh",
|
||||
"compile-cli-all-platforms": "scripts/build-cli-all-platforms.sh",
|
||||
"compile-cli-man-page": "pandoc cli/man/cline.1.md -s -t man -o cli/man/cline.1",
|
||||
"build:npm": "scripts/build-npm-package.sh",
|
||||
"test:install": "bash scripts/test-install.sh",
|
||||
"dev:cli:watch": "node scripts/dev-cli-watch.mjs",
|
||||
"postcompile-standalone": "node scripts/package-standalone.mjs",
|
||||
"postcompile-standalone-npm": "node scripts/package-standalone.mjs --target=npm",
|
||||
"postcompile-standalone-npm": "node scripts/package-npm.mjs",
|
||||
"dev": "npm run protos && npm run watch",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.mjs --watch",
|
||||
@@ -404,6 +419,10 @@
|
||||
"storybook": "cd webview-ui && npm run storybook"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/shared/storage/state-keys.ts": [
|
||||
"node scripts/generate-state-proto.mjs",
|
||||
"git add proto/cline/state.proto"
|
||||
],
|
||||
"*": [
|
||||
"biome check --write --staged --no-errors-on-unmatched --files-ignore-unknown=true"
|
||||
]
|
||||
@@ -417,6 +436,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",
|
||||
@@ -495,7 +515,7 @@
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.12.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"chrome-launcher": "^1.1.2",
|
||||
@@ -528,6 +548,7 @@
|
||||
"p-timeout": "^6.1.4",
|
||||
"p-wait-for": "^5.0.2",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"picomatch": "^4.0.3",
|
||||
"posthog-node": "^5.8.0",
|
||||
"puppeteer-chromium-resolver": "^23.0.0",
|
||||
"puppeteer-core": "^23.4.0",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ service ModelsService {
|
||||
rpc refreshOcaModels(StringRequest) returns (OcaCompatibleModelInfo);
|
||||
// Fetches available models from AIhubmix
|
||||
rpc getAihubmixModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
// Refreshes and returns Vercel AI Gateway models
|
||||
rpc refreshVercelAiGatewayModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
|
||||
}
|
||||
|
||||
// List of VS Code LM models
|
||||
|
||||
+220
-169
@@ -54,182 +54,230 @@ message AutoApprovalSettings {
|
||||
optional bool enable_notifications = 3;
|
||||
}
|
||||
|
||||
// NOTE: Add the new secret fields under SECRETS_KEYS in src/shared/storage/state-keys.ts
|
||||
// and use the scripts/generate-state-proto.mjs script to regenerate this list.
|
||||
message Secrets {
|
||||
optional string api_key = 1;
|
||||
optional string open_router_api_key = 4;
|
||||
optional string aws_access_key = 5;
|
||||
optional string aws_secret_key = 6;
|
||||
optional string aws_session_token = 7;
|
||||
optional string aws_bedrock_api_key = 8;
|
||||
optional string open_ai_api_key = 9;
|
||||
optional string gemini_api_key = 10;
|
||||
optional string open_ai_native_api_key = 11;
|
||||
optional string ollama_api_key = 12;
|
||||
optional string deep_seek_api_key = 13;
|
||||
optional string requesty_api_key = 14;
|
||||
optional string together_api_key = 15;
|
||||
optional string fireworks_api_key = 16;
|
||||
optional string qwen_api_key = 17;
|
||||
optional string doubao_api_key = 18;
|
||||
optional string mistral_api_key = 19;
|
||||
optional string lite_llm_api_key = 20;
|
||||
optional string auth_nonce = 21;
|
||||
optional string asksage_api_key = 22;
|
||||
optional string xai_api_key = 23;
|
||||
optional string moonshot_api_key = 24;
|
||||
optional string zai_api_key = 25;
|
||||
optional string hugging_face_api_key = 26;
|
||||
optional string nebius_api_key = 27;
|
||||
optional string sambanova_api_key = 28;
|
||||
optional string cerebras_api_key = 29;
|
||||
optional string sap_ai_core_client_id = 30;
|
||||
optional string sap_ai_core_client_secret = 31;
|
||||
optional string groq_api_key = 32;
|
||||
optional string huawei_cloud_maas_api_key = 33;
|
||||
optional string baseten_api_key = 34;
|
||||
optional string vercel_ai_gateway_api_key = 35;
|
||||
optional string dify_api_key = 36;
|
||||
optional string oca_api_key = 37;
|
||||
optional string oca_refresh_token = 38;
|
||||
optional string hicap_api_key = 39;
|
||||
optional string mcp_oauth_secrets = 40;
|
||||
optional string cline_account_id = 2;
|
||||
optional string open_router_api_key = 3;
|
||||
optional string aws_access_key = 4;
|
||||
optional string aws_secret_key = 5;
|
||||
optional string aws_session_token = 6;
|
||||
optional string aws_bedrock_api_key = 7;
|
||||
optional string open_ai_api_key = 8;
|
||||
optional string gemini_api_key = 9;
|
||||
optional string open_ai_native_api_key = 10;
|
||||
optional string ollama_api_key = 11;
|
||||
optional string deep_seek_api_key = 12;
|
||||
optional string requesty_api_key = 13;
|
||||
optional string together_api_key = 14;
|
||||
optional string fireworks_api_key = 15;
|
||||
optional string qwen_api_key = 16;
|
||||
optional string doubao_api_key = 17;
|
||||
optional string mistral_api_key = 18;
|
||||
optional string lite_llm_api_key = 19;
|
||||
optional string auth_nonce = 20;
|
||||
optional string asksage_api_key = 21;
|
||||
optional string xai_api_key = 22;
|
||||
optional string moonshot_api_key = 23;
|
||||
optional string zai_api_key = 24;
|
||||
optional string hugging_face_api_key = 25;
|
||||
optional string nebius_api_key = 26;
|
||||
optional string sambanova_api_key = 27;
|
||||
optional string cerebras_api_key = 28;
|
||||
optional string sap_ai_core_client_id = 29;
|
||||
optional string sap_ai_core_client_secret = 30;
|
||||
optional string groq_api_key = 31;
|
||||
optional string huawei_cloud_maas_api_key = 32;
|
||||
optional string baseten_api_key = 33;
|
||||
optional string vercel_ai_gateway_api_key = 34;
|
||||
optional string dify_api_key = 35;
|
||||
optional string minimax_api_key = 36;
|
||||
optional string hicap_api_key = 37;
|
||||
optional string aihubmix_api_key = 38;
|
||||
optional string nous_research_api_key = 39;
|
||||
optional string remote_lite_llm_api_key = 40;
|
||||
optional string oca_api_key = 41;
|
||||
optional string oca_refresh_token = 42;
|
||||
optional string mcp_o_auth_secrets = 43;
|
||||
}
|
||||
|
||||
// NOTE: Add new fields under API_HANDLER_SETTINGS_FIELDS or USER_SETTINGS_FIELDS
|
||||
// in src/shared/storage/state-keys.ts and use the scripts/generate-state-proto.mjs
|
||||
// script to regenerate this list.
|
||||
message Settings {
|
||||
optional string aws_region = 1;
|
||||
optional bool aws_use_cross_region_inference = 2;
|
||||
optional bool aws_bedrock_use_prompt_cache = 3;
|
||||
optional string aws_bedrock_endpoint = 4;
|
||||
optional string aws_profile = 5;
|
||||
optional string aws_authentication = 6;
|
||||
optional bool aws_use_profile = 7;
|
||||
optional string vertex_project_id = 8;
|
||||
optional string vertex_region = 9;
|
||||
optional string requesty_base_url = 10;
|
||||
optional string open_ai_base_url = 11;
|
||||
// map<string, string> open_ai_headers = 12;
|
||||
optional string ollama_base_url = 13;
|
||||
optional string ollama_api_options_ctx_num = 14;
|
||||
optional string lm_studio_base_url = 15;
|
||||
optional string lm_studio_max_tokens = 16;
|
||||
optional string anthropic_base_url = 17;
|
||||
optional string gemini_base_url = 18;
|
||||
optional string azure_api_version = 19;
|
||||
optional string open_router_provider_sorting = 20;
|
||||
optional AutoApprovalSettings auto_approval_settings = 21;
|
||||
optional BrowserSettings browser_settings = 24;
|
||||
optional string lite_llm_base_url = 25;
|
||||
optional bool lite_llm_use_prompt_cache = 26;
|
||||
optional int32 fireworks_model_max_completion_tokens = 27;
|
||||
optional int32 fireworks_model_max_tokens = 28;
|
||||
optional string lite_llm_base_url = 1;
|
||||
optional bool lite_llm_use_prompt_cache = 2;
|
||||
map<string, string> open_ai_headers = 3;
|
||||
optional string anthropic_base_url = 4;
|
||||
optional string open_router_provider_sorting = 5;
|
||||
optional string aws_region = 6;
|
||||
optional bool aws_use_cross_region_inference = 7;
|
||||
optional bool aws_use_global_inference = 8;
|
||||
optional bool aws_bedrock_use_prompt_cache = 9;
|
||||
optional string aws_authentication = 10;
|
||||
optional bool aws_use_profile = 11;
|
||||
optional string aws_profile = 12;
|
||||
optional string aws_bedrock_endpoint = 13;
|
||||
optional string claude_code_path = 14;
|
||||
optional string vertex_project_id = 15;
|
||||
optional string vertex_region = 16;
|
||||
optional string open_ai_base_url = 17;
|
||||
optional string ollama_base_url = 18;
|
||||
optional string ollama_api_options_ctx_num = 19;
|
||||
optional string lm_studio_base_url = 20;
|
||||
optional string lm_studio_max_tokens = 21;
|
||||
optional string gemini_base_url = 22;
|
||||
optional string requesty_base_url = 23;
|
||||
optional int32 fireworks_model_max_completion_tokens = 24;
|
||||
optional int32 fireworks_model_max_tokens = 25;
|
||||
optional string qwen_code_oauth_path = 26;
|
||||
optional string azure_api_version = 27;
|
||||
optional bool azure_identity = 28;
|
||||
optional string qwen_api_line = 29;
|
||||
optional string moonshot_api_line = 30;
|
||||
optional string zai_api_line = 31;
|
||||
optional string telemetry_setting = 32;
|
||||
optional string asksage_api_url = 33;
|
||||
optional bool plan_act_separate_models_setting = 34;
|
||||
optional bool enable_checkpoints_setting = 35;
|
||||
optional int32 request_timeout_ms = 36;
|
||||
optional int32 shell_integration_timeout = 37;
|
||||
optional string default_terminal_profile = 38;
|
||||
optional int32 terminal_output_line_limit = 39;
|
||||
optional string sap_ai_core_token_url = 40;
|
||||
optional string sap_ai_core_base_url = 41;
|
||||
optional string sap_ai_resource_group = 42;
|
||||
optional bool sap_ai_core_use_orchestration_mode = 43;
|
||||
optional string claude_code_path = 44;
|
||||
optional string qwen_code_oauth_path = 45;
|
||||
optional bool strict_plan_mode_enabled = 46;
|
||||
optional bool yolo_mode_toggled = 47;
|
||||
optional bool use_auto_condense = 48;
|
||||
optional string preferred_language = 49;
|
||||
optional OpenaiReasoningEffort openai_reasoning_effort = 50;
|
||||
optional PlanActMode mode = 51;
|
||||
optional DictationSettings dictation_settings = 52;
|
||||
optional FocusChainSettings focus_chain_settings = 53;
|
||||
optional string custom_prompt = 54;
|
||||
optional string dify_base_url = 55;
|
||||
optional double auto_condense_threshold = 56;
|
||||
optional string oca_base_url = 57;
|
||||
optional ApiProvider plan_mode_api_provider = 58;
|
||||
optional string plan_mode_api_model_id = 59;
|
||||
optional int64 plan_mode_thinking_budget_tokens = 60;
|
||||
optional string plan_mode_reasoning_effort = 61;
|
||||
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 62;
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 63;
|
||||
optional string plan_mode_aws_bedrock_custom_model_base_id = 64;
|
||||
optional string plan_mode_open_router_model_id = 65;
|
||||
optional OpenRouterModelInfo plan_mode_open_router_model_info = 66;
|
||||
optional string plan_mode_open_ai_model_id = 67;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 68;
|
||||
optional string plan_mode_ollama_model_id = 69;
|
||||
optional string plan_mode_lm_studio_model_id = 70;
|
||||
optional string plan_mode_lite_llm_model_id = 71;
|
||||
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 72;
|
||||
optional string plan_mode_requesty_model_id = 73;
|
||||
optional OpenRouterModelInfo plan_mode_requesty_model_info = 74;
|
||||
optional string plan_mode_together_model_id = 75;
|
||||
optional string plan_mode_fireworks_model_id = 76;
|
||||
optional string plan_mode_sap_ai_core_model_id = 77;
|
||||
optional string plan_mode_sap_ai_core_deployment_id = 78;
|
||||
optional string plan_mode_groq_model_id = 79;
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 80;
|
||||
optional string plan_mode_baseten_model_id = 81;
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 82;
|
||||
optional string plan_mode_hugging_face_model_id = 83;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 84;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 85;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 86;
|
||||
optional string plan_mode_oca_model_id = 87;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 88;
|
||||
optional ApiProvider act_mode_api_provider = 89;
|
||||
optional string act_mode_api_model_id = 90;
|
||||
optional int64 act_mode_thinking_budget_tokens = 91;
|
||||
optional string act_mode_reasoning_effort = 92;
|
||||
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 93;
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 94;
|
||||
optional string act_mode_aws_bedrock_custom_model_base_id = 95;
|
||||
optional string act_mode_open_router_model_id = 96;
|
||||
optional OpenRouterModelInfo act_mode_open_router_model_info = 97;
|
||||
optional string act_mode_open_ai_model_id = 98;
|
||||
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 99;
|
||||
optional string act_mode_ollama_model_id = 100;
|
||||
optional string act_mode_lm_studio_model_id = 101;
|
||||
optional string act_mode_lite_llm_model_id = 102;
|
||||
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 103;
|
||||
optional string act_mode_requesty_model_id = 104;
|
||||
optional OpenRouterModelInfo act_mode_requesty_model_info = 105;
|
||||
optional string act_mode_together_model_id = 106;
|
||||
optional string act_mode_fireworks_model_id = 107;
|
||||
optional string act_mode_sap_ai_core_model_id = 108;
|
||||
optional string act_mode_sap_ai_core_deployment_id = 109;
|
||||
optional string act_mode_groq_model_id = 110;
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 111;
|
||||
optional string act_mode_baseten_model_id = 112;
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 113;
|
||||
optional string act_mode_hugging_face_model_id = 114;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 115;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 116;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 117;
|
||||
optional string plan_mode_vercel_ai_gateway_model_id = 118;
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 119;
|
||||
optional string act_mode_vercel_ai_gateway_model_id = 120;
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 121;
|
||||
optional string act_mode_oca_model_id = 122;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 123;
|
||||
optional int32 max_consecutive_mistakes = 124;
|
||||
optional bool subagents_enabled = 125;
|
||||
optional int32 subagent_terminal_output_line_limit = 126;
|
||||
optional string aihubmix_api_key = 127;
|
||||
optional string aihubmix_base_url = 128;
|
||||
optional string aihubmix_app_code = 129;
|
||||
optional string plan_mode_aihubmix_model_id = 130;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 131;
|
||||
optional string act_mode_aihubmix_model_id = 132;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 133;
|
||||
optional bool cline_web_tools_enabled = 134;
|
||||
optional bool hooks_enabled = 135;
|
||||
optional bool azure_identity = 136;
|
||||
optional string asksage_api_url = 31;
|
||||
optional int32 request_timeout_ms = 32;
|
||||
optional string sap_ai_resource_group = 33;
|
||||
optional string sap_ai_core_token_url = 34;
|
||||
optional string sap_ai_core_base_url = 35;
|
||||
optional bool sap_ai_core_use_orchestration_mode = 36;
|
||||
optional string dify_base_url = 37;
|
||||
optional string zai_api_line = 38;
|
||||
optional string oca_base_url = 39;
|
||||
optional string minimax_api_line = 40;
|
||||
optional string oca_mode = 41;
|
||||
optional string aihubmix_base_url = 42;
|
||||
optional string aihubmix_app_code = 43;
|
||||
optional string plan_mode_api_model_id = 44;
|
||||
optional int64 plan_mode_thinking_budget_tokens = 45;
|
||||
optional string gemini_plan_mode_thinking_level = 46;
|
||||
optional string plan_mode_reasoning_effort = 47;
|
||||
optional string plan_mode_verbosity = 48;
|
||||
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 49;
|
||||
optional bool plan_mode_aws_bedrock_custom_selected = 50;
|
||||
optional string plan_mode_aws_bedrock_custom_model_base_id = 51;
|
||||
optional string plan_mode_open_router_model_id = 52;
|
||||
optional OpenRouterModelInfo plan_mode_open_router_model_info = 53;
|
||||
optional string plan_mode_open_ai_model_id = 54;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 55;
|
||||
optional string plan_mode_ollama_model_id = 56;
|
||||
optional string plan_mode_lm_studio_model_id = 57;
|
||||
optional string plan_mode_lite_llm_model_id = 58;
|
||||
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 59;
|
||||
optional string plan_mode_requesty_model_id = 60;
|
||||
optional OpenRouterModelInfo plan_mode_requesty_model_info = 61;
|
||||
optional string plan_mode_together_model_id = 62;
|
||||
optional string plan_mode_fireworks_model_id = 63;
|
||||
optional string plan_mode_sap_ai_core_model_id = 64;
|
||||
optional string plan_mode_sap_ai_core_deployment_id = 65;
|
||||
optional string plan_mode_groq_model_id = 66;
|
||||
optional OpenRouterModelInfo plan_mode_groq_model_info = 67;
|
||||
optional string plan_mode_baseten_model_id = 68;
|
||||
optional OpenRouterModelInfo plan_mode_baseten_model_info = 69;
|
||||
optional string plan_mode_hugging_face_model_id = 70;
|
||||
optional OpenRouterModelInfo plan_mode_hugging_face_model_info = 71;
|
||||
optional string plan_mode_huawei_cloud_maas_model_id = 72;
|
||||
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 73;
|
||||
optional string plan_mode_oca_model_id = 74;
|
||||
optional OcaModelInfo plan_mode_oca_model_info = 75;
|
||||
optional string plan_mode_oca_reasoning_effort = 76;
|
||||
optional string plan_mode_aihubmix_model_id = 77;
|
||||
optional OpenAiCompatibleModelInfo plan_mode_aihubmix_model_info = 78;
|
||||
optional string plan_mode_hicap_model_id = 79;
|
||||
optional OpenRouterModelInfo plan_mode_hicap_model_info = 80;
|
||||
optional string plan_mode_nous_research_model_id = 81;
|
||||
optional string plan_mode_vercel_ai_gateway_model_id = 82;
|
||||
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 83;
|
||||
optional string act_mode_api_model_id = 84;
|
||||
optional int64 act_mode_thinking_budget_tokens = 85;
|
||||
optional string gemini_act_mode_thinking_level = 86;
|
||||
optional string act_mode_reasoning_effort = 87;
|
||||
optional string act_mode_verbosity = 88;
|
||||
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 89;
|
||||
optional bool act_mode_aws_bedrock_custom_selected = 90;
|
||||
optional string act_mode_aws_bedrock_custom_model_base_id = 91;
|
||||
optional string act_mode_open_router_model_id = 92;
|
||||
optional OpenRouterModelInfo act_mode_open_router_model_info = 93;
|
||||
optional string act_mode_open_ai_model_id = 94;
|
||||
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 95;
|
||||
optional string act_mode_ollama_model_id = 96;
|
||||
optional string act_mode_lm_studio_model_id = 97;
|
||||
optional string act_mode_lite_llm_model_id = 98;
|
||||
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 99;
|
||||
optional string act_mode_requesty_model_id = 100;
|
||||
optional OpenRouterModelInfo act_mode_requesty_model_info = 101;
|
||||
optional string act_mode_together_model_id = 102;
|
||||
optional string act_mode_fireworks_model_id = 103;
|
||||
optional string act_mode_sap_ai_core_model_id = 104;
|
||||
optional string act_mode_sap_ai_core_deployment_id = 105;
|
||||
optional string act_mode_groq_model_id = 106;
|
||||
optional OpenRouterModelInfo act_mode_groq_model_info = 107;
|
||||
optional string act_mode_baseten_model_id = 108;
|
||||
optional OpenRouterModelInfo act_mode_baseten_model_info = 109;
|
||||
optional string act_mode_hugging_face_model_id = 110;
|
||||
optional OpenRouterModelInfo act_mode_hugging_face_model_info = 111;
|
||||
optional string act_mode_huawei_cloud_maas_model_id = 112;
|
||||
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 113;
|
||||
optional string act_mode_oca_model_id = 114;
|
||||
optional OcaModelInfo act_mode_oca_model_info = 115;
|
||||
optional string act_mode_oca_reasoning_effort = 116;
|
||||
optional string act_mode_aihubmix_model_id = 117;
|
||||
optional OpenAiCompatibleModelInfo act_mode_aihubmix_model_info = 118;
|
||||
optional string act_mode_hicap_model_id = 119;
|
||||
optional OpenRouterModelInfo act_mode_hicap_model_info = 120;
|
||||
optional string act_mode_nous_research_model_id = 121;
|
||||
optional string act_mode_vercel_ai_gateway_model_id = 122;
|
||||
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 123;
|
||||
optional ApiProvider plan_mode_api_provider = 124;
|
||||
optional ApiProvider act_mode_api_provider = 125;
|
||||
optional string hicap_model_id = 126;
|
||||
optional string lm_studio_model_id = 127;
|
||||
optional AutoApprovalSettings auto_approval_settings = 128;
|
||||
optional string global_cline_rules_toggles = 129;
|
||||
optional string global_workflow_toggles = 130;
|
||||
optional string global_skills_toggles = 131;
|
||||
optional BrowserSettings browser_settings = 132;
|
||||
optional string telemetry_setting = 133;
|
||||
optional bool plan_act_separate_models_setting = 134;
|
||||
optional bool enable_checkpoints_setting = 135;
|
||||
optional int32 shell_integration_timeout = 136;
|
||||
optional string default_terminal_profile = 137;
|
||||
optional int32 terminal_output_line_limit = 138;
|
||||
optional int32 max_consecutive_mistakes = 139;
|
||||
optional int32 subagent_terminal_output_line_limit = 140;
|
||||
optional bool strict_plan_mode_enabled = 141;
|
||||
optional bool yolo_mode_toggled = 142;
|
||||
optional bool use_auto_condense = 143;
|
||||
optional bool cline_web_tools_enabled = 144;
|
||||
optional string preferred_language = 145;
|
||||
optional OpenaiReasoningEffort openai_reasoning_effort = 146;
|
||||
optional PlanActMode mode = 147;
|
||||
optional DictationSettings dictation_settings = 148;
|
||||
optional FocusChainSettings focus_chain_settings = 149;
|
||||
optional string custom_prompt = 150;
|
||||
optional double auto_condense_threshold = 151;
|
||||
optional bool hooks_enabled = 152;
|
||||
optional bool subagents_enabled = 153;
|
||||
optional bool enable_parallel_tool_calling = 154;
|
||||
optional bool background_edit_enabled = 155;
|
||||
optional bool skills_enabled = 156;
|
||||
optional bool opt_out_of_remote_config = 157;
|
||||
optional bool open_telemetry_enabled = 158;
|
||||
optional string open_telemetry_metrics_exporter = 159;
|
||||
optional string open_telemetry_logs_exporter = 160;
|
||||
optional string open_telemetry_otlp_protocol = 161;
|
||||
optional string open_telemetry_otlp_endpoint = 162;
|
||||
optional string open_telemetry_otlp_metrics_protocol = 163;
|
||||
optional string open_telemetry_otlp_metrics_endpoint = 164;
|
||||
optional string open_telemetry_otlp_logs_protocol = 165;
|
||||
optional string open_telemetry_otlp_logs_endpoint = 166;
|
||||
optional int32 open_telemetry_metric_export_interval = 167;
|
||||
optional bool open_telemetry_otlp_insecure = 168;
|
||||
optional int32 open_telemetry_log_batch_size = 169;
|
||||
optional int32 open_telemetry_log_batch_timeout = 170;
|
||||
optional int32 open_telemetry_log_max_queue_size = 171;
|
||||
optional bool worktrees_enabled = 172;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -372,6 +420,9 @@ 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;
|
||||
optional bool worktrees_enabled = 40;
|
||||
}
|
||||
|
||||
message UpdateTerminalConnectionTimeoutRequest {
|
||||
|
||||
@@ -223,6 +223,10 @@ message ClineMessage {
|
||||
ClineModelInfo model_info = 23;
|
||||
}
|
||||
|
||||
message ShowWebviewEvent {
|
||||
bool preserve_editor_focus = 1; // When true, webview should not steal focus from editor
|
||||
}
|
||||
|
||||
// UiService provides methods for managing UI interactions
|
||||
service UiService {
|
||||
// Scrolls to a specific settings section in the settings view
|
||||
@@ -252,6 +256,9 @@ service UiService {
|
||||
// Subscribe to settings button clicked events
|
||||
rpc subscribeToSettingsButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to worktrees button clicked events
|
||||
rpc subscribeToWorktreesButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to partial message updates (streaming Cline messages as they're built)
|
||||
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
|
||||
|
||||
@@ -261,11 +268,8 @@ service UiService {
|
||||
// Subscribe to relinquish control events
|
||||
rpc subscribeToRelinquishControl(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to focus chat input events
|
||||
rpc subscribeToFocusChatInput(EmptyRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to webview visibility change events
|
||||
rpc subscribeToDidBecomeVisible(EmptyRequest) returns (stream Empty);
|
||||
// Subscribe to show webview events
|
||||
rpc subscribeToShowWebview(EmptyRequest) returns (stream ShowWebviewEvent);
|
||||
|
||||
// Returns the HTML for the webview index page. This is only used by external clients, not by the vscode webview.
|
||||
rpc getWebviewHtml(EmptyRequest) returns (String);
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package cline;
|
||||
|
||||
import "cline/common.proto";
|
||||
|
||||
option go_package = "github.com/cline/grpc-go/cline";
|
||||
option java_multiple_files = true;
|
||||
option java_package = "bot.cline.proto";
|
||||
|
||||
// Service for git worktree operations
|
||||
service WorktreeService {
|
||||
// Lists all worktrees in the current repository
|
||||
rpc listWorktrees(EmptyRequest) returns (WorktreeList);
|
||||
|
||||
// Creates a new worktree
|
||||
rpc createWorktree(CreateWorktreeRequest) returns (WorktreeResult);
|
||||
|
||||
// Deletes an existing worktree
|
||||
rpc deleteWorktree(DeleteWorktreeRequest) returns (WorktreeResult);
|
||||
|
||||
// Switches to a different worktree (opens in VS Code)
|
||||
rpc switchWorktree(SwitchWorktreeRequest) returns (WorktreeResult);
|
||||
|
||||
// Gets available branches for creating worktrees
|
||||
rpc getAvailableBranches(EmptyRequest) returns (BranchList);
|
||||
|
||||
// Gets suggested defaults for creating a new worktree (auto-generated branch name and path)
|
||||
rpc getWorktreeDefaults(EmptyRequest) returns (WorktreeDefaults);
|
||||
|
||||
// Gets the status of .worktreeinclude file and .gitignore contents for creating one
|
||||
rpc getWorktreeIncludeStatus(EmptyRequest) returns (WorktreeIncludeStatus);
|
||||
|
||||
// Creates a .worktreeinclude file with the provided content
|
||||
rpc createWorktreeInclude(CreateWorktreeIncludeRequest) returns (WorktreeResult);
|
||||
|
||||
// Switches to a different branch in the current worktree (git checkout)
|
||||
rpc checkoutBranch(CheckoutBranchRequest) returns (WorktreeResult);
|
||||
|
||||
// Merges a worktree's branch into the target branch and optionally deletes the worktree
|
||||
rpc mergeWorktree(MergeWorktreeRequest) returns (MergeWorktreeResult);
|
||||
|
||||
// Tracks when the worktrees view is opened (for telemetry)
|
||||
rpc trackWorktreeViewOpened(TrackWorktreeViewOpenedRequest) returns (Empty);
|
||||
}
|
||||
|
||||
// Represents a single git worktree
|
||||
message Worktree {
|
||||
string path = 1; // Absolute path to the worktree
|
||||
string branch = 2; // Branch name (empty if detached)
|
||||
string commit_hash = 3; // Current commit hash
|
||||
bool is_current = 4; // Whether this is the current worktree
|
||||
bool is_bare = 5; // Whether this is the bare repository
|
||||
bool is_detached = 6; // Whether HEAD is detached
|
||||
bool is_locked = 7; // Whether the worktree is locked
|
||||
optional string lock_reason = 8; // Reason for lock if locked
|
||||
}
|
||||
|
||||
// Response containing list of worktrees
|
||||
message WorktreeList {
|
||||
repeated Worktree worktrees = 1;
|
||||
bool is_git_repo = 2; // Whether the current workspace is a git repo
|
||||
string error = 3; // Error message if any
|
||||
bool is_multi_root = 4; // Whether multiple workspace folders are open (worktrees not supported)
|
||||
bool is_subfolder = 5; // Whether workspace is a subfolder of a git repo (not at repo root)
|
||||
string git_root_path = 6; // The actual git root path (useful when is_subfolder is true)
|
||||
}
|
||||
|
||||
// Request to create a new worktree
|
||||
message CreateWorktreeRequest {
|
||||
Metadata metadata = 1;
|
||||
string path = 2; // Path for the new worktree
|
||||
optional string branch = 3; // Branch name (creates new if doesn't exist)
|
||||
optional string base_branch = 4; // Base branch for new branch creation
|
||||
bool create_new_branch = 5; // Whether to create a new branch
|
||||
}
|
||||
|
||||
// Request to delete a worktree
|
||||
message DeleteWorktreeRequest {
|
||||
Metadata metadata = 1;
|
||||
string path = 2; // Path of the worktree to delete
|
||||
bool force = 3; // Force deletion even if dirty
|
||||
bool delete_branch = 4; // Also delete the branch
|
||||
string branch_name = 5; // Name of the branch to delete (required if delete_branch is true)
|
||||
}
|
||||
|
||||
// Request to switch to a worktree
|
||||
message SwitchWorktreeRequest {
|
||||
Metadata metadata = 1;
|
||||
string path = 2; // Path of the worktree to switch to
|
||||
bool new_window = 3; // Whether to open in a new window
|
||||
}
|
||||
|
||||
// Result of worktree operations
|
||||
message WorktreeResult {
|
||||
bool success = 1;
|
||||
string message = 2; // Success or error message
|
||||
optional Worktree worktree = 3; // The affected worktree (for create)
|
||||
}
|
||||
|
||||
// List of available branches
|
||||
message BranchList {
|
||||
repeated string local_branches = 1;
|
||||
repeated string remote_branches = 2;
|
||||
string current_branch = 3;
|
||||
}
|
||||
|
||||
// Suggested defaults for creating a new worktree
|
||||
message WorktreeDefaults {
|
||||
string suggested_branch = 1; // Auto-generated branch name like "worktree/cline-abc12"
|
||||
string suggested_path = 2; // Path in Documents/Cline/Worktrees/<project>-<suffix>
|
||||
}
|
||||
|
||||
// Status of .worktreeinclude file
|
||||
message WorktreeIncludeStatus {
|
||||
bool exists = 1; // Whether .worktreeinclude exists
|
||||
string gitignore_content = 2; // Content of .gitignore (for prefilling)
|
||||
bool has_gitignore = 3; // Whether .gitignore exists
|
||||
}
|
||||
|
||||
// Request to create .worktreeinclude file
|
||||
message CreateWorktreeIncludeRequest {
|
||||
string content = 1; // Content for the .worktreeinclude file
|
||||
}
|
||||
|
||||
// Request to checkout a branch in the current worktree
|
||||
message CheckoutBranchRequest {
|
||||
Metadata metadata = 1;
|
||||
string branch = 2; // Branch name to checkout
|
||||
}
|
||||
|
||||
// Request to merge a worktree's branch into target branch
|
||||
message MergeWorktreeRequest {
|
||||
Metadata metadata = 1;
|
||||
string worktree_path = 2; // Path of the worktree to merge
|
||||
string target_branch = 3; // Branch to merge into (e.g., "main")
|
||||
bool delete_after_merge = 4; // Whether to delete the worktree after successful merge
|
||||
}
|
||||
|
||||
// Result of merge operation
|
||||
message MergeWorktreeResult {
|
||||
bool success = 1;
|
||||
string message = 2; // Success or error message
|
||||
bool has_conflicts = 3; // Whether merge resulted in conflicts
|
||||
repeated string conflicting_files = 4; // List of files with conflicts
|
||||
string source_branch = 5; // The branch that was merged
|
||||
string target_branch = 6; // The branch merged into
|
||||
}
|
||||
|
||||
// Request to track worktree view opened (for telemetry)
|
||||
message TrackWorktreeViewOpenedRequest {
|
||||
string source = 1; // Where the view was opened from: "home_page" or "menu_bar"
|
||||
}
|
||||
@@ -35,6 +35,9 @@ service WorkspaceService {
|
||||
|
||||
// Executes a command in a new terminal
|
||||
rpc executeCommandInTerminal(ExecuteCommandInTerminalRequest) returns (ExecuteCommandInTerminalResponse);
|
||||
|
||||
// Opens a folder/workspace in the IDE
|
||||
rpc openFolder(OpenFolderRequest) returns (OpenFolderResponse);
|
||||
}
|
||||
|
||||
message GetWorkspacePathsRequest {
|
||||
@@ -107,3 +110,13 @@ message ExecuteCommandInTerminalRequest {
|
||||
message ExecuteCommandInTerminalResponse {
|
||||
bool success = 1; // Whether the command was successfully sent to the terminal
|
||||
}
|
||||
|
||||
// Request to open a folder/workspace
|
||||
message OpenFolderRequest {
|
||||
string path = 1; // The path to the folder to open
|
||||
bool new_window = 2; // Whether to open in a new window
|
||||
}
|
||||
|
||||
message OpenFolderResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ cd cli
|
||||
|
||||
# Define target platforms for cross-compilation
|
||||
PLATFORMS=(
|
||||
"windows/amd64"
|
||||
"darwin/arm64"
|
||||
"darwin/amd64"
|
||||
"linux/amd64"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { execSync } from "child_process"
|
||||
import * as fs from "fs/promises"
|
||||
import { globby } from "globby"
|
||||
import { createRequire } from "module"
|
||||
import { platform } from "os"
|
||||
import * as path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { createServiceNameMap, parseProtoForServices } from "./proto-shared-utils.mjs"
|
||||
@@ -26,7 +27,7 @@ function checkGoInstallation() {
|
||||
try {
|
||||
execSync("go version", { stdio: "pipe" })
|
||||
return true
|
||||
} catch (error) {
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -36,12 +37,12 @@ function checkGoTool(toolName) {
|
||||
try {
|
||||
execSync(`which ${toolName}`, { stdio: "pipe" })
|
||||
return true
|
||||
} catch (error) {
|
||||
} catch (_) {
|
||||
// On Windows, 'which' might not be available, try 'where'
|
||||
try {
|
||||
execSync(`where ${toolName}`, { stdio: "pipe" })
|
||||
return true
|
||||
} catch (windowsError) {
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -51,12 +52,26 @@ function checkGoTool(toolName) {
|
||||
function installGoTools() {
|
||||
console.log(chalk.yellow("Installing Go protobuf tools..."))
|
||||
|
||||
const OSPREFIX = (() => {
|
||||
if (platform() === "win32") {
|
||||
if (process.env.ComSpec) {
|
||||
if (process.env.PSModulePath && !process.env.SHELL) {
|
||||
return `$Env:GO111MODULE="on";`
|
||||
}
|
||||
return `set GO111MODULE=on &&`
|
||||
}
|
||||
return `GO111MODULE=on`
|
||||
} else {
|
||||
return `GO111MODULE=on`
|
||||
}
|
||||
})()
|
||||
|
||||
const tools = ["google.golang.org/protobuf/cmd/protoc-gen-go@latest", "google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest"]
|
||||
|
||||
for (const tool of tools) {
|
||||
try {
|
||||
console.log(chalk.cyan(`Installing ${tool}...`))
|
||||
execSync(`GO111MODULE=on go install ${tool}`, {
|
||||
execSync(`${OSPREFIX} go install ${tool}`, {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, GO111MODULE: "on" },
|
||||
})
|
||||
@@ -82,7 +97,9 @@ function checkToolsInPath() {
|
||||
|
||||
if (missingTools.length > 0) {
|
||||
console.log(chalk.yellow("Warning: Some Go protobuf tools are not in your PATH:"))
|
||||
missingTools.forEach((tool) => console.log(chalk.yellow(` - ${tool}`)))
|
||||
missingTools.forEach((tool) => {
|
||||
console.log(chalk.yellow(` - ${tool}`))
|
||||
})
|
||||
console.log()
|
||||
console.log(chalk.cyan("To fix this, add your Go bin directory to your PATH:"))
|
||||
|
||||
@@ -91,7 +108,7 @@ function checkToolsInPath() {
|
||||
try {
|
||||
goPath = execSync("go env GOPATH", { encoding: "utf8" }).trim()
|
||||
goBin = execSync("go env GOBIN", { encoding: "utf8" }).trim()
|
||||
} catch (error) {
|
||||
} catch (_) {
|
||||
console.log(chalk.red("Could not determine Go paths. Please check your Go installation."))
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -573,7 +590,7 @@ ${methods}
|
||||
}
|
||||
|
||||
// Main execution block - run if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
async function main() {
|
||||
try {
|
||||
console.log(chalk.cyan("Starting Go protobuf code generation..."))
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Script to build the Cline NPM package with telemetry keys injected
|
||||
# This script ensures all environment variables are properly set and builds are successful
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Required environment variables
|
||||
REQUIRED_VARS=(
|
||||
"TELEMETRY_SERVICE_API_KEY"
|
||||
"ERROR_SERVICE_API_KEY"
|
||||
)
|
||||
|
||||
# Optional but recommended environment variables
|
||||
OPTIONAL_VARS=(
|
||||
"CLINE_ENVIRONMENT"
|
||||
"POSTHOG_TELEMETRY_ENABLED"
|
||||
)
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}Cline NPM Package Build Script${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 1: Verify required environment variables are set
|
||||
echo -e "${BLUE}Step 1: Verifying environment variables...${NC}"
|
||||
MISSING_VARS=()
|
||||
for VAR in "${REQUIRED_VARS[@]}"; do
|
||||
if [ -z "${!VAR}" ]; then
|
||||
MISSING_VARS+=("$VAR")
|
||||
echo -e "${RED}✗ $VAR is not set${NC}"
|
||||
else
|
||||
# Show first 10 chars for verification (don't expose full key)
|
||||
VAR_VALUE="${!VAR}"
|
||||
echo -e "${GREEN}✓ $VAR is set (${VAR_VALUE:0:10}...)${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check optional variables
|
||||
for VAR in "${OPTIONAL_VARS[@]}"; do
|
||||
if [ -z "${!VAR}" ]; then
|
||||
echo -e "${YELLOW}⚠ $VAR is not set (optional)${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✓ $VAR is set: ${!VAR}${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#MISSING_VARS[@]} -gt 0 ]; then
|
||||
echo -e "\n${RED}Error: Missing required environment variables:${NC}"
|
||||
printf '%s\n' "${MISSING_VARS[@]}"
|
||||
echo -e "\n${YELLOW}Please set these variables before running the build:${NC}"
|
||||
echo -e "export TELEMETRY_SERVICE_API_KEY=\"your_posthog_api_key\""
|
||||
echo -e "export ERROR_SERVICE_API_KEY=\"your_error_tracking_api_key\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 2: Verify Node.js can see the environment variables
|
||||
echo -e "\n${BLUE}Step 2: Verifying Node.js can access environment variables...${NC}"
|
||||
if node -e "
|
||||
const telemetryKey = process.env.TELEMETRY_SERVICE_API_KEY;
|
||||
const errorKey = process.env.ERROR_SERVICE_API_KEY;
|
||||
if (!telemetryKey || !errorKey) {
|
||||
console.error('Node.js cannot see environment variables!');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ TELEMETRY_SERVICE_API_KEY visible to Node.js');
|
||||
console.log('✓ ERROR_SERVICE_API_KEY visible to Node.js');
|
||||
"; then
|
||||
echo -e "${GREEN}✓ Node.js can access environment variables${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Node.js cannot access environment variables${NC}"
|
||||
echo -e "${YELLOW}Make sure to use 'export' when setting variables:${NC}"
|
||||
echo -e "export TELEMETRY_SERVICE_API_KEY=\"...\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 3: Clean previous builds
|
||||
echo -e "\n${BLUE}Step 3: Cleaning previous builds...${NC}"
|
||||
rm -rf dist-standalone
|
||||
echo -e "${GREEN}✓ Cleaned dist-standalone directory${NC}"
|
||||
|
||||
# Step 4: Build Go CLI binaries for all platforms
|
||||
echo -e "\n${BLUE}Step 4: Building Go CLI binaries for all platforms...${NC}"
|
||||
if npm run compile-cli-all-platforms; then
|
||||
echo -e "${GREEN}✓ Go CLI binaries built successfully${NC}"
|
||||
|
||||
# Verify binaries were created
|
||||
if ls cli/bin/cline-* 1> /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ CLI binaries verified:${NC}"
|
||||
ls -lh cli/bin/cline-* | awk '{print " " $9 " (" $5 ")"}'
|
||||
else
|
||||
echo -e "${RED}✗ No CLI binaries found in cli/bin/${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗ Failed to build Go CLI binaries${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 5: Build the standalone package with esbuild
|
||||
echo -e "\n${BLUE}Step 5: Building standalone package with esbuild...${NC}"
|
||||
if npm run compile-standalone-npm; then
|
||||
echo -e "${GREEN}✓ Standalone package built successfully${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Failed to build standalone package${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 6: Verify telemetry keys were injected
|
||||
echo -e "\n${BLUE}Step 6: Verifying telemetry keys were injected...${NC}"
|
||||
|
||||
# Check if the compiled file still has process.env references (bad)
|
||||
if grep -q "process.env.TELEMETRY_SERVICE_API_KEY" dist-standalone/cline-core.js; then
|
||||
echo -e "${RED}✗ Keys were NOT injected! Found 'process.env.TELEMETRY_SERVICE_API_KEY' in compiled code${NC}"
|
||||
echo -e "${YELLOW}This means the environment variables were not replaced during build${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if actual keys are present (good)
|
||||
if grep -q "data.cline.bot" dist-standalone/cline-core.js; then
|
||||
# Extract a snippet of the PostHog config
|
||||
POSTHOG_CONFIG=$(grep -A 3 "data.cline.bot" dist-standalone/cline-core.js | head -5)
|
||||
if echo "$POSTHOG_CONFIG" | grep -q "apiKey.*phc_"; then
|
||||
echo -e "${GREEN}✓ Telemetry keys successfully injected into compiled code${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ PostHog config found but apiKey format unclear${NC}"
|
||||
echo -e "${YELLOW}Config snippet:${NC}"
|
||||
echo "$POSTHOG_CONFIG"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Could not verify PostHog config in compiled code${NC}"
|
||||
fi
|
||||
|
||||
# Step 7: Display build summary
|
||||
echo -e "\n${BLUE}========================================${NC}"
|
||||
echo -e "${GREEN}Build completed successfully!${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}Package location:${NC} dist-standalone/"
|
||||
echo -e "${GREEN}Package version:${NC} $(node -p "require('./dist-standalone/package.json').version" 2>/dev/null || echo "unknown")"
|
||||
echo ""
|
||||
echo -e "${BLUE}Next steps:${NC}"
|
||||
echo -e "1. Test locally: ${YELLOW}cd dist-standalone && npm link${NC}"
|
||||
echo -e "2. Verify: ${YELLOW}cline version${NC}"
|
||||
echo -e "3. Publish: ${YELLOW}cd dist-standalone && npm publish${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Note: Check PostHog dashboard after running cline commands to verify telemetry${NC}"
|
||||
@@ -12,7 +12,7 @@ import { main as generateHostBridgeClient } from "./generate-host-bridge-client.
|
||||
import { main as generateProtoBusSetup } from "./generate-protobus-setup.mjs"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
const PROTOC = `"${path.join(require.resolve("grpc-tools"), "../bin/protoc")}"`
|
||||
|
||||
const PROTO_DIR = path.resolve("proto")
|
||||
const TS_OUT_DIR = path.resolve("src/shared/proto")
|
||||
|
||||
@@ -0,0 +1,724 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Unified Build Orchestrator for Cline
|
||||
*
|
||||
* Cross-platform build script that works on Windows, macOS, and Linux.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/build.ts --surface=<vscode|jetbrains|cli|all> [options]
|
||||
*
|
||||
* Options:
|
||||
* --surface=<surface> Build target: vscode, jetbrains, cli, or all
|
||||
* --platform=<platform> CLI platform: unix, windows, or all (default: unix)
|
||||
* --prod Production build (minification, strip debug symbols)
|
||||
* --all-stages Build both dev and prod
|
||||
*
|
||||
* Examples:
|
||||
* npx tsx scripts/build.ts --surface=vscode
|
||||
* npx tsx scripts/build.ts --surface=cli --platform=windows --prod
|
||||
* npx tsx scripts/build.ts --surface=all --platform=all --all-stages
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process"
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const ROOT_DIR = path.resolve(__dirname, "..")
|
||||
|
||||
// Types
|
||||
type Surface = "vscode" | "jetbrains" | "cli" | "npm" | "all"
|
||||
type PlatformGroup = "unix" | "windows" | "all"
|
||||
|
||||
interface CliTarget {
|
||||
GOOS: string
|
||||
GOARCH: string
|
||||
}
|
||||
|
||||
interface ParsedArgs {
|
||||
surface: Surface
|
||||
platform: PlatformGroup
|
||||
prod: boolean
|
||||
allStages: boolean
|
||||
}
|
||||
|
||||
interface RunOptions {
|
||||
cwd?: string
|
||||
env?: NodeJS.ProcessEnv
|
||||
silent?: boolean
|
||||
}
|
||||
|
||||
interface BuildState {
|
||||
protos: boolean
|
||||
protosGo: boolean
|
||||
webview: boolean
|
||||
}
|
||||
|
||||
// CLI build targets by platform group
|
||||
const CLI_PLATFORMS: Record<"unix" | "windows", CliTarget[]> = {
|
||||
unix: [
|
||||
{ GOOS: "darwin", GOARCH: "amd64" },
|
||||
{ GOOS: "darwin", GOARCH: "arm64" },
|
||||
{ GOOS: "linux", GOARCH: "amd64" },
|
||||
{ GOOS: "linux", GOARCH: "arm64" },
|
||||
],
|
||||
windows: [
|
||||
{ GOOS: "windows", GOARCH: "amd64" },
|
||||
{ GOOS: "windows", GOARCH: "arm64" },
|
||||
],
|
||||
}
|
||||
|
||||
// Colors for terminal output
|
||||
const colors = {
|
||||
reset: "\x1b[0m",
|
||||
bright: "\x1b[1m",
|
||||
red: "\x1b[31m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
blue: "\x1b[34m",
|
||||
cyan: "\x1b[36m",
|
||||
} as const
|
||||
|
||||
function log(message: string, color: string = colors.reset): void {
|
||||
console.log(`${color}${message}${colors.reset}`)
|
||||
}
|
||||
|
||||
function logStep(step: string, message: string): void {
|
||||
log(`\n${colors.bright}[${step}]${colors.reset} ${message}`, colors.cyan)
|
||||
}
|
||||
|
||||
function logSuccess(message: string): void {
|
||||
log(` ✓ ${message}`, colors.green)
|
||||
}
|
||||
|
||||
function logError(message: string): void {
|
||||
log(` ✗ ${message}`, colors.red)
|
||||
}
|
||||
|
||||
function logWarning(message: string): void {
|
||||
log(` ! ${message}`, colors.yellow)
|
||||
}
|
||||
|
||||
function logSeparator(): void {
|
||||
log("=".repeat(60), colors.bright)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments
|
||||
*/
|
||||
function parseArgs(): ParsedArgs {
|
||||
const args = process.argv.slice(2)
|
||||
const result: ParsedArgs = {
|
||||
surface: "all",
|
||||
platform: "unix",
|
||||
prod: false,
|
||||
allStages: false,
|
||||
}
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith("--surface=")) {
|
||||
result.surface = arg.split("=")[1] as Surface
|
||||
} else if (arg.startsWith("--platform=")) {
|
||||
result.platform = arg.split("=")[1] as PlatformGroup
|
||||
} else if (arg === "--prod") {
|
||||
result.prod = true
|
||||
} else if (arg === "--all-stages") {
|
||||
result.allStages = true
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
console.log(`
|
||||
Unified Build Orchestrator for Cline
|
||||
|
||||
Usage:
|
||||
npx tsx scripts/build.ts --surface=<surface> [options]
|
||||
|
||||
Surfaces:
|
||||
vscode Build VS Code extension
|
||||
jetbrains Build JetBrains standalone package
|
||||
cli Build Go CLI binaries
|
||||
npm Build npm package (CLI + standalone for npm distribution)
|
||||
all Build all surfaces (vscode + jetbrains + cli, excludes npm)
|
||||
|
||||
Options:
|
||||
--platform=<platform> CLI platform target: unix, windows, or all (default: unix)
|
||||
--prod Production build (minification, strip debug symbols)
|
||||
--all-stages Build both dev and prod stages
|
||||
|
||||
Environment Variables (required for npm --prod):
|
||||
TELEMETRY_SERVICE_API_KEY PostHog telemetry API key
|
||||
ERROR_SERVICE_API_KEY Error tracking API key
|
||||
|
||||
Examples:
|
||||
npm run build:vscode # VS Code dev build
|
||||
npm run build:cli:windows:prod # CLI Windows production build
|
||||
npm run build:npm # npm package dev build
|
||||
npm run build:npm:prod # npm package prod build (requires env vars)
|
||||
npm run build:all-surfaces:all-platforms:all-stages # Everything
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate surface
|
||||
if (!["vscode", "jetbrains", "cli", "npm", "all"].includes(result.surface)) {
|
||||
logError(`Invalid surface: ${result.surface}. Must be one of: vscode, jetbrains, cli, npm, all`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Validate platform
|
||||
if (!["unix", "windows", "all"].includes(result.platform)) {
|
||||
logError(`Invalid platform: ${result.platform}. Must be one of: unix, windows, all`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command synchronously with cross-platform support
|
||||
*/
|
||||
function run(cmd: string, opts: RunOptions = {}): void {
|
||||
const { cwd = ROOT_DIR, env = process.env, silent = false } = opts
|
||||
|
||||
if (!silent) {
|
||||
log(` $ ${cmd}`, colors.yellow)
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(cmd, {
|
||||
cwd,
|
||||
env,
|
||||
stdio: silent ? "pipe" : "inherit",
|
||||
})
|
||||
} catch {
|
||||
throw new Error(`Command failed: ${cmd}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command and return the output
|
||||
*/
|
||||
function runCapture(cmd: string, opts: { cwd?: string } = {}): string | null {
|
||||
const { cwd = ROOT_DIR } = opts
|
||||
|
||||
try {
|
||||
return execSync(cmd, {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
}).trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get git commit hash
|
||||
*/
|
||||
function getGitCommit(): string {
|
||||
return runCapture("git rev-parse --short HEAD") || "unknown"
|
||||
}
|
||||
|
||||
/**
|
||||
* Read package.json version
|
||||
*/
|
||||
function getPackageVersion(packagePath: string): string {
|
||||
const fullPath = path.join(ROOT_DIR, packagePath)
|
||||
const pkg = JSON.parse(fs.readFileSync(fullPath, "utf8")) as { version: string }
|
||||
return pkg.version
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Go ldflags string
|
||||
*/
|
||||
function buildLdflags(prod: boolean): string {
|
||||
const version = getPackageVersion("package.json")
|
||||
const cliVersion = getPackageVersion("cli/package.json")
|
||||
const commit = getGitCommit()
|
||||
const date = new Date().toISOString()
|
||||
const builtBy = process.env.USER || process.env.USERNAME || "unknown"
|
||||
|
||||
let ldflags =
|
||||
`-X 'github.com/cline/cli/pkg/cli/global.Version=${version}' ` +
|
||||
`-X 'github.com/cline/cli/pkg/cli/global.CliVersion=${cliVersion}' ` +
|
||||
`-X 'github.com/cline/cli/pkg/cli/global.Commit=${commit}' ` +
|
||||
`-X 'github.com/cline/cli/pkg/cli/global.Date=${date}' ` +
|
||||
`-X 'github.com/cline/cli/pkg/cli/global.BuiltBy=${builtBy}'`
|
||||
|
||||
if (prod) {
|
||||
ldflags += " -s -w" // Strip debug symbols and DWARF
|
||||
}
|
||||
|
||||
return ldflags
|
||||
}
|
||||
|
||||
// Track what has been built to avoid duplicate work
|
||||
const buildState: BuildState = {
|
||||
protos: false,
|
||||
protosGo: false,
|
||||
webview: false,
|
||||
}
|
||||
|
||||
/**
|
||||
* Build protobuf definitions
|
||||
*/
|
||||
async function buildProtos(): Promise<void> {
|
||||
if (buildState.protos) {
|
||||
logSuccess("Protos already built, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
logStep("PROTOS", "Building protobuf definitions")
|
||||
run("npm run protos")
|
||||
buildState.protos = true
|
||||
logSuccess("Protos built")
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Go protobuf definitions
|
||||
*/
|
||||
async function buildProtosGo(): Promise<void> {
|
||||
if (buildState.protosGo) {
|
||||
logSuccess("Go protos already built, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
logStep("PROTOS-GO", "Building Go protobuf definitions")
|
||||
run("npm run protos-go")
|
||||
buildState.protosGo = true
|
||||
logSuccess("Go protos built")
|
||||
}
|
||||
|
||||
/**
|
||||
* Build webview UI
|
||||
*/
|
||||
async function buildWebview(): Promise<void> {
|
||||
if (buildState.webview) {
|
||||
logSuccess("Webview already built, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
logStep("WEBVIEW", "Building webview UI")
|
||||
run("npm run build:webview")
|
||||
buildState.webview = true
|
||||
logSuccess("Webview built")
|
||||
}
|
||||
|
||||
/**
|
||||
* Build VS Code extension
|
||||
*/
|
||||
async function buildVscode(prod: boolean): Promise<void> {
|
||||
const stage = prod ? "prod" : "dev"
|
||||
logStep("VSCODE", `Building VS Code extension (${stage})`)
|
||||
|
||||
await buildProtos()
|
||||
await buildWebview()
|
||||
|
||||
// Run esbuild
|
||||
const productionFlag = prod ? " --production" : ""
|
||||
run(`node esbuild.mjs${productionFlag}`)
|
||||
|
||||
logSuccess(`VS Code extension built (${stage})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build JetBrains standalone package
|
||||
*/
|
||||
async function buildJetbrains(prod: boolean): Promise<void> {
|
||||
const stage = prod ? "prod" : "dev"
|
||||
logStep("JETBRAINS", `Building JetBrains standalone package (${stage})`)
|
||||
|
||||
await buildProtos()
|
||||
await buildProtosGo()
|
||||
await buildWebview()
|
||||
|
||||
// Prepare dist-standalone directory
|
||||
const distDir = path.join(ROOT_DIR, "dist-standalone")
|
||||
const extensionDir = path.join(distDir, "extension")
|
||||
|
||||
fs.mkdirSync(extensionDir, { recursive: true })
|
||||
fs.copyFileSync(path.join(ROOT_DIR, "package.json"), path.join(extensionDir, "package.json"))
|
||||
|
||||
// Run esbuild with standalone flag
|
||||
const productionFlag = prod ? " --production" : ""
|
||||
run(`node esbuild.mjs --standalone${productionFlag}`)
|
||||
|
||||
// Run package-standalone.mjs (always builds for all platforms)
|
||||
run("node scripts/package-standalone.mjs")
|
||||
|
||||
logSuccess(`JetBrains standalone package built (${stage})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build CLI binaries for specified platforms
|
||||
*/
|
||||
async function buildCli(platformGroups: PlatformGroup[], prod: boolean): Promise<void> {
|
||||
const stage = prod ? "prod" : "dev"
|
||||
|
||||
// Expand platform groups to individual targets
|
||||
const targets: CliTarget[] = []
|
||||
for (const group of platformGroups) {
|
||||
if (group === "all") {
|
||||
targets.push(...CLI_PLATFORMS.unix, ...CLI_PLATFORMS.windows)
|
||||
} else if (CLI_PLATFORMS[group]) {
|
||||
targets.push(...CLI_PLATFORMS[group])
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate targets
|
||||
const uniqueTargets = [...new Map(targets.map((t) => [`${t.GOOS}-${t.GOARCH}`, t])).values()]
|
||||
|
||||
logStep("CLI", `Building CLI binaries (${stage}) for ${uniqueTargets.length} platform(s)`)
|
||||
|
||||
await buildProtos()
|
||||
await buildProtosGo()
|
||||
|
||||
// Prepare directories
|
||||
const cliDir = path.join(ROOT_DIR, "cli")
|
||||
const cliBinDir = path.join(cliDir, "bin")
|
||||
const distBinDir = path.join(ROOT_DIR, "dist-standalone", "bin")
|
||||
|
||||
fs.mkdirSync(cliBinDir, { recursive: true })
|
||||
fs.mkdirSync(distBinDir, { recursive: true })
|
||||
|
||||
// Also ensure dist-standalone/extension exists for package.json
|
||||
const extensionDir = path.join(ROOT_DIR, "dist-standalone", "extension")
|
||||
fs.mkdirSync(extensionDir, { recursive: true })
|
||||
fs.copyFileSync(path.join(ROOT_DIR, "package.json"), path.join(extensionDir, "package.json"))
|
||||
|
||||
const ldflags = buildLdflags(prod)
|
||||
|
||||
// Build for each target
|
||||
for (const { GOOS, GOARCH } of uniqueTargets) {
|
||||
const ext = GOOS === "windows" ? ".exe" : ""
|
||||
const platformSuffix = `${GOOS}-${GOARCH === "amd64" ? "x64" : GOARCH}`
|
||||
|
||||
log(` Building for ${platformSuffix}...`, colors.blue)
|
||||
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
GOOS,
|
||||
GOARCH,
|
||||
GO111MODULE: "on",
|
||||
}
|
||||
|
||||
// Build cline binary
|
||||
const clineOutput = path.join(cliBinDir, `cline-${platformSuffix}${ext}`)
|
||||
run(`go build -ldflags "${ldflags}" -o "${clineOutput}" ./cmd/cline`, {
|
||||
cwd: cliDir,
|
||||
env,
|
||||
silent: true,
|
||||
})
|
||||
logSuccess(`cline-${platformSuffix}${ext} built`)
|
||||
|
||||
// Build cline-host binary
|
||||
const hostOutput = path.join(cliBinDir, `cline-host-${platformSuffix}${ext}`)
|
||||
run(`go build -ldflags "${ldflags}" -o "${hostOutput}" ./cmd/cline-host`, {
|
||||
cwd: cliDir,
|
||||
env,
|
||||
silent: true,
|
||||
})
|
||||
logSuccess(`cline-host-${platformSuffix}${ext} built`)
|
||||
|
||||
// Copy to dist-standalone/bin
|
||||
fs.copyFileSync(clineOutput, path.join(distBinDir, `cline-${platformSuffix}${ext}`))
|
||||
fs.copyFileSync(hostOutput, path.join(distBinDir, `cline-host-${platformSuffix}${ext}`))
|
||||
}
|
||||
|
||||
// If building for current platform, also create generic binaries
|
||||
const currentOS = process.platform === "win32" ? "windows" : process.platform
|
||||
const currentTarget = uniqueTargets.find((t) => t.GOOS === currentOS && t.GOARCH === process.arch)
|
||||
|
||||
if (currentTarget) {
|
||||
const ext = currentOS === "windows" ? ".exe" : ""
|
||||
const platformSuffix = `${currentOS}-${process.arch}`
|
||||
|
||||
// Copy to generic names in cli/bin and dist-standalone/bin
|
||||
fs.copyFileSync(path.join(cliBinDir, `cline-${platformSuffix}${ext}`), path.join(cliBinDir, `cline${ext}`))
|
||||
fs.copyFileSync(path.join(cliBinDir, `cline-host-${platformSuffix}${ext}`), path.join(cliBinDir, `cline-host${ext}`))
|
||||
fs.copyFileSync(path.join(distBinDir, `cline-${platformSuffix}${ext}`), path.join(distBinDir, `cline${ext}`))
|
||||
fs.copyFileSync(path.join(distBinDir, `cline-host-${platformSuffix}${ext}`), path.join(distBinDir, `cline-host${ext}`))
|
||||
|
||||
logSuccess(`Generic binaries created for current platform (${platformSuffix})`)
|
||||
}
|
||||
|
||||
logSuccess(`CLI binaries built (${stage})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate telemetry environment variables are set
|
||||
*/
|
||||
function validateTelemetryEnvVars(): void {
|
||||
logStep("ENV", "Validating telemetry environment variables")
|
||||
|
||||
const requiredVars = ["TELEMETRY_SERVICE_API_KEY", "ERROR_SERVICE_API_KEY"]
|
||||
const optionalVars = ["CLINE_ENVIRONMENT", "POSTHOG_TELEMETRY_ENABLED"]
|
||||
const missingVars: string[] = []
|
||||
|
||||
for (const varName of requiredVars) {
|
||||
const value = process.env[varName]
|
||||
if (!value) {
|
||||
missingVars.push(varName)
|
||||
logError(`${varName} is not set`)
|
||||
} else {
|
||||
// Show first 10 chars for verification (don't expose full key)
|
||||
logSuccess(`${varName} is set (${value.substring(0, 10)}...)`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const varName of optionalVars) {
|
||||
const value = process.env[varName]
|
||||
if (!value) {
|
||||
logWarning(`${varName} is not set (optional)`)
|
||||
} else {
|
||||
logSuccess(`${varName} is set: ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
log("\n", colors.reset)
|
||||
logError("Missing required environment variables:")
|
||||
log("", colors.reset)
|
||||
log(' export TELEMETRY_SERVICE_API_KEY="your_posthog_api_key"', colors.yellow)
|
||||
log(' export ERROR_SERVICE_API_KEY="your_error_tracking_api_key"', colors.yellow)
|
||||
log("", colors.reset)
|
||||
throw new Error(`Missing required environment variables: ${missingVars.join(", ")}`)
|
||||
}
|
||||
|
||||
logSuccess("Environment variables validated")
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean dist-standalone directory
|
||||
*/
|
||||
function cleanDistStandalone(): void {
|
||||
logStep("CLEAN", "Cleaning dist-standalone directory")
|
||||
|
||||
const distDir = path.join(ROOT_DIR, "dist-standalone")
|
||||
|
||||
if (fs.existsSync(distDir)) {
|
||||
fs.rmSync(distDir, { recursive: true, force: true })
|
||||
logSuccess("Removed dist-standalone directory")
|
||||
} else {
|
||||
logSuccess("dist-standalone directory does not exist, nothing to clean")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify telemetry keys were injected into compiled output
|
||||
*/
|
||||
function verifyTelemetryInjection(): void {
|
||||
logStep("VERIFY", "Verifying telemetry keys were injected")
|
||||
|
||||
const clineCorePath = path.join(ROOT_DIR, "dist-standalone", "cline-core.js")
|
||||
|
||||
if (!fs.existsSync(clineCorePath)) {
|
||||
throw new Error(`Compiled file not found: ${clineCorePath}`)
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(clineCorePath, "utf8")
|
||||
|
||||
// Check if process.env references still exist (bad - means they weren't replaced)
|
||||
if (content.includes("process.env.TELEMETRY_SERVICE_API_KEY")) {
|
||||
logError("Keys were NOT injected! Found 'process.env.TELEMETRY_SERVICE_API_KEY' in compiled code")
|
||||
logError("This means the environment variables were not replaced during build")
|
||||
throw new Error("Telemetry keys were not injected into compiled code")
|
||||
}
|
||||
|
||||
// Check if PostHog endpoint is present (good - means config is there)
|
||||
if (content.includes("data.cline.bot")) {
|
||||
logSuccess("Telemetry keys successfully injected into compiled code")
|
||||
} else {
|
||||
logWarning("Could not verify PostHog config in compiled code")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print npm build summary
|
||||
*/
|
||||
function printNpmBuildSummary(): void {
|
||||
const distDir = path.join(ROOT_DIR, "dist-standalone")
|
||||
let version = "unknown"
|
||||
|
||||
try {
|
||||
const pkgPath = path.join(distDir, "package.json")
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as { version: string }
|
||||
version = pkg.version
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors reading version
|
||||
}
|
||||
|
||||
log("\n", colors.reset)
|
||||
logSeparator()
|
||||
log("NPM Package Build Summary", colors.green)
|
||||
logSeparator()
|
||||
log("")
|
||||
log(` Package location: ${colors.cyan}dist-standalone/${colors.reset}`, colors.reset)
|
||||
log(` Package version: ${colors.cyan}${version}${colors.reset}`, colors.reset)
|
||||
log("")
|
||||
log(" Next steps:", colors.bright)
|
||||
log(` 1. Test locally: ${colors.yellow}cd dist-standalone && npm link${colors.reset}`, colors.reset)
|
||||
log(` 2. Verify: ${colors.yellow}cline version${colors.reset}`, colors.reset)
|
||||
log(` 3. Publish: ${colors.yellow}cd dist-standalone && npm publish${colors.reset}`, colors.reset)
|
||||
log("")
|
||||
log(
|
||||
` ${colors.yellow}Note: Check PostHog dashboard after running cline commands to verify telemetry${colors.reset}`,
|
||||
colors.reset,
|
||||
)
|
||||
logSeparator()
|
||||
}
|
||||
|
||||
/**
|
||||
* Build npm package (CLI + standalone for npm distribution)
|
||||
*/
|
||||
async function buildNpm(prod: boolean): Promise<void> {
|
||||
const stage = prod ? "prod" : "dev"
|
||||
logStep("NPM", `Building npm package (${stage})`)
|
||||
|
||||
// Step 1: Validate telemetry env vars (prod only)
|
||||
if (prod) {
|
||||
validateTelemetryEnvVars()
|
||||
}
|
||||
|
||||
// Step 2: Clean dist-standalone directory
|
||||
cleanDistStandalone()
|
||||
|
||||
// Step 3: Build shared dependencies
|
||||
await buildProtos()
|
||||
await buildProtosGo()
|
||||
await buildWebview()
|
||||
|
||||
// Step 4: Build CLI for all platforms
|
||||
await buildCli(["all"], prod)
|
||||
|
||||
// Step 5: Build standalone with npm target
|
||||
logStep("STANDALONE", "Building standalone package for npm")
|
||||
const productionFlag = prod ? " --production" : ""
|
||||
run(`node esbuild.mjs --standalone${productionFlag}`)
|
||||
run("node scripts/package-standalone.mjs --target=npm")
|
||||
|
||||
// Step 6: Verify telemetry injection (prod only)
|
||||
if (prod) {
|
||||
verifyTelemetryInjection()
|
||||
}
|
||||
|
||||
// Step 7: Print summary
|
||||
printNpmBuildSummary()
|
||||
|
||||
logSuccess(`npm package built (${stage})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build all surfaces with specified options
|
||||
*/
|
||||
async function buildAll(surface: Surface, platform: PlatformGroup, prod: boolean): Promise<void> {
|
||||
const stage = prod ? "prod" : "dev"
|
||||
|
||||
logSeparator()
|
||||
log(`Building: surface=${surface}, platform=${platform}, stage=${stage}`, colors.bright)
|
||||
logSeparator()
|
||||
|
||||
if (surface === "all") {
|
||||
// Build vscode and jetbrains in parallel, then cli
|
||||
// We need to be careful with shared resources (protos, webview)
|
||||
// So we build shared dependencies first, then parallelize
|
||||
|
||||
logStep("SHARED", "Building shared dependencies")
|
||||
await buildProtos()
|
||||
await buildProtosGo()
|
||||
await buildWebview()
|
||||
|
||||
// Now we can build vscode and jetbrains in parallel
|
||||
logStep("PARALLEL", "Building VS Code and JetBrains in parallel")
|
||||
|
||||
const vscodePromise = (async () => {
|
||||
const productionFlag = prod ? " --production" : ""
|
||||
run(`node esbuild.mjs${productionFlag}`)
|
||||
logSuccess(`VS Code extension built (${stage})`)
|
||||
})()
|
||||
|
||||
const jetbrainsPromise = (async () => {
|
||||
// Prepare dist-standalone directory
|
||||
const distDir = path.join(ROOT_DIR, "dist-standalone")
|
||||
const extensionDir = path.join(distDir, "extension")
|
||||
fs.mkdirSync(extensionDir, { recursive: true })
|
||||
fs.copyFileSync(path.join(ROOT_DIR, "package.json"), path.join(extensionDir, "package.json"))
|
||||
|
||||
const productionFlag = prod ? " --production" : ""
|
||||
run(`node esbuild.mjs --standalone${productionFlag}`)
|
||||
run("node scripts/package-standalone.mjs")
|
||||
logSuccess(`JetBrains standalone package built (${stage})`)
|
||||
})()
|
||||
|
||||
await Promise.all([vscodePromise, jetbrainsPromise])
|
||||
|
||||
// Build CLI (includes both unix and windows for "all" surface)
|
||||
await buildCli(["unix", "windows"], prod)
|
||||
} else if (surface === "vscode") {
|
||||
await buildVscode(prod)
|
||||
} else if (surface === "jetbrains") {
|
||||
await buildJetbrains(prod)
|
||||
} else if (surface === "cli") {
|
||||
const platforms: PlatformGroup[] = platform === "all" ? ["all"] : [platform]
|
||||
await buildCli(platforms, prod)
|
||||
} else if (surface === "npm") {
|
||||
await buildNpm(prod)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset build state for new stage
|
||||
*/
|
||||
function resetBuildState(): void {
|
||||
buildState.protos = false
|
||||
buildState.protosGo = false
|
||||
buildState.webview = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs()
|
||||
|
||||
logSeparator()
|
||||
log("Cline Build Orchestrator", colors.bright)
|
||||
logSeparator()
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
if (args.allStages) {
|
||||
// Build both dev and prod
|
||||
log("\nBuilding all stages (dev + prod)...", colors.cyan)
|
||||
|
||||
// Dev build
|
||||
await buildAll(args.surface, args.platform, false)
|
||||
|
||||
// Reset build state for prod build
|
||||
resetBuildState()
|
||||
|
||||
// Prod build
|
||||
await buildAll(args.surface, args.platform, true)
|
||||
} else {
|
||||
await buildAll(args.surface, args.platform, args.prod)
|
||||
}
|
||||
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
|
||||
logSeparator()
|
||||
log(`Build completed successfully in ${duration}s`, colors.green)
|
||||
logSeparator()
|
||||
} catch (error) {
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
|
||||
logSeparator()
|
||||
logError(`Build failed after ${duration}s`)
|
||||
logError((error as Error).message)
|
||||
logSeparator()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -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,413 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generates proto message definitions from TypeScript source of truth.
|
||||
*
|
||||
* This script reads the field definitions from src/shared/storage/state-keys.ts
|
||||
* and generates the corresponding proto message definitions for Secrets and Settings.
|
||||
*
|
||||
* Usage: node scripts/generate-state-proto.mjs
|
||||
*
|
||||
* The generated proto content is written to proto/cline/state.proto,
|
||||
* replacing only the Secrets and Settings messages while preserving
|
||||
* the rest of the file (services, enums, other messages).
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs/promises"
|
||||
import { Project, SyntaxKind } from "ts-morph"
|
||||
|
||||
const STATE_KEYS_PATH = "src/shared/storage/state-keys.ts"
|
||||
const STATE_PROTO_PATH = "proto/cline/state.proto"
|
||||
|
||||
/**
|
||||
* Convert camelCase to snake_case for proto field names
|
||||
*/
|
||||
function camelToSnake(str) {
|
||||
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
|
||||
}
|
||||
|
||||
// Fields that should use int64 instead of int32
|
||||
const INT64_FIELDS = new Set(["planModeThinkingBudgetTokens", "actModeThinkingBudgetTokens"])
|
||||
|
||||
// Fields that should use double instead of int32
|
||||
const DOUBLE_FIELDS = new Set(["autoCondenseThreshold"])
|
||||
|
||||
/**
|
||||
* Infer proto type from TypeScript type expression
|
||||
* @param {string} typeText - The TypeScript type expression
|
||||
* @param {string} [fieldName] - Optional field name for field-specific overrides
|
||||
*/
|
||||
function inferProtoType(typeText, fieldName) {
|
||||
// Remove 'undefined' from union types
|
||||
const cleanType = typeText
|
||||
.replace(/\s*\|\s*undefined/g, "")
|
||||
.replace(/undefined\s*\|\s*/g, "")
|
||||
.trim()
|
||||
|
||||
// Handle common types
|
||||
if (cleanType === "string") {
|
||||
return "string"
|
||||
}
|
||||
if (cleanType === "boolean") {
|
||||
return "bool"
|
||||
}
|
||||
if (cleanType === "number") {
|
||||
// Some number fields need specific numeric types
|
||||
if (fieldName && INT64_FIELDS.has(fieldName)) {
|
||||
return "int64"
|
||||
}
|
||||
if (fieldName && DOUBLE_FIELDS.has(fieldName)) {
|
||||
return "double"
|
||||
}
|
||||
return "int32"
|
||||
}
|
||||
|
||||
// Handle Record<string, string> as map<string, string>
|
||||
if (/Record\s*<\s*string\s*,\s*string\s*>/.test(cleanType)) {
|
||||
return "map<string, string>"
|
||||
}
|
||||
|
||||
// Handle specific known types that map to proto messages/enums
|
||||
// Order matters! More specific types must come before generic ones
|
||||
// (e.g., OpenAiCompatibleModelInfo before ModelInfo)
|
||||
// Check known types BEFORE string literals, since types like `"act" as Mode`
|
||||
// contain quotes but should map to proto enums
|
||||
const knownTypes = [
|
||||
// Specific model info types first
|
||||
["OpenAiCompatibleModelInfo", "OpenAiCompatibleModelInfo"],
|
||||
["LiteLLMModelInfo", "LiteLLMModelInfo"],
|
||||
["OcaModelInfo", "OcaModelInfo"],
|
||||
// Generic ModelInfo last (catches OpenRouterModelInfo, etc.)
|
||||
["ModelInfo", "OpenRouterModelInfo"],
|
||||
// Other types - order matters for substring matching
|
||||
["AutoApprovalSettings", "AutoApprovalSettings"],
|
||||
["BrowserSettings", "BrowserSettings"],
|
||||
["DictationSettings", "DictationSettings"],
|
||||
["FocusChainSettings", "FocusChainSettings"],
|
||||
["OpenaiReasoningEffort", "OpenaiReasoningEffort"],
|
||||
["PlanActMode", "PlanActMode"],
|
||||
["ApiProvider", "ApiProvider"],
|
||||
["LanguageModelChatSelector", "LanguageModelChatSelector"], // Must come before "Mode" check
|
||||
]
|
||||
|
||||
for (const [tsType, protoType] of knownTypes) {
|
||||
if (cleanType.includes(tsType)) {
|
||||
return protoType
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Mode type separately with word boundary to avoid matching "VsCodeLmModelSelector"
|
||||
// This handles TS `Mode` type which maps to proto `PlanActMode`
|
||||
if (/\bMode\b/.test(cleanType)) {
|
||||
return "PlanActMode"
|
||||
}
|
||||
|
||||
// Handle specific string literal unions (treat as string)
|
||||
// This comes after known types check since some types like `"act" as Mode` contain quotes
|
||||
if (cleanType.includes('"') || cleanType.includes("'")) {
|
||||
return "string"
|
||||
}
|
||||
|
||||
// Default to string for complex types we can't map
|
||||
return "string"
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the SECRETS_KEYS array from state-keys.ts
|
||||
*/
|
||||
function parseSecretsKeys(sourceFile) {
|
||||
const secretsDecl = sourceFile.getVariableDeclaration("SECRETS_KEYS")
|
||||
if (!secretsDecl) {
|
||||
throw new Error("Could not find SECRETS_KEYS declaration")
|
||||
}
|
||||
|
||||
let initializer = secretsDecl.getInitializer()
|
||||
if (!initializer) {
|
||||
throw new Error("SECRETS_KEYS has no initializer")
|
||||
}
|
||||
|
||||
// Handle 'as const' expression
|
||||
if (initializer.getKind() === SyntaxKind.AsExpression) {
|
||||
initializer = initializer.getExpression()
|
||||
}
|
||||
|
||||
if (initializer.getKind() !== SyntaxKind.ArrayLiteralExpression) {
|
||||
throw new Error(`SECRETS_KEYS is not an array literal (got ${SyntaxKind[initializer.getKind()]})`)
|
||||
}
|
||||
|
||||
const keys = []
|
||||
for (const element of initializer.getElements()) {
|
||||
const text = element.getText()
|
||||
// Remove quotes and handle special prefixes
|
||||
const key = text.replace(/^['"]|['"]$/g, "")
|
||||
// Skip prefixed keys like "cline:clineAccountId"
|
||||
if (!key.includes(":")) {
|
||||
keys.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse field definitions from an object literal in state-keys.ts
|
||||
*/
|
||||
function parseFieldDefinitions(sourceFile, variableName) {
|
||||
const decl = sourceFile.getVariableDeclaration(variableName)
|
||||
if (!decl) {
|
||||
throw new Error(`Could not find ${variableName} declaration`)
|
||||
}
|
||||
|
||||
const initializer = decl.getInitializer()
|
||||
if (!initializer) {
|
||||
throw new Error(`${variableName} has no initializer`)
|
||||
}
|
||||
|
||||
// Handle 'satisfies' expression
|
||||
let objectLiteral = initializer
|
||||
if (initializer.getKind() === SyntaxKind.SatisfiesExpression) {
|
||||
objectLiteral = initializer.getExpression()
|
||||
}
|
||||
|
||||
if (objectLiteral.getKind() !== SyntaxKind.ObjectLiteralExpression) {
|
||||
throw new Error(`${variableName} is not an object literal`)
|
||||
}
|
||||
|
||||
const fields = []
|
||||
for (const prop of objectLiteral.getProperties()) {
|
||||
if (prop.getKind() !== SyntaxKind.PropertyAssignment) {
|
||||
continue
|
||||
}
|
||||
|
||||
const name = prop.getName()
|
||||
const propInit = prop.getInitializer()
|
||||
|
||||
if (!propInit || propInit.getKind() !== SyntaxKind.ObjectLiteralExpression) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the 'default' property to infer the type
|
||||
const defaultProp = propInit.getProperty("default")
|
||||
if (!defaultProp) {
|
||||
continue
|
||||
}
|
||||
|
||||
let typeText = "string"
|
||||
const defaultInit = defaultProp.getInitializer()
|
||||
if (defaultInit) {
|
||||
// Check for 'as' expression to get the type
|
||||
if (defaultInit.getKind() === SyntaxKind.AsExpression) {
|
||||
const typeNode = defaultInit.getTypeNode()
|
||||
if (typeNode) {
|
||||
typeText = typeNode.getText()
|
||||
}
|
||||
} else {
|
||||
// Infer from literal
|
||||
const text = defaultInit.getText()
|
||||
if (text === "true" || text === "false") {
|
||||
typeText = "boolean"
|
||||
} else if (/^\d+$/.test(text)) {
|
||||
typeText = "number"
|
||||
} else if (/^\d+\.\d+$/.test(text)) {
|
||||
typeText = "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fields.push({
|
||||
name,
|
||||
tsType: typeText,
|
||||
protoType: inferProtoType(typeText, name),
|
||||
})
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert snake_case to camelCase for mapping proto fields back to TS keys
|
||||
*/
|
||||
function snakeToCamel(str) {
|
||||
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse field numbers from an existing proto message definition
|
||||
* Returns a map of camelCase field names to their field numbers
|
||||
*/
|
||||
function parseProtoMessageFieldNumbers(protoContent, messageName) {
|
||||
const fieldNumbers = {}
|
||||
|
||||
// Match the message block (handles single-level nesting for now)
|
||||
const messageRegex = new RegExp(`message\\s+${messageName}\\s*\\{([^}]*(?:\\{[^}]*\\}[^}]*)*)\\}`, "s")
|
||||
const match = protoContent.match(messageRegex)
|
||||
|
||||
if (!match) {
|
||||
return fieldNumbers
|
||||
}
|
||||
|
||||
const messageBody = match[1]
|
||||
|
||||
// Match field definitions: optional/required/repeated type name = number;
|
||||
const fieldRegex = /(?:optional|required|repeated)?\s*\w+\s+(\w+)\s*=\s*(\d+)\s*;/g
|
||||
const matches = messageBody.matchAll(fieldRegex)
|
||||
|
||||
for (const fieldMatch of matches) {
|
||||
const snakeName = fieldMatch[1]
|
||||
const fieldNum = parseInt(fieldMatch[2], 10)
|
||||
const camelName = snakeToCamel(snakeName)
|
||||
fieldNumbers[camelName] = fieldNum
|
||||
}
|
||||
|
||||
return fieldNumbers
|
||||
}
|
||||
|
||||
/**
|
||||
* Load field number mappings from existing proto file
|
||||
*/
|
||||
async function loadFieldNumbersFromProto() {
|
||||
try {
|
||||
const protoContent = await fs.readFile(STATE_PROTO_PATH, "utf-8")
|
||||
const secrets = parseProtoMessageFieldNumbers(protoContent, "Secrets")
|
||||
const settings = parseProtoMessageFieldNumbers(protoContent, "Settings")
|
||||
|
||||
console.log(` Found ${Object.keys(secrets).length} existing Secrets fields`)
|
||||
console.log(` Found ${Object.keys(settings).length} existing Settings fields`)
|
||||
|
||||
return { Secrets: secrets, Settings: settings }
|
||||
} catch {
|
||||
// Proto file doesn't exist, start fresh
|
||||
return { Secrets: {}, Settings: {} }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign field numbers, preserving existing assignments and adding new ones
|
||||
*/
|
||||
function assignFieldNumbers(fields, existingNumbers, startNumber = 1) {
|
||||
const result = {}
|
||||
let nextNumber = startNumber
|
||||
|
||||
// Find the highest existing number
|
||||
for (const num of Object.values(existingNumbers)) {
|
||||
if (num >= nextNumber) {
|
||||
nextNumber = num + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve existing assignments
|
||||
for (const field of fields) {
|
||||
if (existingNumbers[field.name] !== undefined) {
|
||||
result[field.name] = existingNumbers[field.name]
|
||||
}
|
||||
}
|
||||
|
||||
// Assign new numbers for new fields
|
||||
for (const field of fields) {
|
||||
if (result[field.name] === undefined) {
|
||||
result[field.name] = nextNumber++
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate proto message definition
|
||||
*/
|
||||
function generateProtoMessage(messageName, fields, fieldNumbers) {
|
||||
const lines = [`message ${messageName} {`]
|
||||
|
||||
// Sort fields by field number for consistent output
|
||||
const sortedFields = [...fields].sort((a, b) => fieldNumbers[a.name] - fieldNumbers[b.name])
|
||||
|
||||
for (const field of sortedFields) {
|
||||
const snakeName = camelToSnake(field.name)
|
||||
const fieldNum = fieldNumbers[field.name]
|
||||
// Map types cannot have the 'optional' modifier in proto3
|
||||
const prefix = field.protoType.startsWith("map<") ? "" : "optional "
|
||||
lines.push(` ${prefix}${field.protoType} ${snakeName} = ${fieldNum};`)
|
||||
}
|
||||
|
||||
lines.push("}")
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Secrets message from SECRETS_KEYS
|
||||
*/
|
||||
function generateSecretsMessage(secretsKeys, fieldNumbers) {
|
||||
const fields = secretsKeys.map((key) => ({
|
||||
name: key,
|
||||
protoType: "string",
|
||||
}))
|
||||
|
||||
return generateProtoMessage("Secrets", fields, fieldNumbers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a message in the proto file content
|
||||
*/
|
||||
function replaceMessage(protoContent, messageName, newMessageContent) {
|
||||
// Match the message definition including nested braces
|
||||
const messageRegex = new RegExp(`message\\s+${messageName}\\s*\\{[^}]*(?:\\{[^}]*\\}[^}]*)*\\}`, "g")
|
||||
|
||||
if (messageRegex.test(protoContent)) {
|
||||
return protoContent.replace(messageRegex, newMessageContent)
|
||||
} else {
|
||||
// Message doesn't exist, append before the first message or at end
|
||||
console.warn(`Warning: ${messageName} message not found in proto file, appending`)
|
||||
return protoContent + "\n\n" + newMessageContent
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Generating proto definitions from TypeScript source...")
|
||||
|
||||
// Parse TypeScript source
|
||||
const project = new Project({
|
||||
tsConfigFilePath: "tsconfig.json",
|
||||
})
|
||||
const sourceFile = project.addSourceFileAtPath(STATE_KEYS_PATH)
|
||||
|
||||
// Parse definitions
|
||||
const secretsKeys = parseSecretsKeys(sourceFile)
|
||||
console.log(`Found ${secretsKeys.length} secret keys`)
|
||||
|
||||
const apiHandlerFields = parseFieldDefinitions(sourceFile, "API_HANDLER_SETTINGS_FIELDS")
|
||||
const userSettingsFields = parseFieldDefinitions(sourceFile, "USER_SETTINGS_FIELDS")
|
||||
const settingsFields = [...apiHandlerFields, ...userSettingsFields]
|
||||
console.log(`Found ${settingsFields.length} settings fields`)
|
||||
|
||||
// Load existing field numbers from proto file
|
||||
const existingFieldNumbers = await loadFieldNumbersFromProto()
|
||||
|
||||
// Assign field numbers (preserving existing, adding new ones)
|
||||
const secretsFieldNumbers = assignFieldNumbers(
|
||||
secretsKeys.map((k) => ({ name: k })),
|
||||
existingFieldNumbers.Secrets,
|
||||
1,
|
||||
)
|
||||
const settingsFieldNumbers = assignFieldNumbers(settingsFields, existingFieldNumbers.Settings, 1)
|
||||
|
||||
// Generate messages
|
||||
const secretsMessage = generateSecretsMessage(secretsKeys, secretsFieldNumbers)
|
||||
const settingsMessage = generateProtoMessage("Settings", settingsFields, settingsFieldNumbers)
|
||||
|
||||
// Read existing proto file
|
||||
let protoContent = await fs.readFile(STATE_PROTO_PATH, "utf-8")
|
||||
|
||||
// Replace messages
|
||||
protoContent = replaceMessage(protoContent, "Secrets", secretsMessage)
|
||||
protoContent = replaceMessage(protoContent, "Settings", settingsMessage)
|
||||
|
||||
// Write updated proto file
|
||||
await fs.writeFile(STATE_PROTO_PATH, protoContent)
|
||||
console.log(`Updated ${STATE_PROTO_PATH}`)
|
||||
|
||||
console.log("\nGeneration complete! Run 'npm run protos' to regenerate TypeScript from protos.")
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Error:", error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -22,19 +22,27 @@ echo ""
|
||||
# Always rebuild CLI to ensure latest changes
|
||||
echo -e "${CYAN}→${NC} ${DIM}Rebuilding CLI binaries...${NC}"
|
||||
cd "$PROJECT_ROOT"
|
||||
if npm run compile-cli 2>&1 | grep -E "(built|error|Error)" || true; then
|
||||
if npm run build:cli:all-platforms 2>&1 | grep -E "(built|error|Error)" || true; then
|
||||
echo -e "${GREEN}✓${NC} CLI binaries rebuilt"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} CLI build may have issues - check output above"
|
||||
echo -e "${YELLOW}⚠${NC} CLI build failed - aborting install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Always rebuild standalone to ensure latest cline-core.js
|
||||
echo -e "${CYAN}→${NC} ${DIM}Rebuilding standalone package (this may take ~30 seconds)...${NC}"
|
||||
if npm run compile-standalone 2>&1 | tail -5; then
|
||||
rm -rf "$PROJECT_ROOT/dist-standalone"
|
||||
if npm run compile-standalone; then
|
||||
echo -e "${GREEN}✓${NC} Standalone package rebuilt"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Standalone build may have issues - check output above"
|
||||
echo -e "${YELLOW}⚠${NC} Standalone build failed - aborting install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure extension package.json is present for cline-core startup
|
||||
mkdir -p "$PROJECT_ROOT/dist-standalone/extension"
|
||||
cp "$PROJECT_ROOT/package.json" "$PROJECT_ROOT/dist-standalone/extension/package.json"
|
||||
|
||||
echo ""
|
||||
|
||||
echo -e "${CYAN}→${NC} ${DIM}Installing to $INSTALL_DIR${NC}"
|
||||
@@ -50,7 +58,13 @@ fi
|
||||
mkdir -p "$INSTALL_DIR/bin"
|
||||
|
||||
# Copy standalone package first (includes node_modules, cline-core.js, etc.)
|
||||
rsync -a --exclude='bin' "$PROJECT_ROOT/dist-standalone/" "$INSTALL_DIR/"
|
||||
cp -r "$PROJECT_ROOT/dist-standalone/" "$INSTALL_DIR/"
|
||||
|
||||
# Check if OS is windows
|
||||
WIN=false
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
|
||||
WIN=true
|
||||
fi
|
||||
|
||||
# Detect platform for native modules
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
@@ -59,17 +73,29 @@ if [[ "$arch" == "aarch64" ]]; then arch="arm64"; fi
|
||||
if [[ "$arch" == "x86_64" ]]; then arch="x64"; fi
|
||||
platform="$os-$arch"
|
||||
|
||||
# Manually set platform to windows, as windows is tricky
|
||||
if [[ "$WIN" = true ]]; then
|
||||
platform="win-$arch"
|
||||
fi
|
||||
|
||||
echo -e "${CYAN}→${NC} ${DIM}Detected platform as $platform${NC}"
|
||||
|
||||
# Copy platform-specific native modules (like better-sqlite3)
|
||||
if [ -d "$PROJECT_ROOT/dist-standalone/binaries/$platform/node_modules" ]; then
|
||||
echo -e "${CYAN}→${NC} ${DIM}Installing platform-specific modules for $platform${NC}"
|
||||
cp -r "$PROJECT_ROOT/dist-standalone/binaries/$platform/node_modules/"* "$INSTALL_DIR/node_modules/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
|
||||
# Copy binaries (this will create/overwrite the bin directory)
|
||||
mkdir -p "$INSTALL_DIR/bin"
|
||||
cp "$PROJECT_ROOT/cli/bin/cline" "$INSTALL_DIR/bin/"
|
||||
cp "$PROJECT_ROOT/cli/bin/cline-host" "$INSTALL_DIR/bin/"
|
||||
|
||||
if [[ "$WIN" = true ]]; then
|
||||
cp "$PROJECT_ROOT/cli/bin/cline-windows-amd64.exe" "$INSTALL_DIR/bin/cline.exe"
|
||||
cp "$PROJECT_ROOT/cli/bin/cline-host-windows-amd64.exe" "$INSTALL_DIR/bin/cline-host.exe"
|
||||
else
|
||||
cp "$PROJECT_ROOT/cli/bin/cline" "$INSTALL_DIR/bin/"
|
||||
cp "$PROJECT_ROOT/cli/bin/cline-host" "$INSTALL_DIR/bin/"
|
||||
fi
|
||||
# Use system Node.js (symlink to avoid copying large binary)
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
ln -sf "$(which node)" "$INSTALL_DIR/bin/node"
|
||||
@@ -87,7 +113,11 @@ chmod +x "$INSTALL_DIR/bin/node" 2>/dev/null || true
|
||||
# Rebuild better-sqlite3 for system Node.js version
|
||||
echo -e "${CYAN}→${NC} ${DIM}Rebuilding native modules for Node.js $(node --version)...${NC}"
|
||||
cd "$INSTALL_DIR"
|
||||
npm rebuild better-sqlite3 > /dev/null 2>&1
|
||||
npm rebuild better-sqlite3
|
||||
|
||||
mkdir -p "$INSTALL_DIR/dist-standalone/node_modules/better-sqlite3"
|
||||
cp -r "$INSTALL_DIR/node_modules/." "$INSTALL_DIR/dist-standalone/node_modules/better-sqlite3/"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
echo -e "${GREEN}✓${NC} Native modules rebuilt"
|
||||
|
||||
|
||||
+21
-3
@@ -23,6 +23,13 @@ FORCE_INSTALL="${FORCE_INSTALL:-false}"
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
arch=$(uname -m)
|
||||
|
||||
# Check if OS is windows
|
||||
WIN=false
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
|
||||
WIN=true
|
||||
INSTALL_DIR="${CLINE_INSTALL_DIR:-$LOCALAPPDATA/.cline/cli}"
|
||||
fi
|
||||
|
||||
# Normalize architecture names
|
||||
if [[ "$arch" == "aarch64" ]]; then
|
||||
arch="arm64"
|
||||
@@ -47,8 +54,11 @@ case "$os" in
|
||||
platform="linux-$arch"
|
||||
;;
|
||||
*)
|
||||
[[ "$WIN" == true ]] || {
|
||||
echo -e "${RED}${BOLD}ERROR${NC} ${RED}Unsupported OS: $os${NC}" >&2
|
||||
exit 1
|
||||
}
|
||||
platform="win-$arch"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -369,9 +379,17 @@ install_cline() {
|
||||
|
||||
# Extract package
|
||||
print_step "Extracting package"
|
||||
if ! tar -xzf "$package_file" -C "$INSTALL_DIR" --strip-components=0; then
|
||||
print_error "Failed to extract package"
|
||||
exit 1
|
||||
if ! tar -xzf "$package_file" -C "$INSTALL_DIR" --strip-components=0; then
|
||||
if [[ "$WIN" == true ]]; then
|
||||
echo -e "${RED}${BOLD}TAR command failed. Attempting backup method...${NC}"
|
||||
if ! unzip -oq "$package_file" -d "$INSTALL_DIR"; then
|
||||
print_error "Failed to extract package"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_error "Failed to extract package"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Make binaries executable
|
||||
|
||||
@@ -0,0 +1,584 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Unified installer for Cline
|
||||
*
|
||||
* Supports local (development) installation, and standard (production) installation
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/install.ts [--local] [options]
|
||||
*
|
||||
* Examples:
|
||||
* npx tsx scripts/install.ts
|
||||
* npx tsx scripts/install.ts --local
|
||||
* npx tsx scripts/install.ts 3.42.1
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process"
|
||||
import fs from "node:fs/promises"
|
||||
import { homedir } from "node:os"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const ROOT_DIR = path.resolve(__dirname, "..")
|
||||
|
||||
interface ParsedArgs {
|
||||
local: boolean
|
||||
version: string
|
||||
}
|
||||
|
||||
interface RunOptions {
|
||||
cwd?: string
|
||||
env?: NodeJS.ProcessEnv
|
||||
silent?: boolean
|
||||
}
|
||||
|
||||
interface Platform {
|
||||
name: "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win" | "android" | "haiku" | "cygwin" | "netbsd"
|
||||
arch: "arm" | "arm64" | "ia32" | "loong64" | "mips" | "mipsel" | "ppc" | "ppc64" | "riscv64" | "s390" | "s390x" | "x64"
|
||||
}
|
||||
|
||||
const SupportedPlatforms: Platform[] = [
|
||||
{ name: "darwin", arch: "arm64" },
|
||||
{ name: "darwin", arch: "x64" },
|
||||
{ name: "linux", arch: "arm64" },
|
||||
{ name: "linux", arch: "x64" },
|
||||
{ name: "win", arch: "x64" },
|
||||
]
|
||||
|
||||
// Colors for terminal output
|
||||
const colors = {
|
||||
reset: "\x1b[0m",
|
||||
bright: "\x1b[1m",
|
||||
red: "\x1b[31m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
blue: "\x1b[34m",
|
||||
cyan: "\x1b[36m",
|
||||
magenta: "\x1b[35m",
|
||||
} as const
|
||||
|
||||
function log(message: string, color: string = colors.reset): void {
|
||||
console.log(`${color}${message}${colors.reset}`)
|
||||
}
|
||||
|
||||
function logStep(step: string, message: string): void {
|
||||
log(`\n${colors.bright}[${step}]${colors.reset} ${message}`, colors.cyan)
|
||||
}
|
||||
|
||||
function logSuccess(message: string): void {
|
||||
log(` ✓ ${message}`, colors.green)
|
||||
}
|
||||
|
||||
function logError(message: string): void {
|
||||
log(` ✗ ${message}`, colors.red)
|
||||
}
|
||||
|
||||
function logWarning(message: string): void {
|
||||
log(` ! ${message}`, colors.yellow)
|
||||
}
|
||||
|
||||
function logSeparator(): void {
|
||||
log("=".repeat(60), colors.bright)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments
|
||||
*/
|
||||
function parseArgs(): ParsedArgs {
|
||||
const args = process.argv.slice(2)
|
||||
const result: ParsedArgs = {
|
||||
local: false,
|
||||
version: "latest",
|
||||
}
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg === "--local") {
|
||||
result.local = true
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
console.log(`
|
||||
Unified Installer for Cline
|
||||
|
||||
Usage:
|
||||
npx tsx scripts/install.ts [--local]
|
||||
|
||||
Options:
|
||||
--local Install using local build instead of production build
|
||||
`)
|
||||
process.exit(0)
|
||||
} else {
|
||||
// Assume arg is version and attempt to set it. Create validator later
|
||||
result.version = arg
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command synchronously with cross-platform support
|
||||
*/
|
||||
function run(cmd: string, opts: RunOptions = {}): void {
|
||||
const { cwd = ROOT_DIR, env = process.env, silent = false } = opts
|
||||
|
||||
if (!silent) {
|
||||
log(` $ ${cmd}`, colors.yellow)
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(cmd, {
|
||||
cwd,
|
||||
env,
|
||||
stdio: silent ? "pipe" : "inherit",
|
||||
})
|
||||
} catch {
|
||||
throw new Error(`Command failed: ${cmd}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command and return the output
|
||||
*/
|
||||
function runCapture(cmd: string, opts: { cwd?: string } = {}): string | null {
|
||||
const { cwd = ROOT_DIR } = opts
|
||||
|
||||
try {
|
||||
return execSync(cmd, {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
}).trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function detectOS(): Promise<Platform> {
|
||||
const sanitizedPlatform = (() => {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return "win"
|
||||
default:
|
||||
return process.platform
|
||||
}
|
||||
})()
|
||||
return { name: sanitizedPlatform, arch: process.arch }
|
||||
}
|
||||
|
||||
function ValidateOS({ name, arch }: Platform) {
|
||||
return new Promise((resolve) => {
|
||||
const isSupported = !!SupportedPlatforms.find((platform) => {
|
||||
if (platform.arch === arch) {
|
||||
if (platform.name === name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
if (isSupported) {
|
||||
resolve(true)
|
||||
} else {
|
||||
throw new Error(`Unsupported Operating System: ${name}-${arch}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function getInstallDirectory({ name }: Platform) {
|
||||
switch (name) {
|
||||
case "win":
|
||||
return (
|
||||
(process.env.APPDATA && path.join(process.env.APPDATA, ".cline", ".cli")) ||
|
||||
path.join(homedir(), "AppData", "Roaming", ".cline", ".cli")
|
||||
)
|
||||
case "darwin":
|
||||
case "linux":
|
||||
return (
|
||||
(process.env.XDG_CONFIG_HOME && path.join(process.env.XDG_CONFIG_HOME, ".cline", ".cli")) ||
|
||||
path.join(homedir(), ".cline", ".")
|
||||
)
|
||||
default:
|
||||
throw new Error(`Unhandled operating system: ${name}\n\nPlease report this error to support@cline.bot or on github!`)
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildCLI({ name }: Platform) {
|
||||
try {
|
||||
// Build for current platform
|
||||
const buildFor = name === "win" ? "windows" : "unix"
|
||||
log(`Building explicitly for ${buildFor}`)
|
||||
execSync(`npm run build:cli:${buildFor}`, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
throw new Error(`Error building CLI binaries: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildClineCore() {
|
||||
try {
|
||||
// Build standalone package
|
||||
execSync(`npm run compile-standalone`, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
throw new Error(`Error building Cline Core: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function removePreviousCLI({ name }: Platform, installDirectory: string) {
|
||||
switch (name) {
|
||||
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Only windows install is deprecated. This shouldn't be a factor, as we're adding support, but it's future-proofing.
|
||||
case "win":
|
||||
// Check if a previous installation exists at old install location
|
||||
const oldInstallDir = path.join(homedir(), ".cline")
|
||||
const deprecatedInstallDir = await (async () => {
|
||||
try {
|
||||
await fs.access(oldInstallDir)
|
||||
return true
|
||||
} catch (_error) {
|
||||
return false
|
||||
}
|
||||
})()
|
||||
if (deprecatedInstallDir) {
|
||||
logWarning(
|
||||
"Deprecated installation of Cline CLI detected. Migration will be attempted, however data loss may occur.",
|
||||
)
|
||||
log("Copying old files...", colors.yellow)
|
||||
try {
|
||||
await fs.cp(oldInstallDir, path.resolve(installDirectory, "../"), { recursive: true })
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy old files: ${error}`)
|
||||
}
|
||||
try {
|
||||
log("Removing old directory...", colors.yellow)
|
||||
await fs.rm(oldInstallDir, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Unable to perform migration. The following error was returned: \n\n${error}\n\nPlease manually move the .cline folder located at ${oldInstallDir} to ${path.resolve(installDirectory, "..")}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
case "darwin":
|
||||
case "linux":
|
||||
log("Removing existing installation for clean install, if necessary", colors.yellow)
|
||||
const previousInstall = await (async () => {
|
||||
try {
|
||||
await fs.access(installDirectory)
|
||||
return true
|
||||
} catch (_error) {
|
||||
return false
|
||||
}
|
||||
})()
|
||||
if (previousInstall) {
|
||||
try {
|
||||
await fs.rm(installDirectory, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to remove previous installation: ${error}`)
|
||||
}
|
||||
} else {
|
||||
log("No previous cline installation detected.", colors.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureDirectory(directory: string) {
|
||||
const dirExists = await (async () => {
|
||||
try {
|
||||
await fs.access(directory)
|
||||
return true
|
||||
} catch (_error) {
|
||||
return false
|
||||
}
|
||||
})()
|
||||
if (dirExists) {
|
||||
return
|
||||
} else {
|
||||
try {
|
||||
await fs.mkdir(directory, { recursive: true })
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to create installation directory: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyStandalone(installDirectory: string) {
|
||||
const standaloneDir: string = path.resolve(ROOT_DIR, "dist-standalone")
|
||||
try {
|
||||
await fs.cp(standaloneDir, path.resolve(installDirectory, "dist-standalone"), { recursive: true })
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy standalone package files: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPlatformModules({ name, arch }: Platform, installDirectory: string) {
|
||||
const moduleDir: string = path.resolve(ROOT_DIR, "dist-standalone", "binaries", `${name}-${arch}`, "node_modules")
|
||||
try {
|
||||
await fs.cp(moduleDir, path.resolve(installDirectory, "dist-standalone", "node_modules"), { recursive: true })
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy platform module files for ${name}-${arch}: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function copyBinaries({ name, arch }: Platform, installDirectory: string) {
|
||||
try {
|
||||
switch (name) {
|
||||
case "win":
|
||||
await fs.cp(
|
||||
path.resolve(ROOT_DIR, "cli", "bin", `cline-windows-${arch}.exe`),
|
||||
path.resolve(installDirectory, "bin", "cline.exe"),
|
||||
)
|
||||
await fs.cp(
|
||||
path.resolve(ROOT_DIR, "cli", "bin", `cline-host-windows-${arch}.exe`),
|
||||
path.resolve(installDirectory, "bin", "cline-host.exe"),
|
||||
)
|
||||
break
|
||||
case "darwin":
|
||||
case "linux":
|
||||
await fs.cp(
|
||||
path.resolve(ROOT_DIR, "cli", "bin", `cline-${name}-${arch}`),
|
||||
path.resolve(installDirectory, "bin", "cline"),
|
||||
)
|
||||
await fs.cp(
|
||||
path.resolve(ROOT_DIR, "cli", "bin", `cline-host-${name}-${arch}`),
|
||||
path.resolve(installDirectory, "bin", "cline-host"),
|
||||
)
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy platform module files for ${name}-${arch}: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
function linkSystemNode(installDirectory: string): Promise<string> {
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
await fs.cp(path.resolve(process.argv[0]), path.resolve(installDirectory, "bin", "node"))
|
||||
const nodev = execSync("node -v", { cwd: path.resolve(installDirectory, "bin") }).toString()
|
||||
resolve(nodev)
|
||||
} catch (error: any) {
|
||||
if (error.code === "EEXIST") {
|
||||
const nodev = execSync("node -v", { cwd: path.resolve(installDirectory, "bin") }).toString()
|
||||
resolve(nodev)
|
||||
return
|
||||
}
|
||||
throw new Error(`Unable to copy node: ${error}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function makeExecutable({ name }: Platform, installDirectory: string) {
|
||||
try {
|
||||
switch (name) {
|
||||
case "win":
|
||||
await fs.chmod(path.resolve(installDirectory, "bin", "cline.exe"), 0x755)
|
||||
await fs.chmod(path.resolve(installDirectory, "bin", "cline-host.exe"), 0x755)
|
||||
break
|
||||
case "darwin":
|
||||
case "linux":
|
||||
await fs.chmod(path.resolve(installDirectory, "bin", "cline"), 0x755)
|
||||
await fs.chmod(path.resolve(installDirectory, "bin", "cline-host"), 0x755)
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set permissions for files: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildNativeModules(installDirectory: string) {
|
||||
try {
|
||||
execSync("npm rebuild better-sqlite3", { cwd: installDirectory })
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to rebuild modules: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function configurePATH({ name }: Platform, installDirectory: string) {
|
||||
try {
|
||||
const absolutePath = path.resolve(installDirectory, "bin")
|
||||
|
||||
if (name === "win") {
|
||||
try {
|
||||
// Get current user PATH
|
||||
const stdout = execSync(`powershell -Command "[Environment]::GetEnvironmentVariable('Path', 'User')"`, {
|
||||
encoding: "utf-8",
|
||||
})
|
||||
|
||||
const currentPath = stdout.trim()
|
||||
|
||||
// Check if already in PATH
|
||||
if (currentPath.split(";").some((p) => p.toLowerCase() === absolutePath.toLowerCase())) {
|
||||
log("Directory already in PATH", colors.green)
|
||||
return
|
||||
}
|
||||
|
||||
// Add to PATH
|
||||
const newPath = currentPath ? `${currentPath};${absolutePath}` : absolutePath
|
||||
|
||||
execSync(`powershell -Command "[Environment]::SetEnvironmentVariable('Path', '${newPath}', 'User')"`)
|
||||
|
||||
log("Added to PATH. Restart your terminal for changes to take effect.", colors.green)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to add to Windows PATH: ${error}`)
|
||||
}
|
||||
} else {
|
||||
// Unix-like (Linux, macOS, etc.)
|
||||
|
||||
const shellConfigFiles = [
|
||||
path.join(homedir(), ".bashrc"),
|
||||
path.join(homedir(), ".bash_profile"),
|
||||
path.join(homedir(), ".zshrc"),
|
||||
path.join(homedir(), ".profile"),
|
||||
]
|
||||
|
||||
const exportLine = `\nexport PATH="$PATH:${absolutePath}"\n`
|
||||
|
||||
try {
|
||||
// Determine which shell config file to use
|
||||
let targetFile: string | null = null
|
||||
|
||||
for (const file of shellConfigFiles) {
|
||||
try {
|
||||
await fs.access(file)
|
||||
targetFile = file
|
||||
break
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
// Default to .bashrc if none exist
|
||||
if (!targetFile) {
|
||||
targetFile = path.join(homedir(), ".bashrc")
|
||||
}
|
||||
|
||||
// Check if already present
|
||||
const content = await fs.readFile(targetFile, "utf-8")
|
||||
|
||||
if (content.includes(`PATH="$PATH:${absolutePath}"`)) {
|
||||
log("Directory already in PATH", colors.green)
|
||||
return
|
||||
}
|
||||
|
||||
// Append to file
|
||||
fs.appendFile(targetFile, exportLine)
|
||||
|
||||
log(`Added to PATH in ${targetFile}. Run 'source ${targetFile}' or restart your terminal.`, colors.green)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to configure PATH: ${error}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to configure PATH: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function installLocal() {
|
||||
logStep("1", "Checking operating system...")
|
||||
// Detect OS
|
||||
const platform: Platform = await detectOS()
|
||||
logStep("2", "Validating compatibility...")
|
||||
// Validate support
|
||||
await ValidateOS(platform)
|
||||
logSuccess(`Operating System: ${platform.name}-${platform.arch} is supported.`)
|
||||
// Get install directory
|
||||
logStep("3", "Getting install directory...")
|
||||
const installDirectory = await getInstallDirectory(platform)
|
||||
logSuccess(`Set install directory to: ${installDirectory}`)
|
||||
// Rebuild the binaries
|
||||
logStep("4", "Rebuilding binaries...")
|
||||
await rebuildCLI(platform)
|
||||
logSuccess("Binaries built.")
|
||||
// Rebuild standalone package
|
||||
logStep("5", "Rebuilding standalone package...")
|
||||
await rebuildClineCore()
|
||||
logSuccess("Standalone Package rebuilt.")
|
||||
// Remove existing Cline installation
|
||||
logStep("6", "Remove previous Cline CLI Installation")
|
||||
await removePreviousCLI(platform, installDirectory)
|
||||
// Create install directory
|
||||
logStep("7", "Ensuring install directory exists")
|
||||
await ensureDirectory(path.resolve(installDirectory, "bin"))
|
||||
logSuccess(`Validated install directory: ${installDirectory}`)
|
||||
// Copy standalone package first
|
||||
logStep("8", "Copying standalone package")
|
||||
await copyStandalone(installDirectory)
|
||||
logSuccess("Standalone package copied.")
|
||||
// Copy platform-specific modules
|
||||
logStep("9", `Installing platform-specific modules for ${platform.name}-${platform.arch}`)
|
||||
await copyPlatformModules(platform, installDirectory)
|
||||
logSuccess("Modules copied.")
|
||||
// Copy binaries
|
||||
logStep("10", "Copying platform binaries")
|
||||
await copyBinaries(platform, installDirectory)
|
||||
logSuccess("Binaries copied successfully.")
|
||||
// Use system node (via symlink)
|
||||
logStep("11", "Linking system node to Cline")
|
||||
const systemNodeVersion = await linkSystemNode(installDirectory)
|
||||
logSuccess("Link successful.")
|
||||
// Make binaries executable
|
||||
logStep("12", "Ensuring binaries are executable")
|
||||
await makeExecutable(platform, installDirectory)
|
||||
logSuccess("Files are executable.")
|
||||
// Rebuild better-sqlite3 for system node.js
|
||||
logStep("13", `Rebuilding native modules for Node.js version ${systemNodeVersion}`)
|
||||
await rebuildNativeModules(installDirectory)
|
||||
logSuccess("Native modules rebuilt.")
|
||||
// Configure system PATH
|
||||
logStep("14", "Configuring system PATH")
|
||||
await configurePATH(platform, installDirectory)
|
||||
logSuccess("Linked Cline CLI to PATH.")
|
||||
// Installation Complete
|
||||
logSeparator()
|
||||
log("Cline CLI has been installed!", colors.green)
|
||||
log("Now you're Cooking with Cline CLI!", colors.magenta)
|
||||
logSeparator()
|
||||
}
|
||||
|
||||
async function installProd() {
|
||||
// Get install directory
|
||||
// Set github Repo
|
||||
// Get requested version, default to latest
|
||||
// Check prerequisites
|
||||
// Check rate limit
|
||||
// Get requested release
|
||||
// Show info
|
||||
// Remove existing Cline installation
|
||||
// Download package
|
||||
// Inflate package to install directory
|
||||
// Validate
|
||||
// Configure system PATH
|
||||
// Installation Complete
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs()
|
||||
|
||||
logSeparator()
|
||||
log("Cline Installer", colors.bright)
|
||||
logSeparator()
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
if (args.local) {
|
||||
// Build both dev and prod
|
||||
log("\nInstalling for Local Development...", colors.cyan)
|
||||
|
||||
// Dev Install
|
||||
await installLocal()
|
||||
} else {
|
||||
await installProd()
|
||||
}
|
||||
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
|
||||
logSeparator()
|
||||
log(`Installed successfully in ${duration}s`, colors.green)
|
||||
logSeparator()
|
||||
} catch (error) {
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
|
||||
logSeparator()
|
||||
logError(`Install failed after ${duration}s`)
|
||||
logError((error as Error).message)
|
||||
logSeparator()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -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() {
|
||||
@@ -116,6 +66,7 @@ async function copyCliBinaries() {
|
||||
{ os: "darwin", arch: "amd64" },
|
||||
{ os: "linux", arch: "amd64" },
|
||||
{ os: "linux", arch: "arm64" },
|
||||
{ os: "win32", arch: "amd64" },
|
||||
]
|
||||
|
||||
const binDir = path.join(BUILD_DIR, "bin")
|
||||
@@ -539,9 +490,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 +509,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) => {
|
||||
|
||||
@@ -63,7 +63,7 @@ echo ""
|
||||
|
||||
# Test 4: Check for platform support
|
||||
echo "Test 4: Platform Support Check"
|
||||
platforms=("darwin-x64" "darwin-arm64" "linux-x64")
|
||||
platforms=("darwin-x64" "darwin-arm64" "linux-x64" "win-amd64")
|
||||
for platform in "${platforms[@]}"; do
|
||||
if grep -q "$platform" scripts/install.sh; then
|
||||
echo " ✅ PASS: Platform '$platform' supported"
|
||||
|
||||
+40
-13
@@ -24,6 +24,7 @@ import { telemetryService } from "./services/telemetry"
|
||||
import { PostHogClientProvider } from "./services/telemetry/providers/posthog/PostHogClientProvider"
|
||||
import { ShowMessageType } from "./shared/proto/host/window"
|
||||
import { getLatestAnnouncementId } from "./utils/announcements"
|
||||
import { arePathsEqual } from "./utils/path"
|
||||
/**
|
||||
* Performs intialization for Cline that is common to all platforms.
|
||||
*
|
||||
@@ -49,7 +50,7 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
|
||||
// Setup the external services
|
||||
await ErrorService.initialize()
|
||||
await featureFlagsService.poll()
|
||||
await featureFlagsService.poll(null)
|
||||
|
||||
// Migrate custom instructions to global Cline rules (one-time cleanup)
|
||||
await migrateCustomInstructionsToGlobalRules(context)
|
||||
@@ -76,19 +77,12 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
|
||||
await showVersionUpdateAnnouncement(context)
|
||||
|
||||
// Initialize banner service
|
||||
// Check if this workspace was opened from worktree quick launch
|
||||
await checkWorktreeAutoOpen(context)
|
||||
|
||||
// Initialize banner service (TEMPORARILY DISABLED - not fetching banners to prevent API hammering)
|
||||
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)
|
||||
})
|
||||
// DISABLED: .getActiveBanners(true)
|
||||
|
||||
telemetryService.captureExtensionActivated()
|
||||
|
||||
@@ -127,6 +121,39 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this workspace was opened from the worktree quick launch button.
|
||||
* If so, opens the Cline sidebar and clears the state.
|
||||
*/
|
||||
async function checkWorktreeAutoOpen(context: vscode.ExtensionContext): Promise<void> {
|
||||
try {
|
||||
// Read directly from globalState (not StateManager cache) since this may have been
|
||||
// set by another window right before this one opened
|
||||
const worktreeAutoOpenPath = context.globalState.get<string>("worktreeAutoOpenPath")
|
||||
if (!worktreeAutoOpenPath) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get current workspace path
|
||||
const workspacePaths = (await HostProvider.workspace.getWorkspacePaths({})).paths
|
||||
if (workspacePaths.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentPath = workspacePaths[0]
|
||||
|
||||
// Check if current workspace matches the worktree path
|
||||
if (arePathsEqual(currentPath, worktreeAutoOpenPath)) {
|
||||
// Clear the state first to prevent re-triggering
|
||||
await context.globalState.update("worktreeAutoOpenPath", undefined)
|
||||
// Open the Cline sidebar
|
||||
await HostProvider.workspace.openClineSidebarPanel({})
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error("Error checking worktree auto-open", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs cleanup when Cline is deactivated that is common to all platforms.
|
||||
*/
|
||||
|
||||
@@ -376,10 +376,14 @@ function createHandlerForProvider(
|
||||
return new VercelAIGatewayHandler({
|
||||
onRetryAttempt: options.onRetryAttempt,
|
||||
vercelAiGatewayApiKey: options.vercelAiGatewayApiKey,
|
||||
openRouterModelId: mode === "plan" ? options.planModeOpenRouterModelId : options.actModeOpenRouterModelId,
|
||||
openRouterModelInfo: mode === "plan" ? options.planModeOpenRouterModelInfo : options.actModeOpenRouterModelInfo,
|
||||
openRouterModelId:
|
||||
mode === "plan" ? options.planModeVercelAiGatewayModelId : options.actModeVercelAiGatewayModelId,
|
||||
openRouterModelInfo:
|
||||
mode === "plan" ? options.planModeVercelAiGatewayModelInfo : options.actModeVercelAiGatewayModelInfo,
|
||||
reasoningEffort: mode === "plan" ? options.planModeReasoningEffort : options.actModeReasoningEffort,
|
||||
thinkingBudgetTokens:
|
||||
mode === "plan" ? options.planModeThinkingBudgetTokens : options.actModeThinkingBudgetTokens,
|
||||
geminiThinkingLevel: mode === "plan" ? options.geminiPlanModeThinkingLevel : options.geminiActModeThinkingLevel,
|
||||
})
|
||||
case "zai":
|
||||
return new ZAiHandler({
|
||||
|
||||
@@ -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",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user