mirror of
https://github.com/cline/cline.git
synced 2026-09-01 23:19:18 +08:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d0f624f84 | |||
| e68d55a402 | |||
| c093ca1760 | |||
| dc85299a73 | |||
| 4533ed3ea8 | |||
| 66d7664d1b | |||
| 50aeb7098f | |||
| c2e6eafb45 | |||
| 0220628c39 | |||
| 5052220195 | |||
| abf3081e56 | |||
| 125cb78a31 | |||
| 17539228ea | |||
| bdb7cc36fa | |||
| d3e1065707 | |||
| 6303951f20 | |||
| fc184e07a2 | |||
| e88f4ea412 | |||
| 133ade3f1a | |||
| 857411c035 | |||
| c59f068584 | |||
| f7561c31fa | |||
| f0a97dafc8 | |||
| d422f689a6 | |||
| 00efcc1c2b |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add claude 4.5 haiku
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
improve cline command permission validation logic. add cline command permission man page documentation
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add code-simplifier skill for refining code clarity and maintainability
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: finalize document content during approval flow
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Expose --version in cline cli command
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Log Persistence errors to PostHog
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
feat: add OpenAI Codex (ChatGPT Plus/Pro) provider
|
||||
|
||||
Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
|
||||
|
||||
Models available:
|
||||
- gpt-5.2-codex (default)
|
||||
- gpt-5.1-codex-max
|
||||
- gpt-5.1-codex-mini
|
||||
- gpt-5.2
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"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.
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
docs: fix outdated Ollama model names in documentation
|
||||
|
||||
Updated recommended Ollama models to use correct identifiers:
|
||||
- Changed qwen3-coder-30b to qwen2.5-coder:32b
|
||||
- Changed devstral-small to codellama:34b-code
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
OpenAI GPT-5 Codex models are now using Apply Patch tool for diff edits.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"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
|
||||
---
|
||||
|
||||
fix: add chat output on skill use
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Adding telemetry for background exec terminal
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
This pull request introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness. The feature allows users to seamlessly work with Jupyter notebooks using Cline's AI capabilities while preserving the notebook's JSON structure.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Remove deprecated zai-glm-4.6 model from Cerebras provider
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Wiring-up "conditionals" for Cline Rules files.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: removes retry message from UI after retry succeeds
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Support native tool calling for LM Studio and Ollama provider
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
add claude 4.5 opus into sap provider.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix drag & drop files from SSH remote workspaces into chat
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Limite Vertex and LiteLLM options when they're remote configured
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix crash when the Context Menu has a type but no options
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Updated switch component to adhere to 3:1 contrast accessibility standard for both light and dark vscode themes
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Throttle the remote config fetch
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Improve history view filter menu
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"cline": patch
|
||||
---
|
||||
|
||||
Add git worktree management UI for running parallel Cline sessions
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Remove hooks feature setting and make it always enabled (except on Windows).
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: code-simplifier
|
||||
description: Simplify and refine code for clarity, consistency, and maintainability while preserving all functionality. Focuses on recently modified code unless instructed otherwise.
|
||||
---
|
||||
|
||||
You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer.
|
||||
|
||||
You will analyze recently modified code and apply refinements that:
|
||||
|
||||
1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact.
|
||||
|
||||
2. **Apply Project Standards**: Follow the established coding standards from the project's configuration files (e.g., `.clinerules/`, `.editorconfig`, linter configs, README, CONTRIBUTING) including patterns like:
|
||||
- Consistent import/include ordering and organization
|
||||
- Idiomatic function and method declaration style for the language
|
||||
- Explicit type annotations where the language supports them
|
||||
- Framework-appropriate patterns and conventions
|
||||
- Language-idiomatic error handling
|
||||
- Consistent naming conventions (camelCase, snake_case, etc.)
|
||||
|
||||
3. **Enhance Clarity**: Simplify code structure by:
|
||||
- Reducing unnecessary complexity and nesting
|
||||
- Eliminating redundant code and abstractions
|
||||
- Improving readability through clear variable and function names
|
||||
- Consolidating related logic
|
||||
- Removing unnecessary comments that describe obvious code
|
||||
- IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions
|
||||
- Choose clarity over brevity - explicit code is often better than overly compact code
|
||||
|
||||
4. **Maintain Balance**: Avoid over-simplification that could:
|
||||
- Reduce code clarity or maintainability
|
||||
- Create overly clever solutions that are hard to understand
|
||||
- Combine too many concerns into single functions or components
|
||||
- Remove helpful abstractions that improve code organization
|
||||
- Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners)
|
||||
- Make the code harder to debug or extend
|
||||
|
||||
5. **Focus Scope**: Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope.
|
||||
|
||||
Your refinement process:
|
||||
|
||||
1. Identify the recently modified code sections
|
||||
2. Analyze for opportunities to improve elegance and consistency
|
||||
3. Apply project-specific best practices and coding standards
|
||||
4. Ensure all functionality remains unchanged
|
||||
5. Verify the refined code is simpler and more maintainable
|
||||
6. Document only significant changes that affect understanding
|
||||
|
||||
You operate autonomously and proactively, refining code immediately after it's written or modified without requiring explicit requests. Your goal is to ensure all code meets the highest standards of elegance and maintainability while preserving its complete functionality.
|
||||
@@ -187,20 +187,11 @@ jobs:
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
cache-dependency-path: cli/go.sum
|
||||
|
||||
- name: Build CLI binaries
|
||||
run: npm run compile-cli-all-platforms
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
run: npm run download-ripgrep
|
||||
|
||||
- name: Compile NPM package
|
||||
run: npm run compile-standalone-npm
|
||||
- name: Compile Standalone
|
||||
run: npm run compile-standalone
|
||||
|
||||
- name: Install testing platform dependencies
|
||||
if: steps.testing-platform-cache.outputs.cache-hit != 'true'
|
||||
@@ -213,7 +204,7 @@ jobs:
|
||||
# This prevents the job from showing as failed and avoids distracting developers
|
||||
# until the integration tests are ready to be enforced.
|
||||
run: |
|
||||
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage || true
|
||||
npm run test:tp-orchestrator -- tests/specs/ --count=1 --coverage
|
||||
|
||||
- name: Save Coverage Reports
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
+15
-2
@@ -1,10 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## [3.52.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Users with ChatGPT Plus or Pro subscriptions can now use GPT-5 models directly through Cline without needing an API key. Authentication is handled via OAuth through OpenAI's authentication system.
|
||||
- Grok models are now moving out of free tier and into paid plans.
|
||||
- Introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness.
|
||||
|
||||
### Fixed
|
||||
- Bugs in DiffViewProvider for file editing
|
||||
- Ollama's recommended models to use correct identifiers
|
||||
|
||||
|
||||
## [3.51.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Adding OpenAI gpt-5.2-codex model to the model picker
|
||||
- Adding OpenAI gpt-5.2-codex model to the model picker
|
||||
|
||||
## [3.50.0]
|
||||
|
||||
@@ -1737,4 +1750,4 @@ Add Opus 4.1 through Claude Code
|
||||
|
||||
## [0.0.6]
|
||||
|
||||
- Initial release
|
||||
- Initial release
|
||||
|
||||
+23
@@ -147,6 +147,29 @@
|
||||
"src/dev/grit/vscode-api.grit"
|
||||
]
|
||||
},
|
||||
{
|
||||
// Do not use console logging directly, use the Logger service instead.
|
||||
"plugins": [
|
||||
"src/dev/grit/console-log.grit"
|
||||
],
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/webview-ui/**",
|
||||
"!**/evals/**",
|
||||
"!**/standalone/**",
|
||||
"!**/e2e/**",
|
||||
"!**/test/**",
|
||||
"!**/__tests__/**",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.stories.ts",
|
||||
"!src/dev/**",
|
||||
"!**/*.mjs",
|
||||
"!**/*.js",
|
||||
"!**/scripts/**",
|
||||
"!**/*.tsx",
|
||||
"!**/testing-platform/**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"includes": [
|
||||
"**",
|
||||
|
||||
+76
-87
@@ -10,11 +10,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cline/cli/pkg/cli"
|
||||
"github.com/cline/cli/pkg/cli/auth"
|
||||
"github.com/cline/cli/pkg/cli/display"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/output"
|
||||
"github.com/cline/cli/pkg/cli/slash"
|
||||
"github.com/cline/cli/pkg/common"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -71,8 +72,6 @@ see the manual page: man cline`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
var instanceAddress string
|
||||
|
||||
// Validate workspace paths exist
|
||||
if err := common.ValidateDirsExist(workspaces); err != nil {
|
||||
return err
|
||||
@@ -89,13 +88,13 @@ see the manual page: man cline`,
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("Starting new Cline instance...")
|
||||
}
|
||||
instance, err := global.Clients.StartNewInstance(ctx, allWorkspaces...)
|
||||
instance, err := global.Instances.StartNewInstance(ctx, allWorkspaces...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start new instance: %w", err)
|
||||
}
|
||||
instanceAddress = instance.Address
|
||||
global.Config.CoreAddress = instance.CoreAddress
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("Started instance at %s\n\n", instanceAddress)
|
||||
fmt.Printf("Started instance at %s\n\n", global.Config.CoreAddress)
|
||||
}
|
||||
|
||||
// Set up cleanup on exit
|
||||
@@ -103,38 +102,35 @@ see the manual page: man cline`,
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("\nCleaning up instance...")
|
||||
}
|
||||
registry := global.Clients.GetRegistry()
|
||||
if err := global.KillInstanceByAddress(context.Background(), registry, instanceAddress); err != nil {
|
||||
registry := global.Instances.GetRegistry()
|
||||
if err := global.KillInstanceByAddress(context.Background(), registry, global.Config.CoreAddress); err != nil {
|
||||
if global.Config.Verbose {
|
||||
fmt.Printf("Warning: Failed to clean up instance: %v\n", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Check if user has credentials configured
|
||||
if !isUserReadyToUse(ctx, instanceAddress) {
|
||||
// Create renderer for welcome messages
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
fmt.Printf("\n%s\n\n", renderer.Dim("Hey there! Looks like you're new here. Let's get you set up"))
|
||||
// Check if user has credentials configured
|
||||
if !isUserReadyToUse(ctx) {
|
||||
// Create renderer for welcome messages
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
fmt.Printf("\n%s\n\n", renderer.Dim("Hey there! Looks like you're new here. Let's get you set up"))
|
||||
|
||||
if err := auth.HandleAuthMenuNoArgs(ctx); err != nil {
|
||||
// Check if user cancelled - exit cleanly
|
||||
if err == huh.ErrUserAborted {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("auth setup failed: %w", err)
|
||||
if err := auth.HandleAuthMenuNoArgs(ctx); err != nil {
|
||||
// Check if user cancelled - exit cleanly
|
||||
if err == huh.ErrUserAborted {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Re-check after auth wizard
|
||||
if !isUserReadyToUse(ctx, instanceAddress) {
|
||||
return fmt.Errorf("credentials still not configured - please run 'cline auth' to complete setup")
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s\n\n", renderer.Dim("✓ Setup complete, you can now use the Cline CLI"))
|
||||
return fmt.Errorf("auth setup failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
// User specified --address flag, use that
|
||||
instanceAddress = coreAddress
|
||||
|
||||
// Re-check after auth wizard
|
||||
if !isUserReadyToUse(ctx) {
|
||||
return fmt.Errorf("credentials still not configured - please run 'cline auth' to complete setup")
|
||||
}
|
||||
|
||||
fmt.Printf("\n%s\n\n", renderer.Dim("✓ Setup complete, you can now use the Cline CLI"))
|
||||
}
|
||||
|
||||
// Get content from both args and stdin
|
||||
@@ -143,10 +139,13 @@ see the manual page: man cline`,
|
||||
return fmt.Errorf("failed to read prompt: %w", err)
|
||||
}
|
||||
|
||||
// If no prompt from args or stdin, show interactive input
|
||||
if prompt == "" {
|
||||
// If no prompt (or just a mode switch with no message), show interactive input
|
||||
// Loop to allow mode switches without a message
|
||||
bannerShown := false
|
||||
for prompt == "" {
|
||||
// Pass the mode flag and workspaces to banner so it shows correct info
|
||||
prompt, err = promptForInitialTask(ctx, instanceAddress, mode, allWorkspaces)
|
||||
prompt, err = promptForInitialTask(ctx, mode, allWorkspaces, !bannerShown)
|
||||
bannerShown = true
|
||||
if err != nil {
|
||||
// Check if user cancelled - exit cleanly without error
|
||||
if err == huh.ErrUserAborted {
|
||||
@@ -154,6 +153,23 @@ see the manual page: man cline`,
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if user entered a mode switch command
|
||||
if newMode, remaining, isModeSwitch := slash.ParseModeSwitch(prompt); isModeSwitch {
|
||||
mode = newMode
|
||||
prompt = remaining
|
||||
// If just a mode switch with no message, continue loop to re-prompt
|
||||
if prompt == "" {
|
||||
renderer := display.NewRenderer(global.Config.OutputFormat)
|
||||
if mode == "act" {
|
||||
fmt.Printf("\n%s\n\n", renderer.Success("Switched to act mode"))
|
||||
} else {
|
||||
fmt.Printf("\n%s\n\n", renderer.Success("Switched to plan mode"))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if prompt == "" {
|
||||
return fmt.Errorf("prompt required")
|
||||
}
|
||||
@@ -171,7 +187,7 @@ see the manual page: man cline`,
|
||||
Mode: mode,
|
||||
Settings: settings,
|
||||
Yolo: yolo,
|
||||
Address: instanceAddress,
|
||||
Address: global.Config.CoreAddress,
|
||||
Verbose: verbose,
|
||||
Workspaces: allWorkspaces,
|
||||
})
|
||||
@@ -207,51 +223,30 @@ see the manual page: man cline`,
|
||||
}
|
||||
}
|
||||
|
||||
func promptForInitialTask(ctx context.Context, instanceAddress, modeFlag string, workspaces []string) (string, error) {
|
||||
// Show session banner before the initial input
|
||||
showSessionBanner(ctx, instanceAddress, modeFlag, workspaces)
|
||||
|
||||
var prompt string
|
||||
|
||||
// Create custom theme with mode-colored cursor and title
|
||||
theme := huh.ThemeCharm()
|
||||
|
||||
// Set cursor and title color based on mode
|
||||
modeColor := lipgloss.Color("3") // Yellow for plan
|
||||
if modeFlag == "act" {
|
||||
modeColor = lipgloss.Color("39") // Blue for act
|
||||
func promptForInitialTask(ctx context.Context, modeFlag string, workspaces []string, showBanner bool) (string, error) {
|
||||
// Show session banner before the initial input (only on first prompt)
|
||||
if showBanner {
|
||||
showSessionBanner(ctx, modeFlag, workspaces)
|
||||
}
|
||||
|
||||
theme.Focused.TextInput.Cursor = theme.Focused.TextInput.Cursor.Foreground(modeColor)
|
||||
theme.Focused.Title = theme.Focused.Title.Foreground(modeColor)
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewText().
|
||||
Title("Start a new Cline task").
|
||||
Description("What would you like Cline to help you with?").
|
||||
Placeholder("e.g., Create a REST API with authentication...").
|
||||
Lines(5).
|
||||
Value(&prompt),
|
||||
),
|
||||
).WithWidth(48).WithTheme(theme)
|
||||
|
||||
err := form.Run()
|
||||
prompt, err := output.PromptForInitialTask(
|
||||
"Start a new Cline task",
|
||||
"/plan or /act to switch modes\ntab to autocomplete commands\nctrl+e to open editor\nctrl+c to exit",
|
||||
modeFlag,
|
||||
slash.NewRegistry(ctx),
|
||||
)
|
||||
if err != nil {
|
||||
// Check if user cancelled with Control-C
|
||||
if err == huh.ErrUserAborted {
|
||||
// Return a special error that indicates clean cancellation
|
||||
// This allows deferred cleanup to run
|
||||
if err == output.ErrUserAborted {
|
||||
return "", huh.ErrUserAborted
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.TrimSpace(prompt), nil
|
||||
return prompt, nil
|
||||
}
|
||||
|
||||
// showSessionBanner displays session info before initial prompt
|
||||
func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string, workspaces []string) {
|
||||
func showSessionBanner(ctx context.Context, modeFlag string, workspaces []string) {
|
||||
bannerInfo := display.BannerInfo{
|
||||
Version: global.CliVersion,
|
||||
Mode: modeFlag, // Use the mode from command flag, not state
|
||||
@@ -265,21 +260,18 @@ func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string, wo
|
||||
bannerInfo.Workdirs = workspaces
|
||||
|
||||
// Get provider/model using auth functions (same logic as auth menu)
|
||||
manager, err := cli.NewTaskManagerForAddress(ctx, instanceAddress)
|
||||
if err == nil {
|
||||
if providerList, err := auth.GetProviderConfigurations(ctx, manager); err == nil {
|
||||
// Show provider/model for the mode we'll be using
|
||||
var providerDisplay *auth.ProviderDisplay
|
||||
if bannerInfo.Mode == "plan" && providerList.PlanProvider != nil {
|
||||
providerDisplay = providerList.PlanProvider
|
||||
} else if bannerInfo.Mode == "act" && providerList.ActProvider != nil {
|
||||
providerDisplay = providerList.ActProvider
|
||||
}
|
||||
if providerList, err := auth.GetProviderConfigurations(ctx); err == nil {
|
||||
// Show provider/model for the mode we'll be using
|
||||
var providerDisplay *auth.ProviderDisplay
|
||||
if bannerInfo.Mode == "plan" && providerList.PlanProvider != nil {
|
||||
providerDisplay = providerList.PlanProvider
|
||||
} else if bannerInfo.Mode == "act" && providerList.ActProvider != nil {
|
||||
providerDisplay = providerList.ActProvider
|
||||
}
|
||||
|
||||
if providerDisplay != nil {
|
||||
bannerInfo.Provider = auth.GetProviderIDForEnum(providerDisplay.Provider)
|
||||
bannerInfo.ModelID = providerDisplay.ModelID
|
||||
}
|
||||
if providerDisplay != nil {
|
||||
bannerInfo.Provider = auth.GetProviderIDForEnum(providerDisplay.Provider)
|
||||
bannerInfo.ModelID = providerDisplay.ModelID
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,25 +284,22 @@ func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string, wo
|
||||
// isUserReadyToUse checks if the user has completed initial setup
|
||||
// Returns true if welcomeViewCompleted flag is set OR user is authenticated
|
||||
// Matches extension logic: welcomeViewCompleted = Boolean(globalState.welcomeViewCompleted || user?.uid)
|
||||
func isUserReadyToUse(ctx context.Context, instanceAddress string) bool {
|
||||
manager, err := cli.NewTaskManagerForAddress(ctx, instanceAddress)
|
||||
func isUserReadyToUse(ctx context.Context) bool {
|
||||
grpcClient, err := global.GetClientForAddress(ctx, global.Config.CoreAddress)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get state
|
||||
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
|
||||
state, err := grpcClient.State.GetLatestState(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse state JSON
|
||||
stateMap := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(state.StateJson), &stateMap); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check 1: welcomeViewCompleted flag
|
||||
if welcomeCompleted, ok := stateMap["welcomeViewCompleted"].(bool); ok && welcomeCompleted {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestMultiInstanceDefaultUnchanged(t *testing.T) {
|
||||
if len(out1.CoreInstances) != 1 {
|
||||
t.Fatalf("expected 1 instance, got %d", len(out1.CoreInstances))
|
||||
}
|
||||
firstAddr := out1.CoreInstances[0].Address
|
||||
firstAddr := out1.CoreInstances[0].CoreAddress
|
||||
waitForAddressHealthy(t, firstAddr, defaultTimeout)
|
||||
|
||||
// Start second instance
|
||||
@@ -56,29 +56,29 @@ func TestDefaultJsonUpdateAfterRemoval(t *testing.T) {
|
||||
|
||||
// Choose second as new default
|
||||
target := out.CoreInstances[1]
|
||||
waitForAddressHealthy(t, target.Address, defaultTimeout)
|
||||
waitForAddressHealthy(t, target.CoreAddress, defaultTimeout)
|
||||
|
||||
// Set as default
|
||||
_ = mustRunCLI(ctx, t, "instance", "use", target.Address)
|
||||
_ = mustRunCLI(ctx, t, "instance", "use", target.CoreAddress)
|
||||
|
||||
// Verify default switched
|
||||
out = listInstancesJSON(ctx, t)
|
||||
if out.DefaultInstance != target.Address {
|
||||
t.Fatalf("default_instance not updated to %s (got %s)", target.Address, out.DefaultInstance)
|
||||
if out.DefaultInstance != target.CoreAddress {
|
||||
t.Fatalf("default_instance not updated to %s (got %s)", target.CoreAddress, out.DefaultInstance)
|
||||
}
|
||||
|
||||
// Kill the default instance using runtime PID discovery
|
||||
corePID := getCorePID(t, target.Address)
|
||||
corePID := getCorePID(t, target.CoreAddress)
|
||||
if corePID <= 0 {
|
||||
t.Fatalf("could not find PID for core process at %s", target.Address)
|
||||
t.Fatalf("could not find PID for core process at %s", target.CoreAddress)
|
||||
}
|
||||
t.Logf("Killing cline-core process PID %d for instance %s", corePID, target.Address)
|
||||
t.Logf("Killing cline-core process PID %d for instance %s", corePID, target.CoreAddress)
|
||||
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
|
||||
t.Fatalf("kill pid %d: %v", corePID, err)
|
||||
}
|
||||
|
||||
// Wait for removal
|
||||
waitForAddressRemoved(t, target.Address, longTimeout)
|
||||
waitForAddressRemoved(t, target.CoreAddress, longTimeout)
|
||||
|
||||
// Clean up dangling host process (SIGKILL leaves these behind by design)
|
||||
t.Logf("Cleaning up dangling host process on port %d", target.HostPort())
|
||||
@@ -91,7 +91,7 @@ func TestDefaultJsonUpdateAfterRemoval(t *testing.T) {
|
||||
if len(out.CoreInstances) > 0 {
|
||||
found := false
|
||||
for _, it := range out.CoreInstances {
|
||||
if out.DefaultInstance == it.Address {
|
||||
if out.DefaultInstance == it.CoreAddress {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func listInstancesJSON(ctx context.Context, t *testing.T) common.InstancesOutput
|
||||
|
||||
func hasAddress(in common.InstancesOutput, addr string) bool {
|
||||
for _, it := range in.CoreInstances {
|
||||
if it.Address == addr {
|
||||
if it.CoreAddress == addr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func hasAddress(in common.InstancesOutput, addr string) bool {
|
||||
|
||||
func getByAddress(in common.InstancesOutput, addr string) (common.CoreInstanceInfo, bool) {
|
||||
for _, it := range in.CoreInstances {
|
||||
if it.Address == addr {
|
||||
if it.CoreAddress == addr {
|
||||
return it, true
|
||||
}
|
||||
}
|
||||
@@ -313,7 +313,7 @@ func getCorePIDViaRPC(t *testing.T, address string) int {
|
||||
defer cancel()
|
||||
|
||||
// Get client for the address
|
||||
client, err := global.Clients.GetRegistry().GetClient(ctx, address)
|
||||
client, err := global.Instances.GetRegistry().GetClient(ctx, address)
|
||||
if err != nil {
|
||||
t.Logf("Warning: failed to get client for %s, falling back to lsof: %v", address, err)
|
||||
return getCorePIDViaLsof(t, address)
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestMixedLocalhostVs127Coexist(t *testing.T) {
|
||||
t.Fatalf("expected at least 1 instance")
|
||||
}
|
||||
inst := out.CoreInstances[0]
|
||||
waitForAddressHealthy(t, inst.Address, defaultTimeout)
|
||||
waitForAddressHealthy(t, inst.CoreAddress, defaultTimeout)
|
||||
|
||||
// Manually add a SQLite entry for the same port but 127.0.0.1 host
|
||||
addr127 := fmt.Sprintf("127.0.0.1:%d", inst.CorePort())
|
||||
@@ -37,12 +37,12 @@ func TestMixedLocalhostVs127Coexist(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify both addresses appear and are healthy
|
||||
waitForAddressHealthy(t, inst.Address, defaultTimeout)
|
||||
waitForAddressHealthy(t, inst.CoreAddress, defaultTimeout)
|
||||
waitForAddressHealthy(t, addr127, defaultTimeout)
|
||||
|
||||
out = listInstancesJSON(ctx, t)
|
||||
if !hasAddress(out, inst.Address) || !hasAddress(out, addr127) {
|
||||
t.Fatalf("expected both %s and %s present", inst.Address, addr127)
|
||||
if !hasAddress(out, inst.CoreAddress) || !hasAddress(out, addr127) {
|
||||
t.Fatalf("expected both %s and %s present", inst.CoreAddress, addr127)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestStartStopStress(t *testing.T) {
|
||||
before := listInstancesJSON(ctx, t)
|
||||
beforeSet := map[string]struct{}{}
|
||||
for _, it := range before.CoreInstances {
|
||||
beforeSet[it.Address] = struct{}{}
|
||||
beforeSet[it.CoreAddress] = struct{}{}
|
||||
}
|
||||
|
||||
// Start a new instance
|
||||
@@ -69,8 +69,8 @@ func TestStartStopStress(t *testing.T) {
|
||||
waitFor(t, defaultTimeout, func() (bool, string) {
|
||||
after := listInstancesJSON(ctx, t)
|
||||
for _, it := range after.CoreInstances {
|
||||
if _, ok := beforeSet[it.Address]; !ok {
|
||||
newAddr = it.Address
|
||||
if _, ok := beforeSet[it.CoreAddress]; !ok {
|
||||
newAddr = it.CoreAddress
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
@@ -88,12 +88,12 @@ func TestStartStopStress(t *testing.T) {
|
||||
}
|
||||
|
||||
// Get PID using runtime discovery
|
||||
corePID := getCorePID(t, info.Address)
|
||||
corePID := getCorePID(t, info.CoreAddress)
|
||||
if corePID <= 0 {
|
||||
t.Fatalf("could not find PID for new instance at %s", info.Address)
|
||||
t.Fatalf("could not find PID for new instance at %s", info.CoreAddress)
|
||||
}
|
||||
|
||||
t.Logf("Killing new instance %s (PID %d) for iteration %d", info.Address, corePID, i)
|
||||
t.Logf("Killing new instance %s (PID %d) for iteration %d", info.CoreAddress, corePID, i)
|
||||
if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
|
||||
t.Fatalf("kill pid %d: %v", corePID, err)
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func readInstancesFromSQLite(t *testing.T, clineDir string) []common.CoreInstanc
|
||||
|
||||
// Create InstanceInfo
|
||||
info := common.CoreInstanceInfo{
|
||||
Address: heldBy,
|
||||
CoreAddress: heldBy,
|
||||
HostServiceAddress: lockTarget,
|
||||
Status: grpc_health_v1.HealthCheckResponse_UNKNOWN, // Will be updated by health check
|
||||
LastSeen: time.Unix(lockedAt/1000, 0), // Convert from milliseconds
|
||||
|
||||
+14
-14
@@ -29,7 +29,7 @@ func TestStartAndList(t *testing.T) {
|
||||
t.Fatalf("expected 1 instance, got %d", len(out.CoreInstances))
|
||||
}
|
||||
|
||||
addr := out.CoreInstances[0].Address
|
||||
addr := out.CoreInstances[0].CoreAddress
|
||||
t.Logf("Instance address: %s, status: %s", addr, out.CoreInstances[0].Status)
|
||||
|
||||
t.Logf("Waiting for address %s to become healthy...", addr)
|
||||
@@ -44,8 +44,8 @@ func TestStartAndList(t *testing.T) {
|
||||
if out.DefaultInstance == "" {
|
||||
t.Fatalf("default_instance not set")
|
||||
}
|
||||
if out.DefaultInstance != out.CoreInstances[0].Address {
|
||||
t.Fatalf("expected default_instance=%s, got %s", out.CoreInstances[0].Address, out.DefaultInstance)
|
||||
if out.DefaultInstance != out.CoreInstances[0].CoreAddress {
|
||||
t.Fatalf("expected default_instance=%s, got %s", out.CoreInstances[0].CoreAddress, out.DefaultInstance)
|
||||
}
|
||||
|
||||
t.Logf("TestStartAndList completed successfully")
|
||||
@@ -64,7 +64,7 @@ func TestTaskNewDefault(t *testing.T) {
|
||||
if len(out.CoreInstances) != 1 {
|
||||
t.Fatalf("expected 1 instance, got %d", len(out.CoreInstances))
|
||||
}
|
||||
addr := out.CoreInstances[0].Address
|
||||
addr := out.CoreInstances[0].CoreAddress
|
||||
waitForAddressHealthy(t, addr, defaultTimeout)
|
||||
|
||||
// Create a new task at default (success is sufficient)
|
||||
@@ -108,21 +108,21 @@ func TestCrashCleanup(t *testing.T) {
|
||||
|
||||
// Test 1: Graceful shutdown (SIGTERM) - should clean up both processes
|
||||
gracefulTarget := out.CoreInstances[0]
|
||||
waitForAddressHealthy(t, gracefulTarget.Address, defaultTimeout)
|
||||
waitForAddressHealthy(t, gracefulTarget.CoreAddress, defaultTimeout)
|
||||
|
||||
// Get PID using runtime discovery
|
||||
gracefulPID := getCorePID(t, gracefulTarget.Address)
|
||||
gracefulPID := getCorePID(t, gracefulTarget.CoreAddress)
|
||||
if gracefulPID <= 0 {
|
||||
t.Fatalf("could not find PID for graceful target at %s", gracefulTarget.Address)
|
||||
t.Fatalf("could not find PID for graceful target at %s", gracefulTarget.CoreAddress)
|
||||
}
|
||||
|
||||
t.Logf("Testing graceful shutdown (SIGTERM) for instance %s (PID %d)", gracefulTarget.Address, gracefulPID)
|
||||
t.Logf("Testing graceful shutdown (SIGTERM) for instance %s (PID %d)", gracefulTarget.CoreAddress, gracefulPID)
|
||||
if err := syscall.Kill(gracefulPID, syscall.SIGTERM); err != nil {
|
||||
t.Fatalf("kill SIGTERM pid %d: %v", gracefulPID, err)
|
||||
}
|
||||
|
||||
// Wait for registry cleanup
|
||||
waitForAddressRemoved(t, gracefulTarget.Address, longTimeout)
|
||||
waitForAddressRemoved(t, gracefulTarget.CoreAddress, longTimeout)
|
||||
|
||||
// Verify both core and host ports are freed (no dangling processes)
|
||||
waitForPortsClosed(t, gracefulTarget.CorePort(), gracefulTarget.HostPort(), defaultTimeout)
|
||||
@@ -132,21 +132,21 @@ func TestCrashCleanup(t *testing.T) {
|
||||
|
||||
// Test 2: Crash cleanup (SIGKILL) - creates dangling host process that we must clean up
|
||||
crashTarget := out.CoreInstances[1]
|
||||
waitForAddressHealthy(t, crashTarget.Address, defaultTimeout)
|
||||
waitForAddressHealthy(t, crashTarget.CoreAddress, defaultTimeout)
|
||||
|
||||
// Get PID using runtime discovery
|
||||
crashPID := getCorePID(t, crashTarget.Address)
|
||||
crashPID := getCorePID(t, crashTarget.CoreAddress)
|
||||
if crashPID <= 0 {
|
||||
t.Fatalf("could not find PID for crash target at %s", crashTarget.Address)
|
||||
t.Fatalf("could not find PID for crash target at %s", crashTarget.CoreAddress)
|
||||
}
|
||||
|
||||
t.Logf("Testing crash cleanup (SIGKILL) for instance %s (PID %d)", crashTarget.Address, crashPID)
|
||||
t.Logf("Testing crash cleanup (SIGKILL) for instance %s (PID %d)", crashTarget.CoreAddress, crashPID)
|
||||
if err := syscall.Kill(crashPID, syscall.SIGKILL); err != nil {
|
||||
t.Fatalf("kill SIGKILL pid %d: %v", crashPID, err)
|
||||
}
|
||||
|
||||
// Wait for registry cleanup
|
||||
waitForAddressRemoved(t, crashTarget.Address, longTimeout)
|
||||
waitForAddressRemoved(t, crashTarget.CoreAddress, longTimeout)
|
||||
|
||||
// Verify the instance is removed from SQLite (no file to check anymore)
|
||||
// The waitForAddressRemoved already confirms the instance is gone from the registry
|
||||
|
||||
@@ -298,6 +298,64 @@ cline task view
|
||||
\f[I]# Start interactive chat with this task\f[R]
|
||||
cline task chat
|
||||
.EE
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
\f[B]CLINE_COMMAND_PERMISSIONS\f[R]
|
||||
JSON configuration for restricting which shell commands Cline can
|
||||
execute.
|
||||
When set, commands are validated against allow/deny patterns before
|
||||
execution.
|
||||
When not set, all commands are allowed (backward compatibility).
|
||||
.RS
|
||||
.PP
|
||||
Format:
|
||||
\f[CR]{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}\f[R]
|
||||
.PP
|
||||
\f[B]Fields:\f[R]
|
||||
.IP \(bu 2
|
||||
\f[B]allow\f[R] (array of strings): Glob patterns for allowed commands.
|
||||
If specified, only matching commands are permitted.
|
||||
Uses \f[CR]*\f[R] to match any characters and \f[CR]?\f[R] to match a
|
||||
single character.
|
||||
.IP \(bu 2
|
||||
\f[B]deny\f[R] (array of strings): Glob patterns for denied commands.
|
||||
Deny rules take precedence over allow rules.
|
||||
.IP \(bu 2
|
||||
\f[B]allowRedirects\f[R] (boolean): Whether to allow shell redirects
|
||||
(\f[CR]>\f[R], \f[CR]>>\f[R], \f[CR]<\f[R], etc.).
|
||||
Defaults to false.
|
||||
.PP
|
||||
\f[B]Rule evaluation:\f[R]
|
||||
.IP "1." 3
|
||||
Check for dangerous characters (backticks outside single quotes,
|
||||
unquoted newlines)
|
||||
.IP "2." 3
|
||||
Parse command into segments split by operators (\f[CR]&&\f[R],
|
||||
\f[CR]||\f[R], \f[CR]|\f[R], \f[CR];\f[R])
|
||||
.IP "3." 3
|
||||
If redirects detected and \f[CR]allowRedirects\f[R] is not true, command
|
||||
is denied
|
||||
.IP "4." 3
|
||||
Each segment is validated against deny rules first, then allow rules
|
||||
.IP "5." 3
|
||||
Subshell contents (\f[CR]$(...)\f[R] and \f[CR](...)\f[R]) are
|
||||
recursively validated
|
||||
.IP "6." 3
|
||||
All segments must pass for the command to be allowed
|
||||
.PP
|
||||
\f[B]Examples:\f[R]
|
||||
.IP
|
||||
.EX
|
||||
\f[I]# Allow only npm and git commands\f[R]
|
||||
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["npm *", "git *"]}\(aq
|
||||
|
||||
\f[I]# Allow development commands but deny dangerous ones\f[R]
|
||||
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["npm *", "git *", "node *"], "deny": ["rm \-rf *", "sudo *"]}\(aq
|
||||
|
||||
\f[I]# Allow file operations with redirects\f[R]
|
||||
export CLINE_COMMAND_PERMISSIONS=\(aq{"allow": ["cat *", "echo *"], "allowRedirects": true}\(aq
|
||||
.EE
|
||||
.RE
|
||||
.SH ARCHITECTURE
|
||||
Cline operates on a three\-layer architecture:
|
||||
.TP
|
||||
|
||||
@@ -323,6 +323,42 @@ cline task view
|
||||
cline task chat
|
||||
```
|
||||
|
||||
# ENVIRONMENT
|
||||
|
||||
**CLINE_COMMAND_PERMISSIONS**
|
||||
|
||||
: JSON configuration for restricting which shell commands Cline can execute. When set, commands are validated against allow/deny patterns before execution. When not set, all commands are allowed.
|
||||
|
||||
Format: `{"allow": ["pattern1", "pattern2"], "deny": ["pattern3"], "allowRedirects": true}`
|
||||
|
||||
**Fields:**
|
||||
|
||||
- **allow** (array of strings): Glob patterns for allowed commands. If specified, only matching commands are permitted. Uses `*` to match any characters and `?` to match a single character. Setting allow on anything will deny all others.
|
||||
- **deny** (array of strings): Glob patterns for denied commands. Deny rules take precedence over allow rules.
|
||||
- **allowRedirects** (boolean): Whether to allow shell redirects (`>`, `>>`, `<`, etc.). Defaults to false.
|
||||
|
||||
**Rule evaluation:**
|
||||
|
||||
1. Check for dangerous characters (backticks outside single quotes, unquoted newlines)
|
||||
2. Parse command into segments split by operators (`&&`, `||`, `|`, `;`)
|
||||
3. If redirects detected and `allowRedirects` is not true, command is denied
|
||||
4. Each segment is validated against deny rules first, then allow rules
|
||||
5. Subshell contents (`$(...)` and `(...)`) are recursively validated
|
||||
6. All segments must pass for the command to be allowed
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Allow only npm and git commands.
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *"]}'
|
||||
|
||||
# Allow development commands but deny dangerous ones. Deny not strictly required here since allow is set.
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["npm *", "git *", "node *"], "deny": ["rm -rf *", "sudo *"]}'
|
||||
|
||||
# Allow file operations with redirects
|
||||
export CLINE_COMMAND_PERMISSIONS='{"allow": ["cat *", "echo *"], "allowRedirects": true}'
|
||||
```
|
||||
|
||||
# ARCHITECTURE
|
||||
|
||||
Cline operates on a three-layer architecture:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cline",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.10",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"main": "cline-core.js",
|
||||
"bin": {
|
||||
|
||||
@@ -46,21 +46,21 @@ const (
|
||||
// It spawns a fresh instance for auth operations and cleans it up when done
|
||||
func RunAuthFlow(ctx context.Context, args []string) error {
|
||||
// Spawn a fresh instance for auth operations
|
||||
instanceInfo, err := global.Clients.StartNewInstance(ctx)
|
||||
instanceInfo, err := global.Instances.StartNewInstance(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start auth instance: %w", err)
|
||||
}
|
||||
|
||||
// Cleanup when done (success, error, or panic)
|
||||
defer func() {
|
||||
verboseLog("Shutting down auth instance at %s", instanceInfo.Address)
|
||||
if err := global.KillInstanceByAddress(context.Background(), global.Clients.GetRegistry(), instanceInfo.Address); err != nil {
|
||||
verboseLog("Shutting down auth instance at %s", instanceInfo.CoreAddress)
|
||||
if err := global.KillInstanceByAddress(context.Background(), global.Instances.GetRegistry(), instanceInfo.CoreAddress); err != nil {
|
||||
verboseLog("Warning: Failed to kill auth instance: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Store instance address in context for all auth handlers to use
|
||||
authCtx := context.WithValue(ctx, authInstanceAddressKey, instanceInfo.Address)
|
||||
authCtx := context.WithValue(ctx, authInstanceAddressKey, instanceInfo.CoreAddress)
|
||||
|
||||
// Route to existing auth flow
|
||||
return HandleAuthCommand(authCtx, args)
|
||||
@@ -108,12 +108,10 @@ func HandleAuthMenuNoArgs(ctx context.Context) error {
|
||||
// Get current provider config for display
|
||||
var currentProvider string
|
||||
var currentModel string
|
||||
if manager, err := createTaskManager(ctx); err == nil {
|
||||
if providerList, err := GetProviderConfigurations(ctx, manager); err == nil {
|
||||
if providerList.ActProvider != nil {
|
||||
currentProvider = GetProviderDisplayName(providerList.ActProvider.Provider)
|
||||
currentModel = providerList.ActProvider.ModelID
|
||||
}
|
||||
if providerList, err := GetProviderConfigurations(ctx); err == nil {
|
||||
if providerList.ActProvider != nil {
|
||||
currentProvider = GetProviderDisplayName(providerList.ActProvider.Provider)
|
||||
currentModel = providerList.ActProvider.ModelID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,13 +29,17 @@ type ProviderListResult struct {
|
||||
}
|
||||
|
||||
// GetProviderConfigurations retrieves and parses provider configurations from Cline Core state
|
||||
func GetProviderConfigurations(ctx context.Context, manager *task.Manager) (*ProviderListResult, error) {
|
||||
func GetProviderConfigurations(ctx context.Context) (*ProviderListResult, error) {
|
||||
if global.Config.Verbose {
|
||||
fmt.Println("[DEBUG] Retrieving provider configurations from Cline Core")
|
||||
}
|
||||
|
||||
// Get latest state from Cline Core
|
||||
state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
|
||||
grpcClient, err := global.GetDefaultClient(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get provider configs due to unable to get gRPC client: %w", err)
|
||||
}
|
||||
state, err := grpcClient.State.GetLatestState(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get state: %w", err)
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ func (pw *ProviderWizard) handleAddOcaProvider() error {
|
||||
|
||||
// handleListProviders retrieves and displays configured providers
|
||||
func (pw *ProviderWizard) handleListProviders() error {
|
||||
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
|
||||
result, err := GetProviderConfigurations(pw.ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
|
||||
}
|
||||
@@ -413,7 +413,7 @@ func (pw *ProviderWizard) manualModelEntry(provider cline.ApiProvider) (string,
|
||||
// handleChangeModel allows changing the model for any configured provider
|
||||
func (pw *ProviderWizard) handleChangeModel() error {
|
||||
// Step 1: Get current provider configurations
|
||||
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
|
||||
result, err := GetProviderConfigurations(pw.ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
|
||||
}
|
||||
@@ -585,11 +585,11 @@ func getProviderModelIDFromState(stateData map[string]interface{}, provider clin
|
||||
return ""
|
||||
}
|
||||
|
||||
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
|
||||
// getProviderAPIKeyFromState retrieves the API key for a specific provider from state
|
||||
func getProviderAPIKeyFromState(stateData map[string]interface{}, provider cline.ApiProvider) string {
|
||||
// OCA uses account authentication, not API keys. Consider it "present" if authenticated.
|
||||
if provider == cline.ApiProvider_OCA {
|
||||
if state, _ := GetLatestOCAState(context.TODO(), 2 * time.Second); state != nil && state.User != nil {
|
||||
if state, _ := GetLatestOCAState(context.TODO(), 2*time.Second); state != nil && state.User != nil {
|
||||
// Return a sentinel non-empty string so upstream checks pass.
|
||||
return "OCA_AUTH_VERIFIED"
|
||||
}
|
||||
@@ -648,7 +648,7 @@ func convertMapToOpenRouterModelInfo(data map[string]interface{}) *cline.OpenRou
|
||||
// handleRemoveProvider allows removing a configured provider by clearing its API key
|
||||
func (pw *ProviderWizard) handleRemoveProvider() error {
|
||||
// Step 1: Get current provider configurations
|
||||
result, err := GetProviderConfigurations(pw.ctx, pw.manager)
|
||||
result, err := GetProviderConfigurations(pw.ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve provider configurations: %w", err)
|
||||
}
|
||||
@@ -748,7 +748,6 @@ func (pw *ProviderWizard) clearProviderAPIKey(provider cline.ApiProvider) error
|
||||
return RemoveProviderPartial(pw.ctx, pw.manager, provider)
|
||||
}
|
||||
|
||||
|
||||
func signOutOca(ctx context.Context) error {
|
||||
client, err := global.GetDefaultClient(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -40,7 +40,7 @@ func ensureConfigManager(ctx context.Context, address string) error {
|
||||
}
|
||||
|
||||
// Always set the instance we're using as the default
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.GetRegistry()
|
||||
if err := registry.SetDefaultInstance(instanceAddress); err != nil {
|
||||
// Log warning but don't fail - this is not critical
|
||||
fmt.Printf("Warning: failed to set default instance: %v\n", err)
|
||||
@@ -123,11 +123,11 @@ func setCommand() *cobra.Command {
|
||||
Use: "set <key=value> [key=value...]",
|
||||
Aliases: []string{"s"},
|
||||
Short: "Set configuration variables",
|
||||
Long: `Set one or more global configuration variables using key=value format.
|
||||
Long: `Set one or more global configuration variables using key=value format.
|
||||
|
||||
This command merges the provided settings with existing values, preserving
|
||||
unspecified fields. Only the fields you explicitly set will be updated.`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ func NewManager(ctx context.Context, address string) (*Manager, error) {
|
||||
|
||||
// Get the actual address being used
|
||||
clientAddress := address
|
||||
if address == "" && global.Clients != nil {
|
||||
clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
|
||||
if address == "" && global.Instances != nil {
|
||||
clientAddress = global.Instances.GetRegistry().GetDefaultInstance()
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
@@ -110,7 +110,6 @@ func (m *Manager) ListSettings(ctx context.Context) error {
|
||||
"dictationSettings",
|
||||
"autoCondenseThreshold",
|
||||
"autoApprovalSettings",
|
||||
"hooksEnabled",
|
||||
}
|
||||
|
||||
// Render each field using the renderer
|
||||
|
||||
@@ -79,7 +79,7 @@ func RenderField(key string, value interface{}, censor bool) error {
|
||||
"planActSeparateModelsSetting", "enableCheckpointsSetting",
|
||||
"terminalReuseEnabled", "mcpResponsesCollapsed", "strictPlanModeEnabled",
|
||||
"useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout",
|
||||
"terminalOutputLineLimit", "autoCondenseThreshold", "hooksEnabled":
|
||||
"terminalOutputLineLimit", "autoCondenseThreshold":
|
||||
fmt.Printf("%s: %s\n", camelToKebab(key), formatValue(value, key, censor))
|
||||
return nil
|
||||
|
||||
|
||||
@@ -14,21 +14,20 @@ import (
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
// ClineClients manages Cline instances using the new registry system
|
||||
type ClineClients struct {
|
||||
registry *ClientRegistry
|
||||
type ClineInstances struct {
|
||||
registry *InstanceRegistry
|
||||
}
|
||||
|
||||
// NewClineClients creates a new ClineClients instance
|
||||
func NewClineClients(configPath string) *ClineClients {
|
||||
registry := NewClientRegistry(configPath)
|
||||
return &ClineClients{
|
||||
// NewClineInstances creates a new ClineInstances instance
|
||||
func NewClineInstances(configPath string) *ClineInstances {
|
||||
registry := NewInstanceRegistry(configPath)
|
||||
return &ClineInstances{
|
||||
registry: registry,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize performs cleanup of stale instances
|
||||
func (c *ClineClients) Initialize(ctx context.Context) error {
|
||||
func (c *ClineInstances) Initialize(ctx context.Context) error {
|
||||
// Clean up stale entries (direct SQLite operations)
|
||||
_ = c.registry.CleanupStaleInstances(ctx)
|
||||
|
||||
@@ -36,7 +35,8 @@ func (c *ClineClients) Initialize(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// StartNewInstance starts a new Cline instance and waits for cline-core to self-register
|
||||
func (c *ClineClients) StartNewInstance(ctx context.Context, workspaces ...string) (*common.CoreInstanceInfo, error) {
|
||||
// An "instance" is a pair of cline-core and cline-host processes
|
||||
func (c *ClineInstances) StartNewInstance(ctx context.Context, workspaces ...string) (*common.CoreInstanceInfo, error) {
|
||||
// Find available ports
|
||||
corePort, hostPort, err := common.FindAvailablePortPair()
|
||||
if err != nil {
|
||||
@@ -102,7 +102,7 @@ func (c *ClineClients) StartNewInstance(ctx context.Context, workspaces ...strin
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Println("Services started and registered successfully!")
|
||||
fmt.Printf(" Address: %s\n", instance.Address)
|
||||
fmt.Printf(" Address: %s\n", instance.CoreAddress)
|
||||
fmt.Printf(" Core Port: %d\n", instance.CorePort())
|
||||
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
|
||||
fmt.Printf(" Process PID: %d\n", coreCmd.Process.Pid)
|
||||
@@ -120,7 +120,7 @@ func (c *ClineClients) StartNewInstance(ctx context.Context, workspaces ...strin
|
||||
}
|
||||
|
||||
// StartNewInstanceAtPort starts a new Cline instance at the specified port and waits for self-registration
|
||||
func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int, workspaces ...string) (*common.CoreInstanceInfo, error) {
|
||||
func (c *ClineInstances) StartNewInstanceAtPort(ctx context.Context, corePort int, workspaces ...string) (*common.CoreInstanceInfo, error) {
|
||||
// Find available host port (core port + 1000)
|
||||
hostPort := corePort + 1000
|
||||
coreAddress := fmt.Sprintf("localhost:%d", corePort)
|
||||
@@ -189,7 +189,7 @@ func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int,
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Println("Services started and registered successfully!")
|
||||
fmt.Printf(" Address: %s\n", instance.Address)
|
||||
fmt.Printf(" Address: %s\n", instance.CoreAddress)
|
||||
fmt.Printf(" Core Port: %d\n", instance.CorePort())
|
||||
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
|
||||
fmt.Printf(" Process PID: %d\n", coreCmd.Process.Pid)
|
||||
@@ -207,12 +207,12 @@ func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int,
|
||||
}
|
||||
|
||||
// GetRegistry returns the client registry
|
||||
func (c *ClineClients) GetRegistry() *ClientRegistry {
|
||||
func (c *ClineInstances) GetRegistry() *InstanceRegistry {
|
||||
return c.registry
|
||||
}
|
||||
|
||||
// EnsureInstanceAtAddress ensures an instance exists at the given address, starting one if needed
|
||||
func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address string) error {
|
||||
func (c *ClineInstances) EnsureInstanceAtAddress(ctx context.Context, address string) error {
|
||||
// Expect host:port everywhere
|
||||
normalized := address
|
||||
if normalized == "" {
|
||||
@@ -230,7 +230,7 @@ func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address stri
|
||||
return fmt.Errorf("invalid address format %s", address)
|
||||
}
|
||||
|
||||
// Use IPv6-compatible localhost detection
|
||||
// Use IPv6-compatible localhost detection
|
||||
if common.IsLocalAddress(host) {
|
||||
_, err := c.StartNewInstanceAtPort(ctx, port)
|
||||
if err != nil {
|
||||
@@ -305,7 +305,7 @@ func startClineHost(hostPort int, workspaces []string) (*exec.Cmd, error) {
|
||||
}
|
||||
|
||||
// KillInstanceByAddress kills a Cline instance by its address
|
||||
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, address string) error {
|
||||
func KillInstanceByAddress(ctx context.Context, registry *InstanceRegistry, address string) error {
|
||||
// Check if the instance exists in the registry
|
||||
_, err := registry.GetInstance(address)
|
||||
if err != nil {
|
||||
@@ -355,9 +355,9 @@ func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, addres
|
||||
defaultInstance := registry.GetDefaultInstance()
|
||||
if defaultInstance == address || defaultInstance == "" {
|
||||
if len(instances) > 0 {
|
||||
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
|
||||
if err := registry.SetDefaultInstance(instances[0].CoreAddress); err == nil {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
|
||||
fmt.Printf("Updated default instance to: %s\n", instances[0].CoreAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ type GlobalConfig struct {
|
||||
}
|
||||
|
||||
var (
|
||||
Config *GlobalConfig
|
||||
Clients *ClineClients
|
||||
Config *GlobalConfig
|
||||
Instances *ClineInstances
|
||||
|
||||
// Version info - set at build time via ldflags
|
||||
// Version is the Cline Core version (from root package.json)
|
||||
@@ -61,11 +61,11 @@ func InitializeGlobalConfig(cfg *GlobalConfig) error {
|
||||
// Otherwise lipgloss auto-detects terminal capabilities (default behavior)
|
||||
|
||||
Config = cfg
|
||||
Clients = NewClineClients(cfg.ConfigPath)
|
||||
Instances = NewClineInstances(cfg.ConfigPath)
|
||||
|
||||
// Initialize the clients registry
|
||||
ctx := context.Background()
|
||||
if err := Clients.Initialize(ctx); err != nil {
|
||||
if err := Instances.Initialize(ctx); err != nil {
|
||||
return fmt.Errorf("failed to initialize clients: %w", err)
|
||||
}
|
||||
|
||||
@@ -76,25 +76,25 @@ func InitializeGlobalConfig(cfg *GlobalConfig) error {
|
||||
func GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
|
||||
if Config.CoreAddress != "" && Config.CoreAddress != fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT) {
|
||||
// User specified a specific address, use that
|
||||
return Clients.GetRegistry().GetClient(ctx, Config.CoreAddress)
|
||||
return Instances.GetRegistry().GetClient(ctx, Config.CoreAddress)
|
||||
}
|
||||
|
||||
// Use the default instance from registry
|
||||
return Clients.GetRegistry().GetDefaultClient(ctx)
|
||||
return Instances.GetRegistry().GetDefaultClient(ctx)
|
||||
}
|
||||
|
||||
// GetClientForAddress returns a client for a specific address
|
||||
func GetClientForAddress(ctx context.Context, address string) (*client.ClineClient, error) {
|
||||
return Clients.GetRegistry().GetClient(ctx, address)
|
||||
return Instances.GetRegistry().GetClient(ctx, address)
|
||||
}
|
||||
|
||||
// EnsureDefaultInstance ensures a default instance exists
|
||||
func EnsureDefaultInstance(ctx context.Context) error {
|
||||
if Clients == nil {
|
||||
if Instances == nil {
|
||||
return fmt.Errorf("global clients not initialized")
|
||||
}
|
||||
|
||||
registry := Clients.GetRegistry()
|
||||
registry := Instances.GetRegistry()
|
||||
|
||||
// First, check if there are any instances already registered in SQLite
|
||||
instances := registry.ListInstances()
|
||||
@@ -108,11 +108,12 @@ func EnsureDefaultInstance(ctx context.Context) error {
|
||||
if registry.GetDefaultInstance() == "" {
|
||||
// No instances exist, start a new one
|
||||
// Note: StartNewInstance will automatically set it as default since it's the first instance
|
||||
_, err := Clients.StartNewInstance(ctx)
|
||||
_, err := Instances.StartNewInstance(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start new default instance: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,28 +18,28 @@ import (
|
||||
"google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
// ClientRegistry manages Cline client connections using direct SQLite operations
|
||||
type ClientRegistry struct {
|
||||
// InstanceRegistry manages Cline client connections using direct SQLite operations
|
||||
type InstanceRegistry struct {
|
||||
lockManager *sqlite.LockManager
|
||||
configPath string
|
||||
}
|
||||
|
||||
// NewClientRegistry creates a new client registry
|
||||
func NewClientRegistry(configPath string) *ClientRegistry {
|
||||
// NewInstanceRegistry creates a new instance registry
|
||||
func NewInstanceRegistry(configPath string) *InstanceRegistry {
|
||||
lockManager, err := sqlite.NewLockManager(configPath)
|
||||
if err != nil {
|
||||
// Log error but continue - we can still function without SQLite
|
||||
log.Fatalf("Warning: Failed to initialize SQLite lock manager: %v\n", err)
|
||||
}
|
||||
|
||||
return &ClientRegistry{
|
||||
return &InstanceRegistry{
|
||||
lockManager: lockManager,
|
||||
configPath: configPath,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDefaultInstance returns the default instance address from settings file
|
||||
func (r *ClientRegistry) GetDefaultInstance() string {
|
||||
func (r *InstanceRegistry) GetDefaultInstance() string {
|
||||
defaultAddr, err := sqlite.GetDefaultInstance(r.configPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -48,7 +48,7 @@ func (r *ClientRegistry) GetDefaultInstance() string {
|
||||
}
|
||||
|
||||
// SetDefaultInstance sets the default instance (writes default.json)
|
||||
func (r *ClientRegistry) SetDefaultInstance(address string) error {
|
||||
func (r *InstanceRegistry) SetDefaultInstance(address string) error {
|
||||
// Verify the instance exists in SQLite
|
||||
if r.lockManager != nil {
|
||||
exists, err := r.lockManager.HasInstanceAtAddress(address)
|
||||
@@ -64,7 +64,7 @@ func (r *ClientRegistry) SetDefaultInstance(address string) error {
|
||||
}
|
||||
|
||||
// GetInstance returns instance information directly from SQLite
|
||||
func (r *ClientRegistry) GetInstance(address string) (*common.CoreInstanceInfo, error) {
|
||||
func (r *InstanceRegistry) GetInstance(address string) (*common.CoreInstanceInfo, error) {
|
||||
if r.lockManager == nil {
|
||||
return nil, fmt.Errorf("lock manager not available")
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (r *ClientRegistry) GetInstance(address string) (*common.CoreInstanceInfo,
|
||||
}
|
||||
|
||||
// GetClient returns a connected client for the given address (created on-demand)
|
||||
func (r *ClientRegistry) GetClient(ctx context.Context, address string) (*client.ClineClient, error) {
|
||||
func (r *InstanceRegistry) GetClient(ctx context.Context, address string) (*client.ClineClient, error) {
|
||||
// Verify instance exists in SQLite
|
||||
if r.lockManager != nil {
|
||||
exists, err := r.lockManager.HasInstanceAtAddress(address)
|
||||
@@ -104,7 +104,7 @@ func (r *ClientRegistry) GetClient(ctx context.Context, address string) (*client
|
||||
}
|
||||
|
||||
// GetDefaultClient returns a client for the default instance
|
||||
func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
|
||||
func (r *InstanceRegistry) GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
|
||||
defaultAddr := r.GetDefaultInstance()
|
||||
if defaultAddr == "" {
|
||||
return nil, fmt.Errorf("no default instance configured")
|
||||
@@ -117,7 +117,7 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
|
||||
// Database is unavailable - Return error instead of attempting cleanup
|
||||
return nil, fmt.Errorf("cannot verify default instance: database unavailable: %w", err)
|
||||
}
|
||||
|
||||
|
||||
if !exists {
|
||||
// Instance doesn't exist in database but config file references it
|
||||
// This is a stale config - remove it and try to find another instance
|
||||
@@ -127,14 +127,14 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
|
||||
} else {
|
||||
fmt.Printf("Removed stale default instance config (instance %s not found in database)\n", defaultAddr)
|
||||
}
|
||||
|
||||
|
||||
// Try to find and set a new default instance
|
||||
instances := r.ListInstances()
|
||||
if len(instances) > 0 {
|
||||
if err := r.EnsureDefaultInstance(instances); err != nil {
|
||||
return nil, fmt.Errorf("failed to set new default instance: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// Retry with the new default
|
||||
newDefaultAddr := r.GetDefaultInstance()
|
||||
if newDefaultAddr != "" {
|
||||
@@ -142,7 +142,7 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
|
||||
return r.GetClient(ctx, newDefaultAddr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil, fmt.Errorf("no default instance configured")
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineCli
|
||||
}
|
||||
|
||||
// ListInstances returns all registered instances directly from SQLite
|
||||
func (r *ClientRegistry) ListInstances() []*common.CoreInstanceInfo {
|
||||
func (r *InstanceRegistry) ListInstances() []*common.CoreInstanceInfo {
|
||||
if r.lockManager == nil {
|
||||
return []*common.CoreInstanceInfo{}
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func (r *ClientRegistry) ListInstances() []*common.CoreInstanceInfo {
|
||||
}
|
||||
|
||||
// HasInstanceAtAddress checks if an instance exists at the given address (delegates to SQLite)
|
||||
func (r *ClientRegistry) HasInstanceAtAddress(address string) bool {
|
||||
func (r *InstanceRegistry) HasInstanceAtAddress(address string) bool {
|
||||
if r.lockManager == nil {
|
||||
return false
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func (r *ClientRegistry) HasInstanceAtAddress(address string) bool {
|
||||
}
|
||||
|
||||
// CleanupStaleInstances removes stale instances using direct SQLite operations
|
||||
func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
|
||||
func (r *InstanceRegistry) CleanupStaleInstances(ctx context.Context) error {
|
||||
if r.lockManager == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -202,15 +202,15 @@ func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
|
||||
// Try to gracefully shutdown the paired host process before cleanup
|
||||
|
||||
fmt.Printf("Attempting to shutdown dangling host service %s for stale cline core instance %s\n",
|
||||
instance.HostServiceAddress, instance.Address)
|
||||
instance.HostServiceAddress, instance.CoreAddress)
|
||||
r.tryShutdownHostProcess(instance.HostServiceAddress)
|
||||
|
||||
// Remove from SQLite database
|
||||
if err := r.lockManager.RemoveInstanceLock(instance.Address); err != nil {
|
||||
return fmt.Errorf("failed to remove stale instance %s: %w", instance.Address, err)
|
||||
if err := r.lockManager.RemoveInstanceLock(instance.CoreAddress); err != nil {
|
||||
return fmt.Errorf("failed to remove stale instance %s: %w", instance.CoreAddress, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Removed stale instance: %s\n", instance.Address)
|
||||
fmt.Printf("Removed stale instance: %s\n", instance.CoreAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,8 +218,7 @@ func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// tryShutdownHostProcess attempts to gracefully shutdown a host process via RPC
|
||||
// Best effort, don't throw errors i guess
|
||||
func (r *ClientRegistry) tryShutdownHostProcess(hostServiceAddress string) {
|
||||
func (r *InstanceRegistry) tryShutdownHostProcess(hostServiceAddress string) {
|
||||
err := common.RetryOperation(3, 2*time.Second, func() error {
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
@@ -252,7 +251,7 @@ func (r *ClientRegistry) tryShutdownHostProcess(hostServiceAddress string) {
|
||||
}
|
||||
|
||||
// ListInstancesCleaned performs cleanup and returns instances with health checks
|
||||
func (r *ClientRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
|
||||
func (r *InstanceRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
|
||||
// 1. Clean up stale entries (best-effort)
|
||||
_ = r.CleanupStaleInstances(ctx)
|
||||
|
||||
@@ -268,7 +267,7 @@ func (r *ClientRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.Co
|
||||
}
|
||||
|
||||
// EnsureDefaultInstance ensures a default instance is set if instances exist but no default is configured
|
||||
func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceInfo) error {
|
||||
func (r *InstanceRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceInfo) error {
|
||||
currentDefault := r.GetDefaultInstance()
|
||||
|
||||
// If we have no instances, clear any stale default and remove settings file
|
||||
@@ -283,13 +282,13 @@ func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceI
|
||||
|
||||
// If we have instances but no default, pick the first one
|
||||
if currentDefault == "" {
|
||||
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
|
||||
return sqlite.SetDefaultInstance(r.configPath, instances[0].CoreAddress)
|
||||
}
|
||||
|
||||
// Validate current default still exists in the instances
|
||||
defaultExists := false
|
||||
for _, instance := range instances {
|
||||
if instance.Address == currentDefault {
|
||||
if instance.CoreAddress == currentDefault {
|
||||
defaultExists = true
|
||||
break
|
||||
}
|
||||
@@ -297,7 +296,7 @@ func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceI
|
||||
|
||||
if !defaultExists {
|
||||
// Current default doesn't exist, pick a new one from available instances
|
||||
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
|
||||
return sqlite.SetDefaultInstance(r.configPath, instances[0].CoreAddress)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+25
-25
@@ -92,12 +92,12 @@ func newInstanceKillCommand() *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if global.Clients == nil {
|
||||
if global.Instances == nil {
|
||||
return fmt.Errorf("clients not initialized")
|
||||
}
|
||||
|
||||
ctx := cmd.Context()
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.GetRegistry()
|
||||
|
||||
if killAllCLI {
|
||||
return killAllCLIInstances(ctx, registry)
|
||||
@@ -112,7 +112,7 @@ func newInstanceKillCommand() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) error {
|
||||
func killAllCLIInstances(ctx context.Context, registry *global.InstanceRegistry) error {
|
||||
// Get all instances from registry
|
||||
instances, err := registry.ListInstancesCleaned(ctx)
|
||||
if err != nil {
|
||||
@@ -135,7 +135,7 @@ func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) e
|
||||
cliInstances = append(cliInstances, instance)
|
||||
} else {
|
||||
skippedNonCLI++
|
||||
fmt.Printf("⊘ Skipping %s instance: %s\n", platform, instance.Address)
|
||||
fmt.Printf("⊘ Skipping %s instance: %s\n", platform, instance.CoreAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,16 +160,16 @@ func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) e
|
||||
|
||||
// Kill all CLI instances
|
||||
for _, instance := range cliInstances {
|
||||
result := killInstanceProcess(ctx, registry, instance.Address)
|
||||
result := killInstanceProcess(ctx, registry, instance.CoreAddress)
|
||||
killResults = append(killResults, result)
|
||||
|
||||
if result.err != nil {
|
||||
fmt.Printf("✗ Failed to kill %s: %v\n", instance.Address, result.err)
|
||||
fmt.Printf("✗ Failed to kill %s: %v\n", instance.CoreAddress, result.err)
|
||||
} else if result.alreadyDead {
|
||||
fmt.Printf("⚠ Instance %s appears to be already dead\n", instance.Address)
|
||||
fmt.Printf("⚠ Instance %s appears to be already dead\n", instance.CoreAddress)
|
||||
} else {
|
||||
fmt.Printf("✓ Killed %s (PID %d)\n", instance.Address, result.pid)
|
||||
killedAddresses[instance.Address] = true
|
||||
fmt.Printf("✓ Killed %s (PID %d)\n", instance.CoreAddress, result.pid)
|
||||
killedAddresses[instance.CoreAddress] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,8 +190,8 @@ func killAllCLIInstances(ctx context.Context, registry *global.ClientRegistry) e
|
||||
// Check if any of the killed instances are still in the registry
|
||||
stillPresent := []string{}
|
||||
for _, remaining := range remainingInstances {
|
||||
if killedAddresses[remaining.Address] {
|
||||
stillPresent = append(stillPresent, remaining.Address)
|
||||
if killedAddresses[remaining.CoreAddress] {
|
||||
stillPresent = append(stillPresent, remaining.CoreAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ type killResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func killInstanceProcess(ctx context.Context, registry *global.ClientRegistry, address string) killResult {
|
||||
func killInstanceProcess(ctx context.Context, registry *global.InstanceRegistry, address string) killResult {
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
@@ -276,12 +276,12 @@ func newInstanceListCommand() *cobra.Command {
|
||||
Short: "List all registered Cline instances",
|
||||
Long: `List all registered Cline instances with their status and connection details.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if global.Clients == nil {
|
||||
if global.Instances == nil {
|
||||
return fmt.Errorf("clients not initialized")
|
||||
}
|
||||
|
||||
ctx := cmd.Context()
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.GetRegistry()
|
||||
|
||||
// Load, cleanup stale local entries, and update health
|
||||
instances, err := registry.ListInstancesCleaned(ctx)
|
||||
@@ -310,7 +310,7 @@ func newInstanceListCommand() *cobra.Command {
|
||||
var rows []instanceRow
|
||||
for _, instance := range instances {
|
||||
isDefault := ""
|
||||
if instance.Address == defaultInstance {
|
||||
if instance.CoreAddress == defaultInstance {
|
||||
isDefault = "✓"
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ func newInstanceListCommand() *cobra.Command {
|
||||
platform := platformNA
|
||||
if instance.Status == grpc_health_v1.HealthCheckResponse_SERVING {
|
||||
// Get PID from core
|
||||
if client, err := registry.GetClient(ctx, instance.Address); err == nil {
|
||||
if client, err := registry.GetClient(ctx, instance.CoreAddress); err == nil {
|
||||
if processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{}); err == nil {
|
||||
pid = fmt.Sprintf("%d", processInfo.ProcessId)
|
||||
// Update version from RPC if available
|
||||
@@ -341,7 +341,7 @@ func newInstanceListCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
rows = append(rows, instanceRow{
|
||||
address: instance.Address,
|
||||
address: instance.CoreAddress,
|
||||
status: instance.Status.String(),
|
||||
version: instance.Version,
|
||||
lastSeen: lastSeen,
|
||||
@@ -428,11 +428,11 @@ func newInstanceDefaultCommand() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
address := args[0]
|
||||
|
||||
if global.Clients == nil {
|
||||
if global.Instances == nil {
|
||||
return fmt.Errorf("clients not initialized")
|
||||
}
|
||||
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.GetRegistry()
|
||||
|
||||
// Verify the instance exists
|
||||
_, err := registry.GetInstance(address)
|
||||
@@ -464,34 +464,34 @@ func newInstanceNewCommand() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
if global.Clients == nil {
|
||||
if global.Instances == nil {
|
||||
return fmt.Errorf("clients not initialized")
|
||||
}
|
||||
|
||||
fmt.Println("Starting new Cline instance...")
|
||||
|
||||
instance, err := global.Clients.StartNewInstance(ctx)
|
||||
instance, err := global.Instances.StartNewInstance(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start instance: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully started new instance:\n")
|
||||
fmt.Printf(" Address: %s\n", instance.Address)
|
||||
fmt.Printf(" Address: %s\n", instance.CoreAddress)
|
||||
fmt.Printf(" Core Port: %d\n", instance.CorePort())
|
||||
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
|
||||
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.GetRegistry()
|
||||
|
||||
// If --default flag provided, set this instance as the default
|
||||
if setDefault {
|
||||
if err := registry.SetDefaultInstance(instance.Address); err != nil {
|
||||
if err := registry.SetDefaultInstance(instance.CoreAddress); err != nil {
|
||||
fmt.Printf("Warning: Failed to set as default: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf(" Status: Set as default instance\n")
|
||||
}
|
||||
} else {
|
||||
// Otherwise, check if EnsureDefaultInstance already set it as default
|
||||
if registry.GetDefaultInstance() == instance.Address {
|
||||
if registry.GetDefaultInstance() == instance.CoreAddress {
|
||||
fmt.Printf(" Status: Default instance\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
|
||||
return ChangeInputTypeMsg{
|
||||
InputType: InputTypeFeedback,
|
||||
Title: "Your feedback",
|
||||
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor",
|
||||
Placeholder: "/plan or /act to switch modes\nctrl+e to open editor\nctrl+c to exit",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,3 +547,74 @@ func (m *InputModel) openEditor() tea.Cmd {
|
||||
func (m *InputModel) SetSlashRegistry(registry *slash.Registry) {
|
||||
m.completion.SetRegistry(registry)
|
||||
}
|
||||
|
||||
// initialPromptWrapper wraps InputModel to capture the submit result for initial task prompts
|
||||
type initialPromptWrapper struct {
|
||||
model *InputModel
|
||||
result string
|
||||
cancelled bool
|
||||
}
|
||||
|
||||
func (w *initialPromptWrapper) Init() tea.Cmd {
|
||||
return w.model.Init()
|
||||
}
|
||||
|
||||
func (w *initialPromptWrapper) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case InputSubmitMsg:
|
||||
w.result = msg.Value
|
||||
clearCodes := w.model.ClearScreen()
|
||||
if clearCodes != "" {
|
||||
fmt.Print(clearCodes)
|
||||
}
|
||||
return w, tea.Quit
|
||||
|
||||
case InputCancelMsg:
|
||||
w.cancelled = true
|
||||
clearCodes := w.model.ClearScreen()
|
||||
if clearCodes != "" {
|
||||
fmt.Print(clearCodes)
|
||||
}
|
||||
return w, tea.Quit
|
||||
}
|
||||
|
||||
// Forward to wrapped model
|
||||
_, cmd := w.model.Update(msg)
|
||||
return w, cmd
|
||||
}
|
||||
|
||||
func (w *initialPromptWrapper) View() string {
|
||||
return w.model.View()
|
||||
}
|
||||
|
||||
// ErrUserAborted is returned when the user cancels the input prompt
|
||||
var ErrUserAborted = fmt.Errorf("user aborted")
|
||||
|
||||
// PromptForInitialTask displays an interactive prompt for the initial task with slash command autocomplete.
|
||||
// Returns the entered text, or ErrUserAborted if cancelled.
|
||||
func PromptForInitialTask(title, placeholder, mode string, registry *slash.Registry) (string, error) {
|
||||
model := NewInputModelWithRegistry(
|
||||
InputTypeMessage,
|
||||
title,
|
||||
placeholder,
|
||||
mode,
|
||||
registry,
|
||||
)
|
||||
|
||||
wrapper := &initialPromptWrapper{
|
||||
model: &model,
|
||||
}
|
||||
|
||||
p := tea.NewProgram(wrapper)
|
||||
|
||||
_, err := p.Run()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("input prompt failed: %w", err)
|
||||
}
|
||||
|
||||
if wrapper.cancelled {
|
||||
return "", ErrUserAborted
|
||||
}
|
||||
|
||||
return strings.TrimSpace(wrapper.result), nil
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ package slash
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cline/grpc-go/client"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
@@ -32,25 +33,31 @@ var cliLocalCommands = []Command{
|
||||
}
|
||||
|
||||
// NewRegistry creates a new slash command registry
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
commands: make([]Command, 0),
|
||||
func NewRegistry(ctx context.Context) *Registry {
|
||||
defaultCommands := append([]Command{}, cliLocalCommands...)
|
||||
r := &Registry{
|
||||
commands: defaultCommands,
|
||||
}
|
||||
r.FetchFromBackend(ctx)
|
||||
return r
|
||||
}
|
||||
|
||||
// FetchFromBackend fetches available commands from cline-core backend
|
||||
func (r *Registry) FetchFromBackend(ctx context.Context, c *client.ClineClient) error {
|
||||
resp, err := c.Slash.GetAvailableSlashCommands(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
func (r *Registry) FetchFromBackend(ctx context.Context) error {
|
||||
grpcClient, err := global.GetDefaultClient(ctx)
|
||||
if err != nil && global.Config.Verbose {
|
||||
fmt.Printf("Warning: could not get gRPC client: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
resp, err := grpcClient.Slash.GetAvailableSlashCommands(ctx, &cline.EmptyRequest{})
|
||||
if err != nil && global.Config.Verbose {
|
||||
fmt.Printf("Warning: could not get gRPC client: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Start with CLI-local commands
|
||||
r.commands = append([]Command{}, cliLocalCommands...)
|
||||
|
||||
// Add backend commands (only CLI-compatible ones)
|
||||
for _, cmd := range resp.Commands {
|
||||
if cmd.CliCompatible {
|
||||
@@ -66,17 +73,6 @@ func (r *Registry) FetchFromBackend(ctx context.Context, c *client.ClineClient)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCommands returns all available commands
|
||||
func (r *Registry) GetCommands() []Command {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
// Return a copy to avoid race conditions
|
||||
result := make([]Command, len(r.commands))
|
||||
copy(result, r.commands)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetMatching returns commands that start with the given prefix (case-insensitive)
|
||||
func (r *Registry) GetMatching(prefix string) []Command {
|
||||
r.mu.RLock()
|
||||
@@ -123,3 +119,24 @@ func (r *Registry) HasCommands() bool {
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.commands) > 0
|
||||
}
|
||||
|
||||
// ParseModeSwitch checks if message starts with /act or /plan and extracts the mode and remaining message.
|
||||
// Returns (mode, remainingMessage, isModeSwitch).
|
||||
// This is a package-level function so it can be used both during initial task creation
|
||||
// and during interactive input handling.
|
||||
func ParseModeSwitch(message string) (string, string, bool) {
|
||||
trimmed := strings.TrimSpace(message)
|
||||
lower := strings.ToLower(trimmed)
|
||||
|
||||
if strings.HasPrefix(lower, "/plan") {
|
||||
remaining := strings.TrimSpace(trimmed[5:])
|
||||
return "plan", remaining, true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(lower, "/act") {
|
||||
remaining := strings.TrimSpace(trimmed[4:])
|
||||
return "act", remaining, true
|
||||
}
|
||||
|
||||
return "", message, false
|
||||
}
|
||||
|
||||
@@ -19,20 +19,20 @@ import (
|
||||
// Handles localhost/127.0.0.1 equivalence by returning both forms.
|
||||
func normalizeAddressVariants(address string) []string {
|
||||
variants := []string{address}
|
||||
|
||||
|
||||
// Extract host and port
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return variants
|
||||
}
|
||||
|
||||
|
||||
// Add the alternate form for localhost/127.0.0.1
|
||||
if host == "localhost" {
|
||||
variants = append(variants, net.JoinHostPort("127.0.0.1", port))
|
||||
} else if host == "127.0.0.1" {
|
||||
variants = append(variants, net.JoinHostPort("localhost", port))
|
||||
}
|
||||
|
||||
|
||||
return variants
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ func (lm *LockManager) GetInstanceInfo(address string) (*common.CoreInstanceInfo
|
||||
|
||||
query := common.SelectInstanceLockByHolderSQL
|
||||
variants := normalizeAddressVariants(address)
|
||||
|
||||
|
||||
var heldBy, lockTarget string
|
||||
var lockedAt int64
|
||||
var lastErr error
|
||||
@@ -193,7 +193,7 @@ func (lm *LockManager) GetInstanceInfo(address string) (*common.CoreInstanceInfo
|
||||
if err == nil {
|
||||
// Found it!
|
||||
return &common.CoreInstanceInfo{
|
||||
Address: heldBy,
|
||||
CoreAddress: heldBy,
|
||||
HostServiceAddress: lockTarget,
|
||||
Status: grpc_health_v1.HealthCheckResponse_UNKNOWN,
|
||||
LastSeen: time.Unix(lockedAt/1000, 0),
|
||||
@@ -204,7 +204,7 @@ func (lm *LockManager) GetInstanceInfo(address string) (*common.CoreInstanceInfo
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// None of the variants were found
|
||||
if lastErr != nil {
|
||||
return nil, fmt.Errorf("failed to query instance: %w", lastErr)
|
||||
@@ -235,7 +235,7 @@ func (lm *LockManager) ListInstancesWithHealthCheck(ctx context.Context) ([]*com
|
||||
}
|
||||
|
||||
info := &common.CoreInstanceInfo{
|
||||
Address: lock.HeldBy,
|
||||
CoreAddress: lock.HeldBy,
|
||||
HostServiceAddress: lock.LockTarget,
|
||||
Status: status,
|
||||
LastSeen: time.Unix(lock.LockedAt/1000, 0),
|
||||
|
||||
+5
-10
@@ -80,7 +80,7 @@ func ensureTaskManager(ctx context.Context, address string) error {
|
||||
}
|
||||
|
||||
// Always set the instance we're using as the default
|
||||
registry := global.Clients.GetRegistry()
|
||||
registry := global.Instances.GetRegistry()
|
||||
if err := registry.SetDefaultInstance(instanceAddress); err != nil {
|
||||
// Log warning but don't fail - this is not critical
|
||||
fmt.Printf("Warning: failed to set default instance: %v\n", err)
|
||||
@@ -91,10 +91,10 @@ func ensureTaskManager(ctx context.Context, address string) error {
|
||||
|
||||
// ensureInstanceAtAddress ensures an instance exists at the given address
|
||||
func ensureInstanceAtAddress(ctx context.Context, address string) error {
|
||||
if global.Clients == nil {
|
||||
if global.Instances == nil {
|
||||
return fmt.Errorf("global clients not initialized")
|
||||
}
|
||||
return global.Clients.EnsureInstanceAtAddress(ctx, address)
|
||||
return global.Instances.EnsureInstanceAtAddress(ctx, address)
|
||||
}
|
||||
|
||||
func newTaskNewCommand() *cobra.Command {
|
||||
@@ -117,7 +117,7 @@ func newTaskNewCommand() *cobra.Command {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Check if an instance exists when no address specified
|
||||
if address == "" && global.Clients.GetRegistry().GetDefaultInstance() == "" {
|
||||
if address == "" && global.Instances.GetRegistry().GetDefaultInstance() == "" {
|
||||
fmt.Println("No instances available for creating tasks")
|
||||
return nil
|
||||
}
|
||||
@@ -230,7 +230,7 @@ func newTaskSendCommand() *cobra.Command {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Check if an instance exists when no address specified
|
||||
if address == "" && global.Clients.GetRegistry().GetDefaultInstance() == "" {
|
||||
if address == "" && global.Instances.GetRegistry().GetDefaultInstance() == "" {
|
||||
fmt.Println("No instances available for sending messages")
|
||||
return nil
|
||||
}
|
||||
@@ -620,11 +620,6 @@ func CleanupTaskManager() {
|
||||
}
|
||||
}
|
||||
|
||||
// NewTaskManagerForAddress is an exported wrapper around task.NewManagerForAddress
|
||||
func NewTaskManagerForAddress(ctx context.Context, address string) (*task.Manager, error) {
|
||||
return task.NewManagerForAddress(ctx, address)
|
||||
}
|
||||
|
||||
// CreateAndFollowTask creates a new task and immediately follows it in interactive mode
|
||||
// This is used by the root command to provide a streamlined UX
|
||||
func CreateAndFollowTask(ctx context.Context, prompt string, opts TaskOptions) error {
|
||||
|
||||
@@ -13,38 +13,41 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/output"
|
||||
"github.com/cline/cli/pkg/cli/slash"
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
)
|
||||
|
||||
// InputHandler manages interactive user input during follow mode
|
||||
type InputHandler struct {
|
||||
manager *Manager
|
||||
coordinator *StreamCoordinator
|
||||
cancelFunc context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
isRunning bool
|
||||
pollTicker *time.Ticker
|
||||
program *tea.Program
|
||||
programRunning bool
|
||||
programDoneChan chan struct{} // Signals when program actually exits
|
||||
resultChan chan output.InputSubmitMsg
|
||||
cancelChan chan struct{}
|
||||
feedbackApproval bool // Track if we're in feedback after approval
|
||||
feedbackApproved bool // Track the approval decision
|
||||
approvalMessage *types.ClineMessage // Store the approval message for determining action
|
||||
ctx context.Context // Context for restart callback
|
||||
manager *Manager
|
||||
coordinator *StreamCoordinator
|
||||
cancelFunc context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
isRunning bool
|
||||
pollTicker *time.Ticker
|
||||
program *tea.Program
|
||||
programRunning bool
|
||||
programDoneChan chan struct{} // Signals when program actually exits
|
||||
resultChan chan output.InputSubmitMsg
|
||||
cancelChan chan struct{}
|
||||
feedbackApproval bool // Track if we're in feedback after approval
|
||||
feedbackApproved bool // Track the approval decision
|
||||
approvalMessage *types.ClineMessage // Store the approval message for determining action
|
||||
slashCommandRegistry *slash.Registry // Slash command registry for autocomplete
|
||||
ctx context.Context // Context for restart callback
|
||||
}
|
||||
|
||||
// NewInputHandler creates a new input handler
|
||||
func NewInputHandler(manager *Manager, coordinator *StreamCoordinator, cancelFunc context.CancelFunc) *InputHandler {
|
||||
return &InputHandler{
|
||||
manager: manager,
|
||||
coordinator: coordinator,
|
||||
cancelFunc: cancelFunc,
|
||||
isRunning: false,
|
||||
pollTicker: time.NewTicker(500 * time.Millisecond),
|
||||
resultChan: make(chan output.InputSubmitMsg, 1),
|
||||
cancelChan: make(chan struct{}, 1),
|
||||
manager: manager,
|
||||
coordinator: coordinator,
|
||||
cancelFunc: cancelFunc,
|
||||
isRunning: false,
|
||||
pollTicker: time.NewTicker(500 * time.Millisecond),
|
||||
resultChan: make(chan output.InputSubmitMsg, 1),
|
||||
slashCommandRegistry: slash.NewRegistry(context.Background()),
|
||||
cancelChan: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +166,7 @@ func (ih *InputHandler) Start(ctx context.Context, errChan chan error) {
|
||||
|
||||
if shouldSend {
|
||||
// Check for mode switch commands first
|
||||
newMode, remainingMessage, isModeSwitch := ih.parseModeSwitch(message)
|
||||
newMode, remainingMessage, isModeSwitch := slash.ParseModeSwitch(message)
|
||||
if isModeSwitch {
|
||||
// Create styles for mode switch messages (respect global color profile)
|
||||
actStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("39")).Bold(true)
|
||||
@@ -284,9 +287,9 @@ func (ih *InputHandler) promptForInput(ctx context.Context) (string, bool, error
|
||||
model := output.NewInputModelWithRegistry(
|
||||
output.InputTypeMessage,
|
||||
"Cline is ready for your message...",
|
||||
"/plan or /act to switch modes\nctrl+e to open editor\ntab to autocomplete commands",
|
||||
"/plan or /act to switch modes\ntab to autocomplete commands\nctrl+e to open editor\nctrl+c to exit",
|
||||
currentMode,
|
||||
ih.manager.GetSlashRegistry(),
|
||||
ih.slashCommandRegistry,
|
||||
)
|
||||
|
||||
return ih.runInputProgram(ctx, model)
|
||||
@@ -302,7 +305,7 @@ func (ih *InputHandler) promptForApproval(ctx context.Context, msg *types.ClineM
|
||||
"Let Cline use this tool?",
|
||||
"",
|
||||
ih.manager.GetCurrentMode(),
|
||||
ih.manager.GetSlashRegistry(), // Pass registry for feedback input after approval
|
||||
ih.slashCommandRegistry,
|
||||
)
|
||||
|
||||
message, shouldSend, err := ih.runInputProgram(ctx, model)
|
||||
@@ -478,24 +481,6 @@ func (w *inputProgramWrapper) View() string {
|
||||
return w.model.View()
|
||||
}
|
||||
|
||||
// parseModeSwitch checks if message starts with /act or /plan and extracts the mode and remaining message
|
||||
func (ih *InputHandler) parseModeSwitch(message string) (string, string, bool) {
|
||||
trimmed := strings.TrimSpace(message)
|
||||
lower := strings.ToLower(trimmed)
|
||||
|
||||
if strings.HasPrefix(lower, "/plan") {
|
||||
remaining := strings.TrimSpace(trimmed[5:])
|
||||
return "plan", remaining, true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(lower, "/act") {
|
||||
remaining := strings.TrimSpace(trimmed[4:])
|
||||
return "act", remaining, true
|
||||
}
|
||||
|
||||
return "", message, false
|
||||
}
|
||||
|
||||
// handleSpecialCommand processes special commands like /cancel, /exit
|
||||
func (ih *InputHandler) handleSpecialCommand(ctx context.Context, message string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(message)) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/cline/cli/pkg/cli/display"
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/cli/pkg/cli/handlers"
|
||||
"github.com/cline/cli/pkg/cli/slash"
|
||||
"github.com/cline/cli/pkg/cli/types"
|
||||
"github.com/cline/grpc-go/client"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
@@ -38,7 +37,6 @@ type Manager struct {
|
||||
systemRenderer *display.SystemMessageRenderer
|
||||
streamingDisplay *display.StreamingDisplay
|
||||
handlerRegistry *handlers.HandlerRegistry
|
||||
slashRegistry *slash.Registry
|
||||
isStreamingMode bool
|
||||
isInteractive bool
|
||||
currentMode string // "plan" or "act"
|
||||
@@ -68,7 +66,6 @@ func NewManager(client *client.ClineClient) *Manager {
|
||||
systemRenderer: systemRenderer,
|
||||
streamingDisplay: streamingDisplay,
|
||||
handlerRegistry: registry,
|
||||
slashRegistry: slash.NewRegistry(),
|
||||
currentMode: "plan", // Default mode
|
||||
}
|
||||
}
|
||||
@@ -83,9 +80,6 @@ func NewManagerForAddress(ctx context.Context, address string) (*Manager, error)
|
||||
manager := NewManager(client)
|
||||
manager.clientAddress = address
|
||||
|
||||
// Fetch slash commands from backend (non-blocking, errors are logged)
|
||||
manager.fetchSlashCommands(ctx)
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
@@ -99,29 +93,13 @@ func NewManagerForDefault(ctx context.Context) (*Manager, error) {
|
||||
manager := NewManager(client)
|
||||
|
||||
// Get the default instance address
|
||||
if global.Clients != nil {
|
||||
manager.clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
|
||||
if global.Instances != nil {
|
||||
manager.clientAddress = global.Instances.GetRegistry().GetDefaultInstance()
|
||||
}
|
||||
|
||||
// Fetch slash commands from backend (non-blocking, errors are logged)
|
||||
manager.fetchSlashCommands(ctx)
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// fetchSlashCommands fetches available slash commands from the backend
|
||||
// This is non-blocking and errors are logged but don't prevent manager creation
|
||||
func (m *Manager) fetchSlashCommands(ctx context.Context) {
|
||||
if err := m.slashRegistry.FetchFromBackend(ctx, m.client); err != nil {
|
||||
if global.Config.Verbose {
|
||||
m.renderer.RenderDebug("Failed to fetch slash commands: %v", err)
|
||||
}
|
||||
// Non-fatal: CLI-local commands are still available
|
||||
} else if global.Config.Verbose {
|
||||
m.renderer.RenderDebug("Loaded %d slash commands", len(m.slashRegistry.GetCommands()))
|
||||
}
|
||||
}
|
||||
|
||||
// SwitchToInstance switches the manager to use a different Cline instance
|
||||
func (m *Manager) SwitchToInstance(ctx context.Context, address string) error {
|
||||
m.mu.Lock()
|
||||
@@ -1206,11 +1184,11 @@ func (m *Manager) displayMessage(msg *types.ClineMessage, isLast, isPartial bool
|
||||
m.mu.RUnlock()
|
||||
|
||||
dc := &handlers.DisplayContext{
|
||||
State: m.state,
|
||||
Renderer: m.renderer,
|
||||
ToolRenderer: m.toolRenderer,
|
||||
HookRenderer: m.hookRenderer,
|
||||
SystemRenderer: m.systemRenderer,
|
||||
State: m.state,
|
||||
Renderer: m.renderer,
|
||||
ToolRenderer: m.toolRenderer,
|
||||
HookRenderer: m.hookRenderer,
|
||||
SystemRenderer: m.systemRenderer,
|
||||
IsLast: isLast,
|
||||
IsPartial: isPartial,
|
||||
Verbose: global.Config.Verbose,
|
||||
@@ -1320,11 +1298,6 @@ func (m *Manager) GetCurrentMode() string {
|
||||
return m.currentMode
|
||||
}
|
||||
|
||||
// GetSlashRegistry returns the slash command registry
|
||||
func (m *Manager) GetSlashRegistry() *slash.Registry {
|
||||
return m.slashRegistry
|
||||
}
|
||||
|
||||
// extractModeFromState extracts the current mode from state JSON
|
||||
func (m *Manager) extractModeFromState(stateJson string) string {
|
||||
var rawState map[string]interface{}
|
||||
|
||||
@@ -290,12 +290,6 @@ func setSimpleField(settings *cline.Settings, key, value string) error {
|
||||
return err
|
||||
}
|
||||
settings.ActModeAwsBedrockCustomSelected = boolPtr(val)
|
||||
case "hooks_enabled":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.HooksEnabled = boolPtr(val)
|
||||
case "azure_identity":
|
||||
val, err := parseBool(value)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// This is the canonical definition used across all CLI packages
|
||||
type CoreInstanceInfo struct {
|
||||
// Full core address including port
|
||||
Address string `json:"address"`
|
||||
CoreAddress string `json:"address"`
|
||||
// Host bridge service address that core holds (host is ALWAYS running on localhost FYI)
|
||||
HostServiceAddress string `json:"host_port"`
|
||||
Status grpc_health_v1.HealthCheckResponse_ServingStatus `json:"status"`
|
||||
@@ -20,7 +20,7 @@ type CoreInstanceInfo struct {
|
||||
}
|
||||
|
||||
func (c *CoreInstanceInfo) CorePort() int {
|
||||
_, port, _ := ParseHostPort(c.Address)
|
||||
_, port, _ := ParseHostPort(c.CoreAddress)
|
||||
return port
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package hostbridge
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/cline/grpc-go/host"
|
||||
)
|
||||
|
||||
// WatchService implements the host.WatchServiceServer interface
|
||||
type WatchService struct {
|
||||
host.UnimplementedWatchServiceServer
|
||||
coreAddress string
|
||||
verbose bool
|
||||
}
|
||||
|
||||
// NewWatchService creates a new WatchService
|
||||
func NewWatchService(coreAddress string, verbose bool) *WatchService {
|
||||
return &WatchService{
|
||||
coreAddress: coreAddress,
|
||||
verbose: verbose,
|
||||
}
|
||||
}
|
||||
|
||||
// SubscribeToFile subscribes to file change notifications
|
||||
func (s *WatchService) SubscribeToFile(req *host.SubscribeToFileRequest, stream host.WatchService_SubscribeToFileServer) error {
|
||||
if s.verbose {
|
||||
log.Printf("SubscribeToFile called for path: %s", req.GetPath())
|
||||
}
|
||||
|
||||
// For console implementation, we'll just log that we would watch the file
|
||||
// In a real implementation, we'd use fsnotify or similar to watch file changes
|
||||
log.Printf("[Cline] Would watch file: %s", req.GetPath())
|
||||
|
||||
// Keep the stream open but don't send any events for now
|
||||
// In a real implementation, we'd send FileChangeEvent messages when files change
|
||||
<-stream.Context().Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/cline/grpc-go/cline"
|
||||
"github.com/cline/grpc-go/host"
|
||||
)
|
||||
|
||||
@@ -43,24 +44,25 @@ func (s *WorkspaceService) GetWorkspacePaths(ctx context.Context, req *host.GetW
|
||||
// SaveOpenDocumentIfDirty saves an open document if it has unsaved changes
|
||||
func (s *WorkspaceService) SaveOpenDocumentIfDirty(ctx context.Context, req *host.SaveOpenDocumentIfDirtyRequest) (*host.SaveOpenDocumentIfDirtyResponse, error) {
|
||||
if s.verbose {
|
||||
log.Printf("SaveOpenDocumentIfDirty called for path: %s", req.GetPath())
|
||||
log.Printf("SaveOpenDocumentIfDirty called for path: %v", req.FilePath)
|
||||
}
|
||||
|
||||
// For console implementation, we'll assume the document is already saved
|
||||
// In a real implementation, we'd check if the file has unsaved changes
|
||||
f := false
|
||||
return &host.SaveOpenDocumentIfDirtyResponse{
|
||||
WasSaved: false, // Assume no changes to save
|
||||
WasSaved: &f, // Assume no changes to save
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetDiagnostics returns diagnostic information for a file
|
||||
func (s *WorkspaceService) GetDiagnostics(ctx context.Context, req *host.GetDiagnosticsRequest) (*host.GetDiagnosticsResponse, error) {
|
||||
if s.verbose {
|
||||
log.Printf("GetDiagnostics called for path: %s", req.GetPath())
|
||||
log.Printf("GetDiagnostics called")
|
||||
}
|
||||
|
||||
// For console implementation, return empty diagnostics
|
||||
return &host.GetDiagnosticsResponse{
|
||||
Diagnostics: []*host.Diagnostic{},
|
||||
FileDiagnostics: []*cline.FileDiagnostics{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 8.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.2 MiB |
@@ -148,6 +148,7 @@
|
||||
"features/hooks/samples"
|
||||
]
|
||||
},
|
||||
"features/jupyter-notebooks",
|
||||
"features/multiroot-workspace",
|
||||
"features/plan-and-act",
|
||||
"features/skills",
|
||||
@@ -198,6 +199,7 @@
|
||||
"provider-config/anthropic",
|
||||
"provider-config/claude-code",
|
||||
"provider-config/openai",
|
||||
"provider-config/openai-codex",
|
||||
"provider-config/openrouter",
|
||||
"provider-config/cerebras",
|
||||
"provider-config/deepseek",
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: "Jupyter Notebooks"
|
||||
description: "AI-assisted editing of Jupyter notebooks with cell-level context awareness"
|
||||
---
|
||||
|
||||
# Jupyter Notebooks
|
||||
|
||||
Cline provides comprehensive support for Jupyter notebooks (`.ipynb` files), enabling AI-assisted editing with full cell-level context awareness. This feature was developed in collaboration with Amazon to bring AI coding assistance to data science workflows.
|
||||
|
||||
## Getting started
|
||||
|
||||
Jupyter notebook support is a built-in feature of Cline. To use it, you just need to have the Jupyter notebook extension enabled in VS Code. Once you open any `.ipynb` file, you'll see AI-assisted buttons in your notebook interface.
|
||||
|
||||
## How to use
|
||||
|
||||
### Generate Cell
|
||||
|
||||
Click the sparkle icon (✨) in the notebook toolbar to generate new cells with AI assistance.
|
||||
|
||||

|
||||
|
||||
**How it works:** The AI receives context from surrounding cells, so it understands the variables and imports already in scope. This means you can reference existing DataFrames, functions, and other objects without re-explaining them.
|
||||
|
||||
**Example prompt:** "Create a visualization showing the correlation matrix of numeric columns with a heatmap"
|
||||
|
||||
The cell is inserted with proper notebook JSON structure, preserving metadata and ready to execute.
|
||||
|
||||
### Explain Cell
|
||||
|
||||
Click the Explain button in any cell's title bar to get a detailed explanation of what the cell does.
|
||||
|
||||
This is useful for:
|
||||
|
||||
- Revisiting old notebooks
|
||||
- Onboarding to a teammate's analysis
|
||||
- Understanding complex transformations
|
||||
|
||||
**How it works:** Cline extracts the full cell context, including outputs, so explanations can reference actual results like column names, row counts, and computed values.
|
||||
|
||||
### Improve Cell
|
||||
|
||||
Click the Improve button in any cell's title bar to enhance existing cells with AI suggestions.
|
||||
|
||||

|
||||
|
||||
Use this to:
|
||||
|
||||
- Optimize slow pandas operations
|
||||
- Add error handling
|
||||
- Refactor for clarity
|
||||
- Convert loops to vectorized operations
|
||||
|
||||
**How it works:** Cline suggests improvements while preserving the cell's position and metadata in the notebook structure. The AI explains what was changed and why.
|
||||
|
||||
## How cell context works
|
||||
|
||||
Unlike traditional file editing, Jupyter notebooks are JSON documents containing arrays of cells. Each cell has its own type, source content, metadata, execution count, and outputs.
|
||||
|
||||
When you use a Jupyter command, Cline extracts structured context that includes:
|
||||
|
||||
- **Cell type** (code, markdown, or raw)
|
||||
- **Source content** as an array of lines
|
||||
- **Cell metadata** and configuration
|
||||
- **Execution count** for code cells
|
||||
- **Outputs** including data, text, and error traces
|
||||
|
||||
This structured representation allows the AI to understand not just the code, but its context within the notebook and its actual output.
|
||||
|
||||
### JSON structure preservation
|
||||
|
||||
Cline is designed to work carefully with the cell JSON structure, aiming to:
|
||||
|
||||
- Keep cell boundaries intact
|
||||
- Preserve execution counts
|
||||
- Maintain cell metadata
|
||||
- Keep outputs associated with their source cells
|
||||
|
||||
The AI is specifically prompted to preserve notebook structure, though you should always review changes to ensure your notebook format remains correct.
|
||||
|
||||
## Keyboard shortcuts
|
||||
|
||||
You can bind any of these commands to keyboard shortcuts for faster access:
|
||||
|
||||
1. Open VS Code keyboard shortcuts (Cmd/Ctrl + K, Cmd/Ctrl + S)
|
||||
2. Search for `cline.jupyterGenerateCell`, `cline.jupyterExplainCell`, or `cline.jupyterImproveCell`
|
||||
3. Assign your preferred key combinations
|
||||
|
||||
## Tips for best results
|
||||
|
||||
**For Generate Cell:**
|
||||
- Be specific about what you want the cell to do
|
||||
- Reference existing variables by name (the AI can see them)
|
||||
- Mention preferred libraries if you have a preference (e.g., "use seaborn" or "use plotly")
|
||||
|
||||
**For Explain Cell:**
|
||||
- Works best on cells that have been executed (outputs provide additional context)
|
||||
- Good for complex chained operations like pandas groupby/merge sequences
|
||||
|
||||
**For Improve Cell:**
|
||||
- Mention what aspect you want to improve (performance, readability, error handling)
|
||||
- The AI will explain the changes it suggests
|
||||
|
||||
## Limitations
|
||||
|
||||
- Notebook support requires the Jupyter notebook extension to be enabled in VS Code
|
||||
- Cell context extraction depends on VS Code's notebook API
|
||||
- Very large notebooks may require more context than some models can handle efficiently
|
||||
|
||||
## Related
|
||||
|
||||
- [Cline Tools Guide](/exploring-clines-tools/cline-tools-guide) -- Overview of all Cline commands
|
||||
- [Model Selection Guide](/core-features/model-selection-guide) -- Choosing the right model for your workflow
|
||||
@@ -25,11 +25,28 @@ The easiest way to get started is with **Cline** as your provider:
|
||||
|
||||
## Alternative: Use Another Provider
|
||||
|
||||
If you prefer to use your own API keys, you can select from providers like:
|
||||
If you prefer to use your own accounts or API keys, you have several options:
|
||||
|
||||
### Sign in with OpenAI (Recommended)
|
||||
|
||||
The easiest way to use OpenAI models is with **OpenAI Codex**—no API keys needed:
|
||||
|
||||
1. Select **"OpenAI Codex"** from the API Provider dropdown
|
||||
2. Click **"Sign in with OpenAI"**
|
||||
3. Authorize Cline in your browser
|
||||
4. Choose your model
|
||||
|
||||
<Card title="OpenAI Codex Setup Guide" icon="key" href="/provider-config/openai-codex">
|
||||
See the full setup guide with screenshots and troubleshooting tips.
|
||||
</Card>
|
||||
|
||||
### Other Providers
|
||||
|
||||
You can also use API keys with these providers:
|
||||
|
||||
- **OpenRouter** - Great value, multiple models
|
||||
- **Anthropic** - Direct access to Claude models
|
||||
- **OpenAI** - Access to GPT models
|
||||
- **OpenAI** - Access to GPT models via API key
|
||||
- **Google Gemini** - Google's AI models
|
||||
- **Ollama** - Run models locally on your computer
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ Cerebras delivers the world's fastest AI inference through their revolutionary w
|
||||
|
||||
Cline supports the following Cerebras models:
|
||||
|
||||
- `zai-glm-4.6` - Fast general-purpose model on Cerebras (up to 1,000 tokens/s). To be deprecated soon.
|
||||
- `zai-glm-4.7` - Highly capable general-purpose model on Cerebras (up to 1,000 tokens/s), competitive with leading proprietary models on coding tasks.
|
||||
- `qwen-3-235b-a22b-instruct-2507` - Advanced instruction-following model
|
||||
- `qwen-3-235b-a22b-thinking-2507` - Reasoning model with step-by-step thinking
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: "OpenAI Codex"
|
||||
sidebarTitle: "OpenAI Codex"
|
||||
description: "Connect your OpenAI account to Cline via OAuth for seamless access to OpenAI models."
|
||||
---
|
||||
|
||||
OpenAI Codex lets you connect your OpenAI account directly to Cline using OAuth, meaning you don't need to manage API keys. Simply sign in with your OpenAI account through a one-click browser authentication, and you'll automatically have access to all the models available on your OpenAI plan. No additional configuration required.
|
||||
|
||||
<Tip>
|
||||
If you prefer to use API keys instead, see the [OpenAI (API Key)](/provider-config/openai) provider configuration.
|
||||
</Tip>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An [OpenAI account](https://platform.openai.com/signup)
|
||||
- An active OpenAI subscription or API access plan
|
||||
|
||||
<Note>
|
||||
The models available to you depend on your OpenAI account's subscription tier. See [OpenAI's pricing page](https://openai.com/pricing) for details on what's included in each plan.
|
||||
</Note>
|
||||
|
||||
## Connecting Your OpenAI Account
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Cline Settings">
|
||||
Click the settings gear icon in the top-right corner of the Cline extension.
|
||||
</Step>
|
||||
<Step title="Select OpenAI Codex Provider">
|
||||
Choose **"OpenAI Codex"** from the "API Provider" dropdown menu.
|
||||
</Step>
|
||||
<Step title="Sign In with OpenAI">
|
||||
Click the **"Sign in with OpenAI"** button. This will open a browser window for authentication.
|
||||
</Step>
|
||||
<Step title="Authorize Cline">
|
||||
In the browser window that opens:
|
||||
1. Sign in to your OpenAI account (if not already signed in)
|
||||
2. Review the permissions Cline is requesting
|
||||
3. Click **"Authorize"** to grant access
|
||||
</Step>
|
||||
<Step title="Select Your Model">
|
||||
Once authorized, you'll be returned to Cline. Choose your desired model from the **"Model"** dropdown.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
If a model doesn't appear in your dropdown, it may not be available on your OpenAI subscription tier.
|
||||
</Note>
|
||||
|
||||
For the most current list of available models and their capabilities, see the official [OpenAI Models documentation](https://platform.openai.com/docs/models).
|
||||
|
||||
## Managing Your Connection
|
||||
|
||||
### Disconnecting Your Account
|
||||
|
||||
To disconnect your OpenAI account from Cline:
|
||||
|
||||
1. Open Cline Settings
|
||||
2. With OpenAI Codex selected as the provider, click **"Sign out"**
|
||||
3. Confirm the disconnection
|
||||
|
||||
This removes the OAuth connection. You can reconnect at any time by signing in again.
|
||||
|
||||
### Token Refresh
|
||||
|
||||
OAuth tokens are automatically refreshed by Cline. If you encounter authentication errors:
|
||||
|
||||
1. Try disconnecting and reconnecting your account
|
||||
2. Ensure your OpenAI account is in good standing
|
||||
3. Check that your subscription is active
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Authentication failed" error
|
||||
|
||||
- Ensure you're signing in with the correct OpenAI account
|
||||
- Check that your OpenAI subscription is active
|
||||
- Try clearing your browser cache and signing in again
|
||||
|
||||
### Models not appearing
|
||||
|
||||
- The available models depend on your OpenAI subscription tier
|
||||
- Some models may require specific plan upgrades
|
||||
- Check [OpenAI's pricing page](https://openai.com/pricing) for model availability by plan
|
||||
|
||||
### OAuth window doesn't open
|
||||
|
||||
- Check if pop-ups are blocked in your browser
|
||||
- Try using a different browser
|
||||
- Ensure you have a stable internet connection
|
||||
|
||||
### Connection keeps expiring
|
||||
|
||||
- This is rare but can happen if your OpenAI account session expired
|
||||
- Disconnect and reconnect to refresh your authentication
|
||||
|
||||
## Good to Know
|
||||
|
||||
Cline only requests the permissions necessary to make API calls on your behalf. Your OpenAI credentials are never stored by Cline.
|
||||
|
||||
When using OpenAI Codex, Cline accesses models through your OpenAI/ChatGPT subscription. There is no separate per-token API billing or consumption of OpenAI API credits for this provider; usage and limits are governed by your ChatGPT (or OpenAI account) subscription plan, not by Cline.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [OpenAI (API Key)](/provider-config/openai): Alternative configuration using API keys
|
||||
- [Model Selection Guide](/core-features/model-selection-guide): Help choosing the right model
|
||||
Generated
+48
-65
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.51.0",
|
||||
"version": "3.52.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.51.0",
|
||||
"version": "3.52.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -1184,6 +1184,7 @@
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -2646,6 +2647,7 @@
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.9.15",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.7.8",
|
||||
"@types/node": ">=12.12.47"
|
||||
@@ -3229,6 +3231,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz",
|
||||
"integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -3297,6 +3300,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -4912,8 +4916,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4926,8 +4929,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4940,8 +4942,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4954,8 +4955,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4968,8 +4968,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.52.4",
|
||||
@@ -4982,8 +4981,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -4996,8 +4994,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.52.4",
|
||||
@@ -5010,8 +5007,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5024,8 +5020,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5038,8 +5033,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5052,8 +5046,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5066,8 +5059,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5080,8 +5072,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5094,8 +5085,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5108,8 +5098,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5122,8 +5111,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.52.4",
|
||||
@@ -5136,8 +5124,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.52.4",
|
||||
@@ -5150,8 +5137,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5164,8 +5150,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5178,8 +5163,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.52.4",
|
||||
@@ -5192,8 +5176,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.52.4",
|
||||
@@ -5206,8 +5189,7 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true
|
||||
]
|
||||
},
|
||||
"node_modules/@sap-ai-sdk/ai-api": {
|
||||
"version": "2.1.0",
|
||||
@@ -6770,8 +6752,7 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/get-folder-size": {
|
||||
"version": "3.0.4",
|
||||
@@ -6803,6 +6784,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz",
|
||||
"integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -7519,6 +7501,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -8320,6 +8303,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
@@ -8654,9 +8638,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/undici": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
@@ -9525,7 +9509,8 @@
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1342118",
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.2.0",
|
||||
@@ -12476,6 +12461,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
@@ -12709,6 +12695,7 @@
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
@@ -14860,9 +14847,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/open-graph-scraper/node_modules/undici": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
|
||||
"integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==",
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
@@ -15430,6 +15417,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -15611,7 +15599,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -15632,7 +15619,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -16322,7 +16308,6 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz",
|
||||
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -17800,7 +17785,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3"
|
||||
@@ -17817,7 +17801,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -18178,6 +18161,7 @@
|
||||
"version": "5.5.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -18250,9 +18234,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
|
||||
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
|
||||
"version": "7.19.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.19.0.tgz",
|
||||
"integrity": "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
@@ -18430,7 +18414,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -18505,7 +18488,6 @@
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
@@ -19193,6 +19175,7 @@
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
+1
-1
@@ -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.51.0",
|
||||
"version": "3.52.0",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
|
||||
@@ -108,7 +108,6 @@ message Secrets {
|
||||
message Settings {
|
||||
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;
|
||||
@@ -257,7 +256,6 @@ message Settings {
|
||||
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;
|
||||
@@ -278,6 +276,7 @@ message Settings {
|
||||
optional int32 open_telemetry_log_batch_timeout = 170;
|
||||
optional int32 open_telemetry_log_max_queue_size = 171;
|
||||
optional bool worktrees_enabled = 172;
|
||||
map<string, string> open_ai_headers = 173;
|
||||
}
|
||||
|
||||
message DictationSettings {
|
||||
@@ -384,6 +383,8 @@ message UpdateTaskSettingsRequest {
|
||||
|
||||
// Message for updating settings
|
||||
message UpdateSettingsRequest {
|
||||
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
|
||||
|
||||
Metadata metadata = 1;
|
||||
optional ModelsApiConfiguration api_configuration = 2;
|
||||
optional string telemetry_setting = 3;
|
||||
@@ -408,7 +409,6 @@ message UpdateSettingsRequest {
|
||||
optional DictationSettings dictation_settings = 23;
|
||||
optional double auto_condense_threshold = 24;
|
||||
optional bool multi_root_enabled = 25;
|
||||
optional bool hooks_enabled = 26;
|
||||
optional string vscode_terminal_execution_mode = 27;
|
||||
optional int32 max_consecutive_mistakes = 28;
|
||||
optional bool subagents_enabled = 29;
|
||||
|
||||
@@ -70,6 +70,7 @@ enum ClineSay {
|
||||
HOOK_STATUS = 30;
|
||||
HOOK_OUTPUT_STREAM = 31;
|
||||
COMMAND_PERMISSION_DENIED = 32;
|
||||
CONDITIONAL_RULES_APPLIED = 33;
|
||||
}
|
||||
|
||||
// Enum for ClineSayTool tool types
|
||||
|
||||
@@ -105,7 +105,7 @@ async function main(): Promise<void> {
|
||||
console.log("Extracting standalone.zip to extensions directory...")
|
||||
try {
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
execSync(`unzip -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
|
||||
execSync(`unzip -o -q "${standaloneZipPath}" -d "${extensionsDir}"`, { stdio: "inherit" })
|
||||
}
|
||||
console.log(`Successfully extracted standalone.zip to: ${extensionsDir}`)
|
||||
} catch (error) {
|
||||
|
||||
+7
-8
@@ -2,18 +2,18 @@ import * as vscode from "vscode"
|
||||
import {
|
||||
cleanupMcpMarketplaceCatalogFromGlobalState,
|
||||
migrateCustomInstructionsToGlobalRules,
|
||||
migrateHooksEnabledToBoolean,
|
||||
migrateTaskHistoryToFile,
|
||||
migrateWelcomeViewCompleted,
|
||||
migrateWorkspaceToGlobalStorage,
|
||||
} from "./core/storage/state-migrations"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import { Logger } from "./services/logging/Logger"
|
||||
import "./utils/path" // necessary to have access to String.prototype.toPosix
|
||||
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { FileContextTracker } from "./core/context/context-tracking/FileContextTracker"
|
||||
import { StateManager } from "./core/storage/StateManager"
|
||||
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
|
||||
import { ExtensionRegistryInfo } from "./registry"
|
||||
import { BannerService } from "./services/banner/BannerService"
|
||||
import { audioRecordingService } from "./services/dictation/AudioRecordingService"
|
||||
@@ -26,7 +26,6 @@ import { ShowMessageType } from "./shared/proto/host/window"
|
||||
import { syncWorker } from "./shared/services/worker/sync"
|
||||
import { getBlobStoreSettingsFromEnv } from "./shared/services/worker/worker"
|
||||
import { getLatestAnnouncementId } from "./utils/announcements"
|
||||
import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
|
||||
import { arePathsEqual } from "./utils/path"
|
||||
/**
|
||||
* Performs intialization for Cline that is common to all platforms.
|
||||
@@ -35,10 +34,13 @@ import { arePathsEqual } from "./utils/path"
|
||||
* @returns The webview provider
|
||||
*/
|
||||
export async function initialize(context: vscode.ExtensionContext): Promise<WebviewProvider> {
|
||||
// Configure the shared Logging class to use HostProvider's output channel
|
||||
Logger.setOutput((msg: string) => HostProvider.get().logToChannel(msg))
|
||||
|
||||
try {
|
||||
await StateManager.initialize(context)
|
||||
} catch (error) {
|
||||
console.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
|
||||
Logger.error("[Controller] CRITICAL: Failed to initialize StateManager - extension may not function properly:", error)
|
||||
HostProvider.window.showMessage({
|
||||
type: ShowMessageType.ERROR,
|
||||
message: "Failed to initialize Cline's application state. Please restart the extension.",
|
||||
@@ -70,9 +72,6 @@ export async function initialize(context: vscode.ExtensionContext): Promise<Webv
|
||||
// Ensure taskHistory.json exists and migrate legacy state (runs once)
|
||||
await migrateTaskHistoryToFile(context)
|
||||
|
||||
// Migrate hooksEnabled from ClineFeatureSetting to boolean (one-time cleanup)
|
||||
await migrateHooksEnabledToBoolean(context)
|
||||
|
||||
// Clean up MCP marketplace catalog from global state (moved to disk cache)
|
||||
await cleanupMcpMarketplaceCatalogFromGlobalState(context)
|
||||
|
||||
@@ -127,7 +126,7 @@ async function showVersionUpdateAnnouncement(context: vscode.ExtensionContext) {
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
|
||||
Logger.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ class ClineEndpoint {
|
||||
this.environment = Environment.production
|
||||
break
|
||||
}
|
||||
console.info("Cline environment updated: ", this.environment)
|
||||
}
|
||||
|
||||
public getEnvironment(): EnvironmentConfig {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ApiConfiguration, ModelInfo, QwenApiRegions } from "@shared/api"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ClineTool } from "@/shared/tools"
|
||||
import { AIhubmixHandler } from "./providers/aihubmix"
|
||||
import { AnthropicHandler } from "./providers/anthropic"
|
||||
@@ -480,7 +481,7 @@ export function buildApiHandler(configuration: ApiConfiguration, mode: Mode): Ap
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("buildApiHandler error:", error)
|
||||
Logger.error("buildApiHandler error:", error)
|
||||
}
|
||||
|
||||
return createHandlerForProvider(apiProvider, options, mode)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AskSageModelId, askSageDefaultModelId, askSageDefaultURL, askSageModels, ModelInfo } from "@shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -58,7 +59,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
private apiKey: string
|
||||
|
||||
constructor(options: AskSageHandlerOptions) {
|
||||
console.log("init api url", options.asksageApiUrl, askSageDefaultURL)
|
||||
Logger.log("init api url", options.asksageApiUrl, askSageDefaultURL)
|
||||
this.options = options
|
||||
this.apiKey = options.asksageApiKey || ""
|
||||
this.apiUrl = options.asksageApiUrl || askSageDefaultURL
|
||||
@@ -164,7 +165,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Failed to fetch AskSage usage", await response.text())
|
||||
Logger.error("Failed to fetch AskSage usage", await response.text())
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -177,7 +178,7 @@ export class AskSageHandler implements ApiHandler {
|
||||
outputTokens: 0,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching AskSage usage:", error)
|
||||
Logger.error("Error fetching AskSage usage:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { BedrockModelId, bedrockDefaultModelId, bedrockModels, CLAUDE_SONNET_1M_
|
||||
import { calculateApiCostOpenAI, calculateApiCostQwen } from "@utils/cost"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
@@ -449,7 +450,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing Deepseek response chunk:", error)
|
||||
Logger.error("Error parsing Deepseek response chunk:", error)
|
||||
// Propagate the error by yielding a text response with error information
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -550,7 +551,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
// Debug logging to see actual response structure
|
||||
// console.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
|
||||
// Logger.log("Bedrock chunk:", JSON.stringify(chunk, null, 2))
|
||||
|
||||
// Handle thinking response in additionalModelResponseFields (LangChain format)
|
||||
const metadata = chunk.metadata as ExtendedMetadata | undefined
|
||||
@@ -854,7 +855,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Log unsupported content types for debugging
|
||||
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
Logger.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
return null
|
||||
})
|
||||
.filter((item): item is ContentBlock => item !== null)
|
||||
@@ -912,7 +913,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to process image content:", error)
|
||||
Logger.error("Failed to process image content:", error)
|
||||
// Return a text content indicating the error instead of null
|
||||
// This ensures users are aware of the issue
|
||||
return {
|
||||
@@ -1111,7 +1112,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error with OpenAI model via Converse API:", error)
|
||||
Logger.error("Error with OpenAI model via Converse API:", error)
|
||||
|
||||
// Try to extract more detailed error information
|
||||
let errorMessage = "Failed to process OpenAI model request"
|
||||
@@ -1246,7 +1247,7 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error with Qwen model via Converse API:", error)
|
||||
Logger.error("Error with Qwen model via Converse API:", error)
|
||||
|
||||
// Try to extract more detailed error information
|
||||
let errorMessage = "Failed to process Qwen model request"
|
||||
|
||||
@@ -7,10 +7,10 @@ import { ClineEnv } from "@/config"
|
||||
import { ClineAccountService } from "@/services/account/ClineAccountService"
|
||||
import { AuthService } from "@/services/auth/AuthService"
|
||||
import { buildClineExtraHeaders } from "@/services/EnvUtils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { CLINE_ACCOUNT_AUTH_ERROR_MESSAGE } from "@/shared/ClineAccount"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
@@ -126,7 +126,7 @@ export class ClineHandler implements ApiHandler {
|
||||
// openrouter returns an error object instead of the openai sdk throwing an error
|
||||
if ("error" in chunk) {
|
||||
const error = chunk.error as OpenRouterErrorResponse["error"]
|
||||
console.error(`Cline API Error: ${error?.code} - ${error?.message}`)
|
||||
Logger.error(`Cline API Error: ${error?.code} - ${error?.message}`)
|
||||
// Include metadata in the error message if available
|
||||
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
|
||||
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
|
||||
@@ -143,7 +143,7 @@ export class ClineHandler implements ApiHandler {
|
||||
const choiceWithError = choice as any
|
||||
if (choiceWithError.error) {
|
||||
const error = choiceWithError.error
|
||||
console.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
Logger.error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
throw new Error(`Cline Mid-Stream Error: ${error.code || error.type || "Unknown"} - ${error.message}`)
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -217,14 +217,14 @@ export class ClineHandler implements ApiHandler {
|
||||
|
||||
// Fallback to generation endpoint if usage chunk not returned
|
||||
if (!didOutputUsage) {
|
||||
console.warn("Cline API did not return usage chunk, fetching from generation endpoint")
|
||||
Logger.warn("Cline API did not return usage chunk, fetching from generation endpoint")
|
||||
const apiStreamUsage = await this.getApiStreamUsage()
|
||||
if (apiStreamUsage) {
|
||||
yield apiStreamUsage
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cline API Error:", error)
|
||||
Logger.error("Cline API Error:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -260,7 +260,7 @@ export class ClineHandler implements ApiHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching cline generation details:", error)
|
||||
Logger.error("Error fetching cline generation details:", error)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ModelInfo } from "../../../shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -84,7 +85,7 @@ export class DifyHandler implements ApiHandler {
|
||||
this.apiKey = options.difyApiKey || ""
|
||||
this.baseUrl = options.difyBaseUrl || ""
|
||||
|
||||
console.log("[DIFY DEBUG] Constructor called with:", {
|
||||
Logger.log("[DIFY DEBUG] Constructor called with:", {
|
||||
hasApiKey: !!this.apiKey,
|
||||
baseUrl: this.baseUrl,
|
||||
})
|
||||
@@ -98,7 +99,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[]): ApiStream {
|
||||
console.log("[DIFY DEBUG] createMessage called with:", {
|
||||
Logger.log("[DIFY DEBUG] createMessage called with:", {
|
||||
systemPromptLength: systemPrompt?.length || 0,
|
||||
messagesCount: messages?.length || 0,
|
||||
})
|
||||
@@ -115,8 +116,8 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const fullUrl = `${this.baseUrl}/chat-messages`
|
||||
console.log("[DIFY DEBUG] Making request to:", fullUrl)
|
||||
console.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
|
||||
Logger.log("[DIFY DEBUG] Making request to:", fullUrl)
|
||||
Logger.log("[DIFY DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
@@ -129,21 +130,21 @@ export class DifyHandler implements ApiHandler {
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error("[DIFY DEBUG] Network error during fetch:", error)
|
||||
Logger.error("[DIFY DEBUG] Network error during fetch:", error)
|
||||
const cause = error.cause ? ` | Cause: ${error.cause}` : ""
|
||||
throw new Error(`Dify API network error: ${error.message}${cause}`)
|
||||
}
|
||||
|
||||
console.log("[DIFY DEBUG] Response status:", response.status)
|
||||
Logger.log("[DIFY DEBUG] Response status:", response.status)
|
||||
const headersObj: Record<string, string> = {}
|
||||
response.headers.forEach((value, key) => {
|
||||
headersObj[key] = value
|
||||
})
|
||||
console.log("[DIFY DEBUG] Response headers:", headersObj)
|
||||
Logger.log("[DIFY DEBUG] Response headers:", headersObj)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error("[DIFY DEBUG] Error response:", errorText)
|
||||
Logger.error("[DIFY DEBUG] Error response:", errorText)
|
||||
throw new Error(`Dify API error: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
@@ -159,14 +160,14 @@ export class DifyHandler implements ApiHandler {
|
||||
const processedEvents: string[] = []
|
||||
let lastEventTime = Date.now()
|
||||
|
||||
console.log("[DIFY DEBUG] Starting to read streaming response...")
|
||||
Logger.log("[DIFY DEBUG] Starting to read streaming response...")
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
console.log("[DIFY DEBUG] Stream ended naturally")
|
||||
console.log(
|
||||
Logger.log("[DIFY DEBUG] Stream ended naturally")
|
||||
Logger.log(
|
||||
"[DIFY DEBUG] Final state - hasYieldedContent:",
|
||||
hasYieldedContent,
|
||||
"fullText length:",
|
||||
@@ -178,7 +179,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
console.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
|
||||
Logger.log("[DIFY DEBUG] Raw chunk received:", JSON.stringify(chunk))
|
||||
|
||||
buffer += chunk
|
||||
const lines = buffer.split("\n")
|
||||
@@ -187,41 +188,41 @@ export class DifyHandler implements ApiHandler {
|
||||
buffer = lines.pop() || ""
|
||||
|
||||
for (const line of lines) {
|
||||
console.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
|
||||
Logger.log("[DIFY DEBUG] Processing line:", JSON.stringify(line))
|
||||
|
||||
if (line.startsWith("data: ")) {
|
||||
const data = line.slice(6).trim()
|
||||
console.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
|
||||
Logger.log("[DIFY DEBUG] Extracted data:", JSON.stringify(data))
|
||||
|
||||
if (data === "[DONE]") {
|
||||
console.log("[DIFY DEBUG] Received [DONE] signal")
|
||||
Logger.log("[DIFY DEBUG] Received [DONE] signal")
|
||||
break
|
||||
}
|
||||
|
||||
if (data === "") {
|
||||
console.log("[DIFY DEBUG] Empty data line, skipping")
|
||||
Logger.log("[DIFY DEBUG] Empty data line, skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
console.log("[DIFY DEBUG] Parsed JSON:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Parsed JSON:", parsed)
|
||||
processedEvents.push(parsed.event || "unknown")
|
||||
lastEventTime = Date.now()
|
||||
|
||||
// Capture conversation_id as soon as it's available
|
||||
if (parsed.conversation_id && !this.conversationId) {
|
||||
this.conversationId = parsed.conversation_id
|
||||
console.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
|
||||
Logger.log("[DIFY DEBUG] Captured conversation_id:", this.conversationId)
|
||||
}
|
||||
|
||||
// Handle different Dify event types based on actual Dify API
|
||||
if (parsed.event === "message") {
|
||||
console.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
|
||||
Logger.log("[DIFY DEBUG] Message event, answer:", parsed.answer)
|
||||
// Dify sends the full text in each "answer" chunk, so we replace.
|
||||
if (typeof parsed.answer === "string") {
|
||||
fullText = parsed.answer
|
||||
console.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
|
||||
Logger.log("[DIFY DEBUG] Updated fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -229,10 +230,10 @@ export class DifyHandler implements ApiHandler {
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} else if (parsed.event === "message_replace") {
|
||||
console.log("[DIFY DEBUG] Replace message event:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Replace message event:", parsed)
|
||||
if (parsed.answer) {
|
||||
fullText = parsed.answer // Replace instead of append
|
||||
console.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
|
||||
Logger.log("[DIFY DEBUG] Replaced fullText length:", fullText.length)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -240,7 +241,7 @@ export class DifyHandler implements ApiHandler {
|
||||
hasYieldedContent = true
|
||||
}
|
||||
} else if (parsed.event === "message_end") {
|
||||
console.log("[DIFY DEBUG] Message end event", parsed)
|
||||
Logger.log("[DIFY DEBUG] Message end event", parsed)
|
||||
// Message completed. Yield final text if we have any.
|
||||
if (fullText) {
|
||||
yield {
|
||||
@@ -260,19 +261,19 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
return // End of stream
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Error event:", parsed)
|
||||
Logger.error("[DIFY DEBUG] Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
} else if (parsed.event === "workflow_started" || parsed.event === "workflow_finished") {
|
||||
console.log("[DIFY DEBUG] Workflow event:", parsed.event)
|
||||
Logger.log("[DIFY DEBUG] Workflow event:", parsed.event)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "node_started" || parsed.event === "node_finished") {
|
||||
console.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
|
||||
Logger.log("[DIFY DEBUG] Node event:", parsed.event, parsed.data)
|
||||
// These are informational events, continue processing
|
||||
} else if (parsed.event === "ping") {
|
||||
console.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
|
||||
Logger.log("[DIFY DEBUG] Ping event received, keeping connection alive.")
|
||||
// Ping event, do nothing
|
||||
} else {
|
||||
console.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Unknown event type:", parsed.event, "Full object:", parsed)
|
||||
// Try to extract text from other possible fields
|
||||
if (parsed.text) {
|
||||
fullText += parsed.text
|
||||
@@ -299,17 +300,17 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
|
||||
Logger.warn("[DIFY DEBUG] Failed to parse JSON:", data, "Error:", e)
|
||||
}
|
||||
} else if (line.trim() !== "") {
|
||||
console.log(
|
||||
Logger.log(
|
||||
"[DIFY DEBUG] Non-data line (not starting with 'data:'), trying to parse as direct JSON:",
|
||||
JSON.stringify(line),
|
||||
)
|
||||
// Try to parse as direct JSON (fallback for non-SSE responses, though Dify uses SSE)
|
||||
try {
|
||||
const parsed = JSON.parse(line.trim())
|
||||
console.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
|
||||
Logger.log("[DIFY DEBUG] Parsed direct JSON:", parsed)
|
||||
processedEvents.push(parsed.event || "direct-json")
|
||||
|
||||
// Handle the same event types as above
|
||||
@@ -330,7 +331,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
return
|
||||
} else if (parsed.event === "error") {
|
||||
console.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
|
||||
Logger.error("[DIFY DEBUG] Direct JSON Error event:", parsed)
|
||||
throw new Error(`Dify API error: ${parsed.message || "Unknown error"}`)
|
||||
} else if (parsed.answer || parsed.text || parsed.content) {
|
||||
// Fallback for any content in direct JSON
|
||||
@@ -344,7 +345,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, continue
|
||||
console.log("[DIFY DEBUG] Line is not direct JSON, continuing")
|
||||
Logger.log("[DIFY DEBUG] Line is not direct JSON, continuing")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,11 +360,11 @@ export class DifyHandler implements ApiHandler {
|
||||
streamDuration: Date.now() - lastEventTime,
|
||||
conversationId: this.conversationId,
|
||||
}
|
||||
console.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
|
||||
Logger.error("[DIFY DEBUG] No content was yielded! Diagnostic info:", diagnosticInfo)
|
||||
|
||||
// If we have any accumulated text at all, yield it as a fallback
|
||||
if (fullText.trim()) {
|
||||
console.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
|
||||
Logger.log("[DIFY DEBUG] Yielding accumulated text as fallback:", fullText)
|
||||
yield {
|
||||
type: "text",
|
||||
text: fullText,
|
||||
@@ -380,7 +381,7 @@ export class DifyHandler implements ApiHandler {
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
console.log("[DIFY DEBUG] Stream reader released")
|
||||
Logger.log("[DIFY DEBUG] Stream reader released")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +400,7 @@ export class DifyHandler implements ApiHandler {
|
||||
|
||||
// Only prepend the system prompt if it's the very first message of a new conversation.
|
||||
if (!this.conversationId && systemPrompt) {
|
||||
console.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
|
||||
Logger.log("[DIFY DEBUG] Prepending system prompt for new conversation.")
|
||||
return `${systemPrompt}\n\n---\n\n${userQuery}`
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { GeminiModelId, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { RetriableError, withRetry } from "../retry"
|
||||
import { convertAnthropicMessageToGemini } from "../transform/gemini-format"
|
||||
@@ -332,7 +333,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
throughputTokensPerSec: throughputTokensPerSecSdk,
|
||||
})
|
||||
} else {
|
||||
console.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
|
||||
Logger.warn("GeminiHandler: ulid not available for telemetry in createMessage.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -408,7 +409,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
trace.cacheRead = { price: cacheReadsPrice, tokens: cacheReadTokens ?? 0, cost: cacheReadCost }
|
||||
}
|
||||
|
||||
// console.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
|
||||
// Logger.log(`[GeminiHandler] calculateCost -> ${totalCost}`, trace)
|
||||
return totalCost
|
||||
}
|
||||
|
||||
@@ -450,13 +451,13 @@ export class GeminiHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
if (response.totalTokens === undefined) {
|
||||
console.warn("Gemini token counting returned undefined, using fallback")
|
||||
Logger.warn("Gemini token counting returned undefined, using fallback")
|
||||
return this.estimateTokens(content)
|
||||
}
|
||||
|
||||
return response.totalTokens
|
||||
} catch (error) {
|
||||
console.warn("Gemini token counting failed, using fallback", error)
|
||||
Logger.warn("Gemini token counting failed, using fallback", error)
|
||||
return this.estimateTokens(content)
|
||||
}
|
||||
}
|
||||
@@ -475,7 +476,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
const jsonStr = JSON.stringify(block)
|
||||
return total + jsonStr.length
|
||||
} catch (e) {
|
||||
console.warn("Failed to stringify block for token estimation", e)
|
||||
Logger.warn("Failed to stringify block for token estimation", e)
|
||||
return total
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import OpenAI from "openai"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isAnthropicModelId } from "@/utils/model-utils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
@@ -70,7 +71,7 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
const data: LiteLlmModelInfoResponse = await response.json()
|
||||
return data
|
||||
} else {
|
||||
console.error("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
Logger.error("Failed to fetch LiteLLM model info:", response.statusText)
|
||||
// Try with Authorization header instead
|
||||
const retryResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
@@ -84,12 +85,12 @@ export async function fetchLiteLlmModelsInfo(baseUrl: string, apiKey: string): P
|
||||
const data: LiteLlmModelInfoResponse = await retryResponse.json()
|
||||
return data
|
||||
} else {
|
||||
console.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
Logger.error("Failed to fetch LiteLLM model info with Authorization header:", retryResponse.statusText)
|
||||
throw new Error(`Failed to fetch LiteLLM model info: ${retryResponse.statusText}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching LiteLLM model info:", error)
|
||||
Logger.error("Error fetching LiteLLM model info:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -169,7 +170,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error getting LiteLLM model cost info:", error)
|
||||
Logger.warn("Error getting LiteLLM model cost info:", error)
|
||||
}
|
||||
|
||||
// Fallback to zero costs if we can't get the information
|
||||
@@ -200,7 +201,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
|
||||
return totalCost
|
||||
} catch (error) {
|
||||
console.error("Error calculating spend:", error)
|
||||
Logger.error("Error calculating spend:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
OCI_HEADER_OPC_REQUEST_ID,
|
||||
} from "@/services/auth/oca/utils/constants"
|
||||
import { createOcaHeaders } from "@/services/auth/oca/utils/utils"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { OcaModelInfo } from "@/shared/api"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiFormat } from "@/shared/proto/index.cline"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, type CommonApiHandlerOptions } from ".."
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -134,11 +134,11 @@ export class OcaHandler implements ApiHandler {
|
||||
const data: { cost: number } = await response.json()
|
||||
return data.cost
|
||||
} else {
|
||||
console.error("Error calculating spend:", response.statusText)
|
||||
Logger.error("Error calculating spend:", response.statusText)
|
||||
return undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error calculating spend:", error)
|
||||
Logger.error("Error calculating spend:", error)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type ModelInfo, openAiModelInfoSaneDefaults } from "@shared/api"
|
||||
import { type Config, type Message, Ollama } from "ollama"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
@@ -93,7 +94,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (streamError: any) {
|
||||
console.error("Error processing Ollama stream:", streamError)
|
||||
Logger.error("Error processing Ollama stream:", streamError)
|
||||
throw new Error(`Ollama stream processing error: ${streamError.message || "Unknown error"}`)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -107,7 +108,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
const statusCode = error.status || error.statusCode
|
||||
const errorMessage = error.message || "Unknown error"
|
||||
|
||||
console.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
|
||||
Logger.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import * as os from "os"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
import { ModelInfo, OpenAiCodexModelId, openAiCodexDefaultModelId, openAiCodexModels } from "@shared/api"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import {
|
||||
ModelInfo,
|
||||
OpenAiCodexModelId,
|
||||
openAiCodexDefaultModelId,
|
||||
openAiCodexModels,
|
||||
} from "@shared/api"
|
||||
import * as os from "os"
|
||||
import { v7 as uuidv7 } from "uuid"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { openAiCodexOAuthManager } from "@/integrations/openai-codex/oauth"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { convertToOpenAIResponsesInput } from "../transform/openai-response-format"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
@@ -53,7 +48,9 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
private normalizeUsage(usage: any, model: { id: string; info: ModelInfo }): ApiStreamUsageChunk | undefined {
|
||||
if (!usage) return undefined
|
||||
if (!usage) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const inputDetails = usage.input_tokens_details ?? usage.prompt_tokens_details
|
||||
|
||||
@@ -90,11 +87,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
return out
|
||||
}
|
||||
|
||||
async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: ClineStorageMessage[],
|
||||
tools?: ChatCompletionTool[],
|
||||
): ApiStream {
|
||||
async *createMessage(systemPrompt: string, messages: ClineStorageMessage[], tools?: ChatCompletionTool[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
|
||||
// Reset state for this request
|
||||
@@ -104,9 +97,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
// Get access token from OAuth manager
|
||||
let accessToken = await openAiCodexOAuthManager.getAccessToken()
|
||||
if (!accessToken) {
|
||||
throw new Error(
|
||||
"Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow in settings.",
|
||||
)
|
||||
throw new Error("Not authenticated with OpenAI Codex. Please sign in using the OpenAI Codex OAuth flow in settings.")
|
||||
}
|
||||
|
||||
// Format conversation for Responses API
|
||||
@@ -179,11 +170,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
return body
|
||||
}
|
||||
|
||||
private async *executeRequest(
|
||||
requestBody: any,
|
||||
model: { id: string; info: ModelInfo },
|
||||
accessToken: string,
|
||||
): ApiStream {
|
||||
private async *executeRequest(requestBody: any, model: { id: string; info: ModelInfo }, accessToken: string): ApiStream {
|
||||
// Create AbortController for cancellation
|
||||
this.abortController = new AbortController()
|
||||
|
||||
@@ -237,11 +224,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async *makeCodexRequest(
|
||||
requestBody: any,
|
||||
model: { id: string; info: ModelInfo },
|
||||
accessToken: string,
|
||||
): ApiStream {
|
||||
private async *makeCodexRequest(requestBody: any, model: { id: string; info: ModelInfo }, accessToken: string): ApiStream {
|
||||
const url = `${CODEX_API_BASE_URL}/responses`
|
||||
|
||||
// Get ChatGPT account ID for organization subscriptions
|
||||
@@ -302,10 +285,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private async *handleStreamResponse(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
model: { id: string; info: ModelInfo },
|
||||
): ApiStream {
|
||||
private async *handleStreamResponse(body: ReadableStream<Uint8Array>, model: { id: string; info: ModelInfo }): ApiStream {
|
||||
const reader = body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
@@ -317,7 +297,9 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split("\n")
|
||||
@@ -380,10 +362,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
// Handle tool/function call deltas
|
||||
if (
|
||||
event?.type === "response.tool_call_arguments.delta" ||
|
||||
event?.type === "response.function_call_arguments.delta"
|
||||
) {
|
||||
if (event?.type === "response.tool_call_arguments.delta" || event?.type === "response.function_call_arguments.delta") {
|
||||
const callId = event.call_id || event.tool_call_id || event.id || this.pendingToolCallId
|
||||
const name = event.name || event.function_name || this.pendingToolCallName
|
||||
const args = event.delta || event.arguments
|
||||
@@ -484,10 +463,7 @@ export class OpenAiCodexHandler implements ApiHandler {
|
||||
getModel(): { id: OpenAiCodexModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
|
||||
const id =
|
||||
modelId && modelId in openAiCodexModels
|
||||
? (modelId as OpenAiCodexModelId)
|
||||
: openAiCodexDefaultModelId
|
||||
const id = modelId && modelId in openAiCodexModels ? (modelId as OpenAiCodexModelId) : openAiCodexDefaultModelId
|
||||
|
||||
const info: ModelInfo = openAiCodexModels[id]
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
import { calculateApiCostOpenAI } from "@utils/cost"
|
||||
import OpenAI from "openai"
|
||||
import type { ChatCompletionReasoningEffort, ChatCompletionTool } from "openai/resources/chat/completions"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { ApiFormat } from "@/shared/proto/cline/models"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { isGPT5ModelFamily } from "@/utils/model-utils"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
@@ -135,7 +135,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
try {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
} catch (error) {
|
||||
console.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
Logger.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,11 +316,11 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
chunk.response?.status === "incomplete" &&
|
||||
chunk.response?.incomplete_details?.reason === "max_output_tokens"
|
||||
) {
|
||||
console.log("Ran out of tokens")
|
||||
Logger.log("Ran out of tokens")
|
||||
if (chunk.response?.output_text?.length > 0) {
|
||||
console.log("Partial output:", chunk.response.output_text)
|
||||
Logger.log("Partial output:", chunk.response.output_text)
|
||||
} else {
|
||||
console.log("Ran out of tokens during reasoning")
|
||||
Logger.log("Ran out of tokens during reasoning")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch, getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { createOpenRouterStream } from "../transform/openrouter-stream"
|
||||
@@ -80,7 +81,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
// Check for error field directly on chunk
|
||||
if ("error" in chunk) {
|
||||
const error = chunk.error as OpenRouterErrorResponse["error"]
|
||||
console.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
|
||||
Logger.error(`OpenRouter API Error: ${error?.code} - ${error?.message}`)
|
||||
// Include metadata in the error message if available
|
||||
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
|
||||
throw new Error(`OpenRouter API Error ${error.code}: ${error.message}${metadataStr}`)
|
||||
@@ -95,7 +96,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
const choiceWithError = choice as any
|
||||
if (choiceWithError.error) {
|
||||
const error = choiceWithError.error
|
||||
console.error(
|
||||
Logger.error(
|
||||
`OpenRouter Mid-Stream Error: ${error?.code || "Unknown"} - ${error?.message || "Unknown error"}`,
|
||||
)
|
||||
// Format error details
|
||||
@@ -179,7 +180,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
try {
|
||||
const generationIterator = this.fetchGenerationDetails(this.lastGenerationId)
|
||||
const generation = (await generationIterator.next()).value
|
||||
// console.log("OpenRouter generation details:", generation)
|
||||
// Logger.log("OpenRouter generation details:", generation)
|
||||
return {
|
||||
type: "usage",
|
||||
cacheWriteTokens: 0,
|
||||
@@ -191,7 +192,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching OpenRouter generation details:", error)
|
||||
Logger.error("Error fetching OpenRouter generation details:", error)
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
@@ -199,7 +200,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
|
||||
@withRetry({ maxRetries: 4, baseDelay: 250, maxDelay: 1000, retryAllErrors: true })
|
||||
async *fetchGenerationDetails(genId: string) {
|
||||
// console.log("Fetching generation details for:", genId)
|
||||
// Logger.log("Fetching generation details for:", genId)
|
||||
try {
|
||||
const response = await axios.get(`https://openrouter.ai/api/v1/generation?id=${genId}`, {
|
||||
headers: {
|
||||
@@ -211,7 +212,7 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
yield response.data?.data
|
||||
} catch (error) {
|
||||
// ignore if fails
|
||||
console.error("Error fetching OpenRouter generation details:", error)
|
||||
Logger.error("Error fetching OpenRouter generation details:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -76,7 +77,7 @@ export class QwenCodeHandler implements ApiHandler {
|
||||
const credsStr = await fs.readFile(keyFile, "utf-8")
|
||||
return JSON.parse(credsStr)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
Logger.error(
|
||||
`Error reading or parsing credentials file at ${getQwenCachedCredentialPath(this.options.qwenCodeOauthPath)}`,
|
||||
)
|
||||
throw new Error(`Failed to load Qwen OAuth credentials: ${error}`)
|
||||
|
||||
@@ -12,6 +12,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -134,7 +135,7 @@ export class QwenHandler implements ApiHandler {
|
||||
try {
|
||||
yield* toolCallProcessor.processToolCallDeltas(delta.tool_calls)
|
||||
} catch (error) {
|
||||
console.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
Logger.error("Error processing tool call delta:", error, delta.tool_calls)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import JSON5 from "json5"
|
||||
import OpenAI from "openai"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { getAxiosSettings } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
@@ -144,7 +145,7 @@ namespace Bedrock {
|
||||
}
|
||||
|
||||
// Log unsupported content types for debugging
|
||||
console.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
Logger.warn(`Unsupported content type: ${(item as ContentItem).type}`)
|
||||
return null
|
||||
})
|
||||
.filter((item): item is BedrockContentBlock => item !== null)
|
||||
@@ -213,7 +214,7 @@ namespace Bedrock {
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to process image content:", error)
|
||||
Logger.error("Failed to process image content:", error)
|
||||
// Return a text content indicating the error instead of null
|
||||
// This ensures users are aware of the issue
|
||||
return {
|
||||
@@ -412,7 +413,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
url: destination.url || this.options.sapAiCoreBaseUrl!,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to create AI Core destination:", error)
|
||||
Logger.error("Failed to create AI Core destination:", error)
|
||||
throw new Error(`Unable to create AI Core destination: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
@@ -473,7 +474,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
})
|
||||
.filter((deployment: any) => deployment !== null)
|
||||
} catch (error) {
|
||||
console.error("Error fetching deployments:", error)
|
||||
Logger.error("Error fetching deployments:", error)
|
||||
throw new Error("Failed to fetch deployments")
|
||||
}
|
||||
}
|
||||
@@ -577,7 +578,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in SAP orchestration mode:", error)
|
||||
Logger.error("Error in SAP orchestration mode:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -596,7 +597,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
|
||||
if (!deploymentId) {
|
||||
// Fallback to runtime deployment id fetching for users who haven't opened the SAP provider UI
|
||||
console.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
|
||||
Logger.log(`No pre-configured deployment ID found for model ${model.id}, falling back to runtime fetching`)
|
||||
deploymentId = await this.getDeploymentForModel(model.id)
|
||||
}
|
||||
|
||||
@@ -800,8 +801,8 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error("Error status:", error.response.status)
|
||||
console.error("Error headers:", error.response.headers)
|
||||
Logger.error("Error status:", error.response.status)
|
||||
Logger.error("Error headers:", error.response.headers)
|
||||
|
||||
// Handle error data - need to read stream if responseType was 'stream'
|
||||
let errorMessage = "Unknown error"
|
||||
@@ -830,10 +831,10 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
} else if (typeof error.response.data === "object") {
|
||||
errorMessage = JSON.stringify(error.response.data, null, 2)
|
||||
}
|
||||
console.error("Error data:", errorMessage)
|
||||
Logger.error("Error data:", errorMessage)
|
||||
} catch (e) {
|
||||
console.error("Failed to read error data:", e)
|
||||
console.error("Raw error data:", error.response.data)
|
||||
Logger.error("Failed to read error data:", e)
|
||||
Logger.error("Raw error data:", error.response.data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,11 +847,11 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
throw new Error(`HTTP ${error.response.status}: ${errorMessage}`)
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
console.error("Error request:", error.request)
|
||||
Logger.error("Error request:", error.request)
|
||||
throw new Error("No response received from server")
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.error("Error message:", error.message)
|
||||
Logger.error("Error message:", error.message)
|
||||
throw new Error(`Error setting up request: ${error.message}`)
|
||||
}
|
||||
}
|
||||
@@ -898,13 +899,13 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON data:", error)
|
||||
Logger.error("Failed to parse JSON data:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming completion:", error)
|
||||
Logger.error("Error streaming completion:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -965,7 +966,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON data:", error)
|
||||
Logger.error("Failed to parse JSON data:", error)
|
||||
yield {
|
||||
type: "text",
|
||||
text: `[ERROR] Failed to parse response data: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -975,7 +976,7 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming completion:", error)
|
||||
Logger.error("Error streaming completion:", error)
|
||||
yield {
|
||||
type: "text",
|
||||
text: `[ERROR] Failed to process stream: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -1044,13 +1045,13 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse GPT JSON data:", error)
|
||||
Logger.error("Failed to parse GPT JSON data:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming GPT completion:", error)
|
||||
Logger.error("Error streaming GPT completion:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1109,13 +1110,13 @@ export class SapAiCoreHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse Gemini JSON data:", error)
|
||||
Logger.error("Failed to parse Gemini JSON data:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error streaming Gemini completion:", error)
|
||||
Logger.error("Error streaming Gemini completion:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import OpenAI from "openai"
|
||||
import type { ChatCompletionTool as OpenAITool } from "openai/resources/chat/completions"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { fetch } from "@/shared/net"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -125,11 +126,11 @@ export class VercelAIGatewayHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
if (!didOutputUsage) {
|
||||
console.warn("Vercel AI Gateway did not provide usage information in stream")
|
||||
Logger.warn("Vercel AI Gateway did not provide usage information in stream")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Vercel AI Gateway error details:", error)
|
||||
console.error("Error stack:", error.stack)
|
||||
Logger.error("Vercel AI Gateway error details:", error)
|
||||
Logger.error("Error stack:", error.stack)
|
||||
throw new Error(`Vercel AI Gateway error: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "@shared/vsCo
|
||||
import { calculateApiCostAnthropic } from "@utils/cost"
|
||||
import * as vscode from "vscode"
|
||||
import { ClineStorageMessage } from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { ApiHandler, CommonApiHandlerOptions, SingleCompletionHandler } from "../"
|
||||
import { withRetry } from "../retry"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
@@ -123,7 +124,7 @@ declare module "vscode" {
|
||||
* const systemPrompt = "You are a helpful assistant";
|
||||
* const messages = [{ role: "user", content: "Hello!" }];
|
||||
* for await (const chunk of handler.createMessage(systemPrompt, messages)) {
|
||||
* console.log(chunk);
|
||||
* Logger.log(chunk);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@@ -147,7 +148,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
this.client = null
|
||||
this.ensureCleanState()
|
||||
} catch (error) {
|
||||
console.error("Error during configuration change cleanup:", error)
|
||||
Logger.error("Error during configuration change cleanup:", error)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -279,7 +280,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
|
||||
private async getClient(): Promise<vscode.LanguageModelChat> {
|
||||
if (!this.client) {
|
||||
console.debug("Cline <Language Model API>: Getting client with options:", {
|
||||
Logger.debug("Cline <Language Model API>: Getting client with options:", {
|
||||
vsCodeLmModelSelector: this.options.vsCodeLmModelSelector,
|
||||
hasOptions: !!this.options,
|
||||
selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [],
|
||||
@@ -288,11 +289,11 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
try {
|
||||
// Use default empty selector if none provided to get all available models
|
||||
const selector = this.options?.vsCodeLmModelSelector || {}
|
||||
console.debug("Cline <Language Model API>: Creating client with selector:", selector)
|
||||
Logger.debug("Cline <Language Model API>: Creating client with selector:", selector)
|
||||
this.client = await this.createClient(selector)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error"
|
||||
console.error("Cline <Language Model API>: Client creation failed:", message)
|
||||
Logger.error("Cline <Language Model API>: Client creation failed:", message)
|
||||
throw new Error(`Cline <Language Model API>: Failed to create client: ${message}`)
|
||||
}
|
||||
}
|
||||
@@ -413,7 +414,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
if (chunk instanceof vscode.LanguageModelTextPart) {
|
||||
// Validate text part value
|
||||
if (typeof chunk.value !== "string") {
|
||||
console.warn("Cline <Language Model API>: Invalid text part value received:", chunk.value)
|
||||
Logger.warn("Cline <Language Model API>: Invalid text part value received:", chunk.value)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -426,18 +427,18 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
try {
|
||||
// Validate tool call parameters
|
||||
if (!chunk.name || typeof chunk.name !== "string") {
|
||||
console.warn("Cline <Language Model API>: Invalid tool name received:", chunk.name)
|
||||
Logger.warn("Cline <Language Model API>: Invalid tool name received:", chunk.name)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!chunk.callId || typeof chunk.callId !== "string") {
|
||||
console.warn("Cline <Language Model API>: Invalid tool callId received:", chunk.callId)
|
||||
Logger.warn("Cline <Language Model API>: Invalid tool callId received:", chunk.callId)
|
||||
continue
|
||||
}
|
||||
|
||||
// Ensure input is a valid object
|
||||
if (!chunk.input || typeof chunk.input !== "object") {
|
||||
console.warn("Cline <Language Model API>: Invalid tool input received:", chunk.input)
|
||||
Logger.warn("Cline <Language Model API>: Invalid tool input received:", chunk.input)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -453,7 +454,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
accumulatedText += toolCallText
|
||||
|
||||
// Log tool call for debugging
|
||||
console.debug("Cline <Language Model API>: Processing tool call:", {
|
||||
Logger.debug("Cline <Language Model API>: Processing tool call:", {
|
||||
name: chunk.name,
|
||||
callId: chunk.callId,
|
||||
inputSize: JSON.stringify(chunk.input).length,
|
||||
@@ -464,10 +465,10 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
text: toolCallText,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Cline <Language Model API>: Failed to process tool call:", error)
|
||||
Logger.error("Cline <Language Model API>: Failed to process tool call:", error)
|
||||
}
|
||||
} else {
|
||||
console.warn("Cline <Language Model API>: Unknown chunk type received:", chunk)
|
||||
Logger.warn("Cline <Language Model API>: Unknown chunk type received:", chunk)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,7 +490,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
console.error("Cline <Language Model API>: Stream error details:", {
|
||||
Logger.error("Cline <Language Model API>: Stream error details:", {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name,
|
||||
@@ -500,12 +501,12 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
} else if (typeof error === "object" && error !== null) {
|
||||
// Handle error-like objects
|
||||
const errorDetails = JSON.stringify(error, null, 2)
|
||||
console.error("Cline <Language Model API>: Stream error object:", errorDetails)
|
||||
Logger.error("Cline <Language Model API>: Stream error object:", errorDetails)
|
||||
throw new Error(`Cline <Language Model API>: Response stream error: ${errorDetails}`)
|
||||
} else {
|
||||
// Fallback for unknown error types
|
||||
const errorMessage = String(error)
|
||||
console.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
|
||||
Logger.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
|
||||
throw new Error(`Cline <Language Model API>: Response stream error: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
@@ -526,7 +527,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
// Log any missing properties for debugging
|
||||
for (const [prop, value] of Object.entries(requiredProps)) {
|
||||
if (!value && value !== 0) {
|
||||
console.warn(`Cline <Language Model API>: Client missing ${prop} property`)
|
||||
Logger.warn(`Cline <Language Model API>: Client missing ${prop} property`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,7 +558,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
|
||||
? stringifyVsCodeLmModelSelector(this.options.vsCodeLmModelSelector)
|
||||
: "vscode-lm"
|
||||
|
||||
console.debug("Cline <Language Model API>: No client available, using fallback model info")
|
||||
Logger.debug("Cline <Language Model API>: No client available, using fallback model info")
|
||||
|
||||
return {
|
||||
id: fallbackId,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
interface RetryOptions {
|
||||
maxRetries?: number
|
||||
baseDelay?: number
|
||||
@@ -72,7 +74,7 @@ export function withRetry(options: RetryOptions = {}) {
|
||||
try {
|
||||
await handlerInstance.options.onRetryAttempt(attempt + 1, maxRetries, delay, error)
|
||||
} catch (e) {
|
||||
console.error("Error in onRetryAttempt callback:", e)
|
||||
Logger.error("Error in onRetryAttempt callback:", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
const o1SystemPrompt = (systemPrompt: string) => `
|
||||
# System Prompt
|
||||
@@ -317,7 +318,7 @@ function parseToolCall(toolName: string, content: string): ToolCall | null {
|
||||
|
||||
// Validate required parameters
|
||||
if (!validateToolInput(toolName, tool_input)) {
|
||||
console.error(`Invalid tool call for ${toolName}:`, content)
|
||||
Logger.error(`Invalid tool call for ${toolName}:`, content)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -358,8 +359,8 @@ function validateToolInput(toolName: string, tool_input: Record<string, string>)
|
||||
// </write_to_file>`;
|
||||
//
|
||||
// const { normalText, toolCalls } = parseAIResponse(aiResponse);
|
||||
// console.log(normalText);
|
||||
// console.log(toolCalls);
|
||||
// Logger.log(normalText);
|
||||
// Logger.log(toolCalls);
|
||||
|
||||
// Convert OpenAI response to Anthropic format
|
||||
export function convertO1ResponseToAnthropicMessage(
|
||||
@@ -432,4 +433,4 @@ export function convertO1ResponseToAnthropicMessage(
|
||||
// usage: { prompt_tokens: 50, completion_tokens: 100 }
|
||||
// };
|
||||
// const anthropicMessage = convertO1ResponseToAnthropicMessage(openAICompletion);
|
||||
// console.log(anthropicMessage);
|
||||
// Logger.log(anthropicMessage);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ClineTextContentBlock,
|
||||
ClineUserToolResultContentBlock,
|
||||
} from "@/shared/messages/content"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
// OpenAI API has a maximum tool call ID length of 40 characters
|
||||
const MAX_TOOL_CALL_ID_LENGTH = 40
|
||||
@@ -424,7 +425,7 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
|
||||
try {
|
||||
parsedInput = JSON.parse(toolCall.function?.arguments || "{}")
|
||||
} catch (error) {
|
||||
console.error("Failed to parse tool arguments:", error)
|
||||
Logger.error("Failed to parse tool arguments:", error)
|
||||
}
|
||||
return {
|
||||
type: "tool_use",
|
||||
@@ -439,7 +440,7 @@ export function convertToAnthropicMessage(completion: OpenAI.Chat.Completions.Ch
|
||||
return anthropicMessage
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error converting OpenAI message to Anthropic format:", error)
|
||||
Logger.error("Error converting OpenAI message to Anthropic format:", error)
|
||||
}
|
||||
|
||||
return anthropicMessage
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
ChatCompletionToolChoiceOption,
|
||||
ChatCompletionTool as OpenAITool,
|
||||
} from "openai/resources/chat/completions"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import type { ApiStreamToolCallsChunk } from "./stream"
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import * as vscode from "vscode"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
/**
|
||||
* Safely converts a value into a plain object.
|
||||
@@ -23,7 +24,7 @@ export function asObjectSafe(value: any): object {
|
||||
|
||||
return {}
|
||||
} catch (error) {
|
||||
console.warn("Cline <Language Model API>: Failed to parse object:", error)
|
||||
Logger.warn("Cline <Language Model API>: Failed to parse object:", error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import OpenAI from "openai"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { ModelInfo } from "@/shared/api"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
|
||||
// Type that represents the OpenAI ResponseStream with its private properties
|
||||
// The #private property issue can be resolved by using the AsyncIterable interface
|
||||
@@ -143,11 +143,11 @@ export async function* handleResponsesApiStreamResponse(
|
||||
chunk.response?.status === "incomplete" &&
|
||||
chunk.response?.incomplete_details?.reason === "max_output_tokens"
|
||||
) {
|
||||
console.log("Ran out of tokens")
|
||||
Logger.log("Ran out of tokens")
|
||||
if (chunk.response?.output_text?.length > 0) {
|
||||
console.log("Partial output:", chunk.response.output_text)
|
||||
Logger.log("Partial output:", chunk.response.output_text)
|
||||
} else {
|
||||
console.log("Ran out of tokens during reasoning")
|
||||
Logger.log("Ran out of tokens during reasoning")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { describe, it } from "mocha"
|
||||
import { constructNewFileContent as cnfc } from "./diff"
|
||||
|
||||
async function cnfc2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
|
||||
return cnfc(diffContent, originalContent, isFinal, "v2")
|
||||
const result = await cnfc(diffContent, originalContent, isFinal, "v2")
|
||||
return result.newContent
|
||||
}
|
||||
|
||||
describe("constructNewFileContent", () => {
|
||||
@@ -217,13 +218,13 @@ replaced
|
||||
} else {
|
||||
const result1 = await cnfc(diff, original, isFinal ?? true)
|
||||
const result2 = await cnfc2(diff, original, isFinal ?? true)
|
||||
const _equal = result1 === result2
|
||||
const _equal2 = result1 === expected
|
||||
const _equal = result1.newContent === result2
|
||||
const _equal2 = result1.newContent === expected
|
||||
// Verify both implementations produce same result
|
||||
expect(result1).to.equal(result2)
|
||||
expect(result1.newContent).to.equal(result2)
|
||||
|
||||
// Verify result matches expected
|
||||
expect(result1).to.equal(expected)
|
||||
expect(result1.newContent).to.equal(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -264,7 +265,7 @@ replaced`
|
||||
// Should still work and replace line2 with "replaced"
|
||||
const expected = "line1\nreplaced\nline3"
|
||||
|
||||
expect(result1).to.equal(expected)
|
||||
expect(result1.newContent).to.equal(expected)
|
||||
})
|
||||
|
||||
it("should handle missing final REPLACE marker with multiple lines of replacement", async () => {
|
||||
@@ -281,7 +282,7 @@ replaced`
|
||||
const result1 = await cnfc(diff, original, true) // isFinal = true
|
||||
const expected = "function test() {\n\tconst a = 42;\n\tconsole.log('updated');\n\treturn a;\n}"
|
||||
|
||||
expect(result1).to.equal(expected)
|
||||
expect(result1.newContent).to.equal(expected)
|
||||
})
|
||||
|
||||
// it("should NOT process incomplete replacement when isFinal is false", async () => {
|
||||
@@ -319,7 +320,7 @@ new second
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const expectedResult = "first\nnew second\nthird\nnew fourth\n"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
it("should handle multiple out-of-order replacements", async () => {
|
||||
@@ -342,7 +343,7 @@ fifth
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const expectedResult = "one\nsecond\nthree\nfourth\nfifth\n"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
it("should handle out-of-order replacements with indentation", async () => {
|
||||
@@ -360,7 +361,7 @@ fifth
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const expectedResult = "function test() {\n\tconst a = 10;\n\tconst b = 2;\n\tconst c = 30;\n\n}"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
it("should handle out-of-order replacements with empty lines", async () => {
|
||||
@@ -380,6 +381,6 @@ new body content
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const expectedResult = "header\nnew body content\nnew footer\n"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,17 @@ const SEARCH_BLOCK_START = "------- SEARCH"
|
||||
const SEARCH_BLOCK_END = "======="
|
||||
const REPLACE_BLOCK_END = "+++++++ REPLACE"
|
||||
|
||||
/**
|
||||
* Converts a character index in a string to a 1-based line number.
|
||||
* @param content - The full content string
|
||||
* @param charIndex - The character index in the content
|
||||
* @returns The 1-based line number where charIndex falls
|
||||
*/
|
||||
export function getLineNumberFromCharIndex(content: string, charIndex: number): number {
|
||||
if (charIndex <= 0) return 1
|
||||
return content.substring(0, charIndex).split("\n").length
|
||||
}
|
||||
|
||||
const SEARCH_BLOCK_CHAR = "-"
|
||||
const REPLACE_BLOCK_CHAR = "+"
|
||||
const LEGACY_SEARCH_BLOCK_CHAR = "<"
|
||||
@@ -236,7 +247,7 @@ export async function constructNewFileContent(
|
||||
originalContent: string,
|
||||
isFinal: boolean,
|
||||
version: "v1" | "v2" = "v1",
|
||||
): Promise<string> {
|
||||
): Promise<{ newContent: string; matchIndices: number[] }> {
|
||||
const constructor = constructNewFileContentVersionMapping[version]
|
||||
if (!constructor) {
|
||||
throw new Error(`Invalid version '${version}' for file content constructor`)
|
||||
@@ -246,13 +257,17 @@ export async function constructNewFileContent(
|
||||
|
||||
const constructNewFileContentVersionMapping: Record<
|
||||
string,
|
||||
(diffContent: string, originalContent: string, isFinal: boolean) => Promise<string>
|
||||
(diffContent: string, originalContent: string, isFinal: boolean) => Promise<{ newContent: string; matchIndices: number[] }>
|
||||
> = {
|
||||
v1: constructNewFileContentV1,
|
||||
v2: constructNewFileContentV2,
|
||||
} as const
|
||||
|
||||
async function constructNewFileContentV1(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
|
||||
async function constructNewFileContentV1(
|
||||
diffContent: string,
|
||||
originalContent: string,
|
||||
isFinal: boolean,
|
||||
): Promise<{ newContent: string; matchIndices: number[] }> {
|
||||
let result = ""
|
||||
let lastProcessedIndex = 0
|
||||
|
||||
@@ -470,7 +485,9 @@ async function constructNewFileContentV1(diffContent: string, originalContent: s
|
||||
result += originalContent.slice(currentPos)
|
||||
}
|
||||
|
||||
return result
|
||||
// Return all match indices from the replacements
|
||||
// This is used to determine the line numbers for each SEARCH/REPLACE block in the UI
|
||||
return { newContent: result, matchIndices: replacements.map((r) => r.start) }
|
||||
}
|
||||
|
||||
enum ProcessingState {
|
||||
@@ -566,7 +583,7 @@ class NewFileContentConstructor {
|
||||
this.internalProcessLine(line, true, this.pendingNonStandardLines.length)
|
||||
}
|
||||
|
||||
public getResult() {
|
||||
public getResult(): { newContent: string; matchIndices: number[] } {
|
||||
// If this is the final chunk, append any remaining original content
|
||||
if (this.isFinal && this.lastProcessedIndex < this.originalContent.length) {
|
||||
this.result += this.originalContent.slice(this.lastProcessedIndex)
|
||||
@@ -574,7 +591,10 @@ class NewFileContentConstructor {
|
||||
if (this.isFinal && this.state !== ProcessingState.Idle) {
|
||||
throw new Error("File processing incomplete - SEARCH/REPLACE operations still active during finalization")
|
||||
}
|
||||
return this.result
|
||||
// Note: V2 implementation doesn't currently track match indices
|
||||
// For now, return empty array. If V2 becomes the default and we need line numbers,
|
||||
// we should add state to track all match indices.
|
||||
return { newContent: this.result, matchIndices: [] }
|
||||
}
|
||||
|
||||
private internalProcessLine(
|
||||
@@ -800,7 +820,11 @@ class NewFileContentConstructor {
|
||||
}
|
||||
}
|
||||
|
||||
export async function constructNewFileContentV2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
|
||||
export async function constructNewFileContentV2(
|
||||
diffContent: string,
|
||||
originalContent: string,
|
||||
isFinal: boolean,
|
||||
): Promise<{ newContent: string; matchIndices: number[] }> {
|
||||
const newFileContentConstructor = new NewFileContentConstructor(originalContent, isFinal)
|
||||
|
||||
const lines = diffContent.split("\n")
|
||||
|
||||
@@ -3,7 +3,8 @@ import { describe, it } from "mocha"
|
||||
import { constructNewFileContent as cnfc } from "./diff"
|
||||
|
||||
async function cnfc2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
|
||||
return cnfc(diffContent, originalContent, isFinal, "v2")
|
||||
const result = await cnfc(diffContent, originalContent, isFinal, "v2")
|
||||
return result.newContent
|
||||
}
|
||||
|
||||
describe("Diff Format Edge Cases", () => {
|
||||
@@ -18,7 +19,7 @@ new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nnew content\nafter"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -33,7 +34,7 @@ new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nnew content\nafter"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -48,7 +49,7 @@ new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nnew content\nafter"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -62,7 +63,7 @@ new content
|
||||
+++++++ REPLACE`
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
expect(result1).to.equal("before\nnew content\nafter")
|
||||
expect(result1.newContent).to.equal("before\nnew content\nafter")
|
||||
expect(result2).to.equal("before\nnew content\nafter")
|
||||
})
|
||||
|
||||
@@ -77,7 +78,7 @@ new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nnew content\nafter"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -92,7 +93,7 @@ new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nnew content\nafter"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -112,7 +113,7 @@ second new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nfirst new content\nafter\nsecond new content\nend"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
|
||||
@@ -132,7 +133,7 @@ second new content
|
||||
const result1 = await cnfc(diff, original, isFinal)
|
||||
const result2 = await cnfc2(diff, original, isFinal)
|
||||
const expectedResult = "before\nfirst new content\nafter\nsecond new content\nend"
|
||||
expect(result1).to.equal(expectedResult)
|
||||
expect(result1.newContent).to.equal(expectedResult)
|
||||
expect(result2).to.equal(expectedResult)
|
||||
})
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user