@caretive/caret-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmrc.tmp +2 -0
- package/README.md +72 -0
- package/cmd/cline/main.go +348 -0
- package/cmd/cline-host/main.go +71 -0
- package/e2e/default_update_test.go +154 -0
- package/e2e/helpers_test.go +378 -0
- package/e2e/main_test.go +47 -0
- package/e2e/mixed_stress_test.go +120 -0
- package/e2e/sqlite_helper.go +161 -0
- package/e2e/start_list_test.go +178 -0
- package/go.mod +64 -0
- package/go.sum +162 -0
- package/man/cline.1 +331 -0
- package/man/cline.1.md +332 -0
- package/package.json +54 -0
- package/pkg/cli/auth/auth_cline_provider.go +285 -0
- package/pkg/cli/auth/auth_menu.go +323 -0
- package/pkg/cli/auth/auth_subscription.go +130 -0
- package/pkg/cli/auth/byo_quick_setup.go +247 -0
- package/pkg/cli/auth/models_cline.go +141 -0
- package/pkg/cli/auth/models_list_fetch.go +156 -0
- package/pkg/cli/auth/models_list_static.go +69 -0
- package/pkg/cli/auth/providers_byo.go +184 -0
- package/pkg/cli/auth/providers_list.go +517 -0
- package/pkg/cli/auth/update_api_configurations.go +647 -0
- package/pkg/cli/auth/wizard_byo.go +764 -0
- package/pkg/cli/auth/wizard_byo_bedrock.go +193 -0
- package/pkg/cli/auth/wizard_byo_oca.go +366 -0
- package/pkg/cli/auth.go +43 -0
- package/pkg/cli/clerror/cline_error.go +187 -0
- package/pkg/cli/config/manager.go +208 -0
- package/pkg/cli/config/settings_renderer.go +198 -0
- package/pkg/cli/config.go +152 -0
- package/pkg/cli/display/ansi.go +27 -0
- package/pkg/cli/display/banner.go +211 -0
- package/pkg/cli/display/deduplicator.go +95 -0
- package/pkg/cli/display/markdown_renderer.go +139 -0
- package/pkg/cli/display/renderer.go +304 -0
- package/pkg/cli/display/segment_streamer.go +212 -0
- package/pkg/cli/display/streaming.go +134 -0
- package/pkg/cli/display/system_renderer.go +269 -0
- package/pkg/cli/display/tool_renderer.go +455 -0
- package/pkg/cli/display/tool_result_parser.go +371 -0
- package/pkg/cli/display/typewriter.go +210 -0
- package/pkg/cli/doctor.go +65 -0
- package/pkg/cli/global/cline-clients.go +501 -0
- package/pkg/cli/global/global.go +113 -0
- package/pkg/cli/global/registry.go +304 -0
- package/pkg/cli/handlers/ask_handlers.go +339 -0
- package/pkg/cli/handlers/handler.go +130 -0
- package/pkg/cli/handlers/say_handlers.go +521 -0
- package/pkg/cli/instances.go +506 -0
- package/pkg/cli/logs.go +382 -0
- package/pkg/cli/output/coordinator.go +167 -0
- package/pkg/cli/output/input_model.go +497 -0
- package/pkg/cli/sqlite/locks.go +366 -0
- package/pkg/cli/task/history_handler.go +72 -0
- package/pkg/cli/task/input_handler.go +577 -0
- package/pkg/cli/task/manager.go +1283 -0
- package/pkg/cli/task/settings_parser.go +754 -0
- package/pkg/cli/task/stream_coordinator.go +60 -0
- package/pkg/cli/task.go +675 -0
- package/pkg/cli/terminal/keyboard.go +695 -0
- package/pkg/cli/tui/HELP_WANTED.md +1 -0
- package/pkg/cli/types/history.go +17 -0
- package/pkg/cli/types/messages.go +329 -0
- package/pkg/cli/types/state.go +59 -0
- package/pkg/cli/updater/updater.go +409 -0
- package/pkg/cli/version.go +43 -0
- package/pkg/common/constants.go +6 -0
- package/pkg/common/schema.go +54 -0
- package/pkg/common/types.go +54 -0
- package/pkg/common/utils.go +185 -0
- package/pkg/generated/field_overrides.go +39 -0
- package/pkg/generated/providers.go +1584 -0
- package/pkg/hostbridge/diff.go +351 -0
- package/pkg/hostbridge/disabled/watch.go +39 -0
- package/pkg/hostbridge/disabled/window.go +63 -0
- package/pkg/hostbridge/disabled/workspace.go +66 -0
- package/pkg/hostbridge/env.go +166 -0
- package/pkg/hostbridge/grpc_server.go +113 -0
- package/pkg/hostbridge/simple.go +43 -0
- package/pkg/hostbridge/simple_workspace.go +85 -0
- package/pkg/hostbridge/window.go +129 -0
- package/scripts/publish-caret-cli.sh +39 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
"github.com/cline/cli/pkg/cli/global"
|
|
10
|
+
"github.com/cline/grpc-go/cline"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// AuthStatusListener manages subscription to auth status updates
|
|
14
|
+
type AuthStatusListener struct {
|
|
15
|
+
stream cline.AccountService_SubscribeToAuthStatusUpdateClient
|
|
16
|
+
updatesCh chan *cline.AuthState
|
|
17
|
+
errCh chan error
|
|
18
|
+
ctx context.Context
|
|
19
|
+
cancel context.CancelFunc
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// NewAuthStatusListener creates a new auth status listener
|
|
23
|
+
func NewAuthStatusListener(parentCtx context.Context) (*AuthStatusListener, error) {
|
|
24
|
+
client, err := global.GetDefaultClient(parentCtx)
|
|
25
|
+
if err != nil {
|
|
26
|
+
return nil, fmt.Errorf("failed to get client: %w", err)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create cancellable context
|
|
30
|
+
ctx, cancel := context.WithCancel(parentCtx)
|
|
31
|
+
|
|
32
|
+
// Subscribe to auth status updates
|
|
33
|
+
stream, err := client.Account.SubscribeToAuthStatusUpdate(ctx, &cline.EmptyRequest{})
|
|
34
|
+
if err != nil {
|
|
35
|
+
cancel()
|
|
36
|
+
return nil, fmt.Errorf("failed to subscribe to auth updates: %w", err)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return &AuthStatusListener{
|
|
40
|
+
stream: stream,
|
|
41
|
+
updatesCh: make(chan *cline.AuthState, 10),
|
|
42
|
+
errCh: make(chan error, 1),
|
|
43
|
+
ctx: ctx,
|
|
44
|
+
cancel: cancel,
|
|
45
|
+
}, nil
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Start begins listening to the auth status update stream
|
|
49
|
+
func (l *AuthStatusListener) Start() error {
|
|
50
|
+
verboseLog("Starting auth status listener...")
|
|
51
|
+
|
|
52
|
+
go l.readStream()
|
|
53
|
+
|
|
54
|
+
return nil
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// readStream reads from the gRPC stream and forwards messages to channels
|
|
58
|
+
func (l *AuthStatusListener) readStream() {
|
|
59
|
+
defer close(l.updatesCh)
|
|
60
|
+
defer close(l.errCh)
|
|
61
|
+
|
|
62
|
+
for {
|
|
63
|
+
select {
|
|
64
|
+
case <-l.ctx.Done():
|
|
65
|
+
verboseLog("Auth listener context cancelled")
|
|
66
|
+
return
|
|
67
|
+
default:
|
|
68
|
+
state, err := l.stream.Recv()
|
|
69
|
+
if err != nil {
|
|
70
|
+
if err == io.EOF {
|
|
71
|
+
verboseLog("Auth status stream closed")
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
verboseLog("Error reading from auth status stream: %v", err)
|
|
75
|
+
select {
|
|
76
|
+
case l.errCh <- err:
|
|
77
|
+
case <-l.ctx.Done():
|
|
78
|
+
}
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
verboseLog("Received auth state update: user=%v", state.User != nil)
|
|
83
|
+
|
|
84
|
+
select {
|
|
85
|
+
case l.updatesCh <- state:
|
|
86
|
+
case <-l.ctx.Done():
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// WaitForAuthentication blocks until authentication succeeds or timeout occurs
|
|
94
|
+
func (l *AuthStatusListener) WaitForAuthentication(timeout time.Duration) error {
|
|
95
|
+
verboseLog("Waiting for authentication (timeout: %v)...", timeout)
|
|
96
|
+
|
|
97
|
+
timer := time.NewTimer(timeout)
|
|
98
|
+
defer timer.Stop()
|
|
99
|
+
|
|
100
|
+
for {
|
|
101
|
+
select {
|
|
102
|
+
case <-timer.C:
|
|
103
|
+
return fmt.Errorf("authentication timeout after %v - please try again", timeout)
|
|
104
|
+
|
|
105
|
+
case <-l.ctx.Done():
|
|
106
|
+
return fmt.Errorf("authentication cancelled")
|
|
107
|
+
|
|
108
|
+
case err := <-l.errCh:
|
|
109
|
+
return fmt.Errorf("authentication stream error: %w", err)
|
|
110
|
+
|
|
111
|
+
case state := <-l.updatesCh:
|
|
112
|
+
if isAuthenticated(state) {
|
|
113
|
+
verboseLog("Authentication successful!")
|
|
114
|
+
return nil
|
|
115
|
+
}
|
|
116
|
+
verboseLog("Received auth update but not authenticated yet...")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Stop closes the stream and cleans up resources
|
|
122
|
+
func (l *AuthStatusListener) Stop() {
|
|
123
|
+
verboseLog("Stopping auth status listener...")
|
|
124
|
+
l.cancel()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// isAuthenticated checks if AuthState indicates successful authentication
|
|
128
|
+
func isAuthenticated(state *cline.AuthState) bool {
|
|
129
|
+
return state != nil && state.User != nil
|
|
130
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/cline/cli/pkg/cli/global"
|
|
9
|
+
"github.com/cline/cli/pkg/cli/task"
|
|
10
|
+
"github.com/cline/grpc-go/cline"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// Package-level variables for command-line flags
|
|
14
|
+
var (
|
|
15
|
+
QuickProvider string // Provider ID (e.g., "openai", "anthropic")
|
|
16
|
+
QuickAPIKey string // API key for the provider
|
|
17
|
+
QuickModelID string // Model ID to configure
|
|
18
|
+
QuickBaseURL string // Base URL (optional, for openai compatible only)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// QuickSetupFromFlags performs quick setup using command-line flags
|
|
22
|
+
// Returns error if validation fails or configuration cannot be applied
|
|
23
|
+
func QuickSetupFromFlags(ctx context.Context, provider, apiKey, modelID, baseURL string) error {
|
|
24
|
+
// Validate all input parameters
|
|
25
|
+
providerEnum, err := validateQuickSetupInputs(provider, apiKey, modelID, baseURL)
|
|
26
|
+
if err != nil {
|
|
27
|
+
return err
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Create task manager for state operations
|
|
31
|
+
manager, err := createTaskManager(ctx)
|
|
32
|
+
if err != nil {
|
|
33
|
+
return fmt.Errorf("failed to create task manager: %w", err)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validate and fetch model information if needed
|
|
37
|
+
finalModelID, modelInfo, err := validateAndFetchModel(ctx, manager, providerEnum, modelID, apiKey)
|
|
38
|
+
if err != nil {
|
|
39
|
+
return fmt.Errorf("model validation failed: %w", err)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// For Ollama, baseURL is stored in the API key field
|
|
43
|
+
finalAPIKey := apiKey
|
|
44
|
+
finalBaseURL := baseURL
|
|
45
|
+
if providerEnum == cline.ApiProvider_OLLAMA {
|
|
46
|
+
if baseURL != "" {
|
|
47
|
+
finalAPIKey = baseURL
|
|
48
|
+
finalBaseURL = ""
|
|
49
|
+
} else if apiKey != "" {
|
|
50
|
+
// User provided API key for Ollama - treat it as baseURL
|
|
51
|
+
finalAPIKey = apiKey
|
|
52
|
+
finalBaseURL = ""
|
|
53
|
+
} else {
|
|
54
|
+
// Use default Ollama baseURL
|
|
55
|
+
finalAPIKey = "http://localhost:11434"
|
|
56
|
+
finalBaseURL = ""
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Configure the provider using existing AddProviderPartial function
|
|
61
|
+
if err := AddProviderPartial(ctx, manager, providerEnum, finalModelID, finalAPIKey, finalBaseURL, modelInfo); err != nil {
|
|
62
|
+
return fmt.Errorf("failed to configure provider: %w", err)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Set the provider as active for both Plan and Act modes
|
|
66
|
+
if err := UpdateProviderPartial(ctx, manager, providerEnum, ProviderUpdatesPartial{}, true); err != nil {
|
|
67
|
+
return fmt.Errorf("failed to set provider as active: %w", err)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Mark welcome view as completed
|
|
71
|
+
if err := markWelcomeViewCompleted(ctx, manager); err != nil {
|
|
72
|
+
// Non-fatal error, just log it
|
|
73
|
+
if global.Config.Verbose {
|
|
74
|
+
fmt.Printf("[DEBUG] Warning: failed to mark welcome view as completed: %v\n", err)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Flush pending state changes to disk immediately
|
|
79
|
+
// This ensures all configuration changes are persisted before the instance terminates
|
|
80
|
+
if _, err := manager.GetClient().State.FlushPendingState(ctx, &cline.EmptyRequest{}); err != nil {
|
|
81
|
+
return fmt.Errorf("failed to flush pending state: %w", err)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Success message
|
|
85
|
+
fmt.Printf("\n✓ Successfully configured %s provider\n", GetProviderDisplayName(providerEnum))
|
|
86
|
+
fmt.Printf(" Model: %s\n", finalModelID)
|
|
87
|
+
if providerEnum == cline.ApiProvider_OLLAMA {
|
|
88
|
+
fmt.Printf(" Base URL: %s\n", finalAPIKey)
|
|
89
|
+
} else {
|
|
90
|
+
fmt.Println(" API Key: Configured")
|
|
91
|
+
}
|
|
92
|
+
if finalBaseURL != "" {
|
|
93
|
+
fmt.Printf(" Custom Base URL: %s\n", finalBaseURL)
|
|
94
|
+
}
|
|
95
|
+
fmt.Println("\nYou can now use Cline with this provider.")
|
|
96
|
+
fmt.Println("Run 'cline start' to begin a new task.")
|
|
97
|
+
|
|
98
|
+
return nil
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// validateQuickSetupInputs validates all input parameters for quick setup
|
|
102
|
+
// Returns the validated provider enum or an error if validation fails
|
|
103
|
+
func validateQuickSetupInputs(provider, apiKey, modelID, baseURL string) (cline.ApiProvider, error) {
|
|
104
|
+
// Validate required parameters
|
|
105
|
+
if provider == "" {
|
|
106
|
+
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("provider is required. Use --provider or -p flag")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if strings.TrimSpace(apiKey) == "" && provider != "ollama" {
|
|
110
|
+
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("API key is required for %s provider. Use --apikey or -k flag", provider)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if strings.TrimSpace(modelID) == "" {
|
|
114
|
+
return cline.ApiProvider_ANTHROPIC, fmt.Errorf("model ID is required. Use --modelid or -m flag")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Validate and map provider string to enum
|
|
118
|
+
providerEnum, err := validateQuickSetupProvider(provider)
|
|
119
|
+
if err != nil {
|
|
120
|
+
return cline.ApiProvider_ANTHROPIC, err
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Validate that baseURL is only provided for OpenAI-compatible providers
|
|
124
|
+
if err := validateBaseURL(baseURL, providerEnum); err != nil {
|
|
125
|
+
return cline.ApiProvider_ANTHROPIC, err
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return providerEnum, nil
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// validateBaseURL checks if the user's input includes a baseURL for a provider other than OpenAI (compatible)
|
|
132
|
+
// Returns error if baseURL is provided for unsupported providers
|
|
133
|
+
func validateBaseURL(baseURL string, providerEnum cline.ApiProvider) error {
|
|
134
|
+
if providerEnum != cline.ApiProvider_OPENAI {
|
|
135
|
+
if baseURL != "" {
|
|
136
|
+
return fmt.Errorf("base URL is only supported for OpenAI and OpenAI-compatible providers")
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return nil
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
// validateQuickSetupProvider validates the provider ID and returns the enum value
|
|
144
|
+
// Returns error if provider is invalid or not supported for quick setup
|
|
145
|
+
func validateQuickSetupProvider(providerID string) (cline.ApiProvider, error) {
|
|
146
|
+
// Normalize provider ID (trim whitespace, lowercase)
|
|
147
|
+
normalizedID := strings.TrimSpace(strings.ToLower(providerID))
|
|
148
|
+
|
|
149
|
+
// Explicitly block Bedrock
|
|
150
|
+
if normalizedID == "bedrock" {
|
|
151
|
+
return cline.ApiProvider_BEDROCK, fmt.Errorf("bedrock provider is not supported for quick setup due to complex authentication requirements. Please use interactive setup: cline auth")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Map provider string to enum using existing function
|
|
155
|
+
provider, ok := mapProviderStringToEnum(normalizedID)
|
|
156
|
+
if !ok {
|
|
157
|
+
// Provider not found - provide helpful error message
|
|
158
|
+
supportedProviders := []string{
|
|
159
|
+
"openai-native", "openai", "anthropic", "gemini",
|
|
160
|
+
"openrouter", "xai", "cerebras", "ollama",
|
|
161
|
+
}
|
|
162
|
+
return cline.ApiProvider_ANTHROPIC, fmt.Errorf(
|
|
163
|
+
"invalid provider '%s'. Supported providers: %s",
|
|
164
|
+
providerID,
|
|
165
|
+
strings.Join(supportedProviders, ", "),
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate against supported quick setup providers
|
|
170
|
+
supportedProviders := map[cline.ApiProvider]bool{
|
|
171
|
+
cline.ApiProvider_OPENAI_NATIVE: true,
|
|
172
|
+
cline.ApiProvider_OPENAI: true,
|
|
173
|
+
cline.ApiProvider_ANTHROPIC: true,
|
|
174
|
+
cline.ApiProvider_GEMINI: true,
|
|
175
|
+
cline.ApiProvider_OPENROUTER: true,
|
|
176
|
+
cline.ApiProvider_XAI: true,
|
|
177
|
+
cline.ApiProvider_CEREBRAS: true,
|
|
178
|
+
cline.ApiProvider_OLLAMA: true,
|
|
179
|
+
cline.ApiProvider_NOUSRESEARCH: true,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if !supportedProviders[provider] {
|
|
183
|
+
return provider, fmt.Errorf(
|
|
184
|
+
"provider '%s' is not supported for quick setup. Please use interactive setup: cline auth",
|
|
185
|
+
providerID,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return provider, nil
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// validateAndFetchModel validates the model ID or fetches from provider if needed
|
|
193
|
+
// Returns the final model ID and optional model info
|
|
194
|
+
// For providers with static models, validates against the list
|
|
195
|
+
// For providers with dynamic models, fetches the list if possible
|
|
196
|
+
func validateAndFetchModel(ctx context.Context, manager *task.Manager, provider cline.ApiProvider, modelID, apiKey string) (string, interface{}, error) {
|
|
197
|
+
// Normalize model ID
|
|
198
|
+
modelID = strings.TrimSpace(modelID)
|
|
199
|
+
if modelID == "" {
|
|
200
|
+
return "", nil, fmt.Errorf("model ID cannot be empty")
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// For most providers, we trust the user's input since we can't easily validate without making API calls
|
|
204
|
+
// The actual validation will happen when the model is used
|
|
205
|
+
switch provider {
|
|
206
|
+
case cline.ApiProvider_OPENROUTER:
|
|
207
|
+
// OpenRouter supports model info fetching, but it requires an API call
|
|
208
|
+
// For quick setup, we'll trust the user's input and return nil for model info
|
|
209
|
+
// The actual model info will be fetched when needed
|
|
210
|
+
if global.Config.Verbose {
|
|
211
|
+
fmt.Printf("[DEBUG] OpenRouter model ID: %s (will be validated on first use)\n", modelID)
|
|
212
|
+
}
|
|
213
|
+
return modelID, nil, nil
|
|
214
|
+
|
|
215
|
+
case cline.ApiProvider_OLLAMA:
|
|
216
|
+
// Ollama models can be validated by fetching the list, but this requires the server to be running
|
|
217
|
+
// For quick setup, we'll trust the user's input
|
|
218
|
+
if global.Config.Verbose {
|
|
219
|
+
fmt.Printf("[DEBUG] Ollama model ID: %s (will be validated when server is accessible)\n", modelID)
|
|
220
|
+
}
|
|
221
|
+
return modelID, nil, nil
|
|
222
|
+
|
|
223
|
+
default:
|
|
224
|
+
// For other providers (Anthropic, OpenAI, Gemini, XAI, Cerebras), trust user input
|
|
225
|
+
// Model validation will occur when the model is actually used
|
|
226
|
+
if global.Config.Verbose {
|
|
227
|
+
fmt.Printf("[DEBUG] %s model ID: %s (will be validated on first use)\n", GetProviderDisplayName(provider), modelID)
|
|
228
|
+
}
|
|
229
|
+
return modelID, nil, nil
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// markWelcomeViewCompleted marks the welcome view as completed in the state
|
|
234
|
+
// This prevents the welcome view from showing up after quick setup
|
|
235
|
+
func markWelcomeViewCompleted(ctx context.Context, manager *task.Manager) error {
|
|
236
|
+
// Use the State service to update the welcome view flag
|
|
237
|
+
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
|
|
238
|
+
if err != nil {
|
|
239
|
+
return fmt.Errorf("failed to mark welcome view as completed: %w", err)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if global.Config.Verbose {
|
|
243
|
+
fmt.Println("[DEBUG] Marked welcome view as completed")
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return nil
|
|
247
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
|
|
7
|
+
"github.com/cline/cli/pkg/cli/global"
|
|
8
|
+
"github.com/cline/cli/pkg/cli/task"
|
|
9
|
+
"github.com/cline/grpc-go/cline"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// DefaultClineModelID is the default model ID for Cline provider.
|
|
13
|
+
// Cline uses OpenRouter-compatible model IDs.
|
|
14
|
+
const DefaultClineModelID = "anthropic/claude-sonnet-4.5"
|
|
15
|
+
|
|
16
|
+
// FetchClineModels fetches available Cline models from Cline Core.
|
|
17
|
+
// Note: Cline provider uses OpenRouter-compatible API and model format.
|
|
18
|
+
// The models are fetched using the same method as OpenRouter.
|
|
19
|
+
func FetchClineModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OpenRouterModelInfo, error) {
|
|
20
|
+
if global.Config.Verbose {
|
|
21
|
+
fmt.Println("Fetching Cline models (using OpenRouter-compatible API)")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Cline uses OpenRouter model fetching
|
|
25
|
+
models, err := FetchOpenRouterModels(ctx, manager)
|
|
26
|
+
if err != nil {
|
|
27
|
+
return nil, fmt.Errorf("failed to fetch Cline models: %w", err)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return models, nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// GetClineModelInfo retrieves information for a specific Cline model.
|
|
34
|
+
func GetClineModelInfo(modelID string, models map[string]*cline.OpenRouterModelInfo) (*cline.OpenRouterModelInfo, error) {
|
|
35
|
+
modelInfo, exists := models[modelID]
|
|
36
|
+
if !exists {
|
|
37
|
+
return nil, fmt.Errorf("model %s not found", modelID)
|
|
38
|
+
}
|
|
39
|
+
return modelInfo, nil
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// SetDefaultClineModel configures the default Cline model after authentication.
|
|
43
|
+
// This is called automatically after successful Cline sign-in.
|
|
44
|
+
func SetDefaultClineModel(ctx context.Context, manager *task.Manager) error {
|
|
45
|
+
|
|
46
|
+
// Fetch available models
|
|
47
|
+
models, err := FetchClineModels(ctx, manager)
|
|
48
|
+
if err != nil {
|
|
49
|
+
// If we can't fetch models, we'll use the default without model info
|
|
50
|
+
fmt.Printf("Warning: Could not fetch Cline models: %v\n", err)
|
|
51
|
+
fmt.Printf("Using default model: %s\n", DefaultClineModelID)
|
|
52
|
+
return applyDefaultClineModel(ctx, manager, nil)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if default model is available
|
|
56
|
+
modelInfo, err := GetClineModelInfo(DefaultClineModelID, models)
|
|
57
|
+
if err != nil {
|
|
58
|
+
fmt.Printf("Warning: Default model not found: %v\n", err)
|
|
59
|
+
// Try to use any available model
|
|
60
|
+
for modelID := range models {
|
|
61
|
+
fmt.Printf("Using available model: %s\n", modelID)
|
|
62
|
+
return applyClineModelConfiguration(ctx, manager, modelID, models[modelID])
|
|
63
|
+
}
|
|
64
|
+
return fmt.Errorf("no usable Cline models found")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if err := applyClineModelConfiguration(ctx, manager, DefaultClineModelID, modelInfo); err != nil {
|
|
68
|
+
return err
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if err := setWelcomeViewCompletedWithManager(ctx, manager); err != nil {
|
|
72
|
+
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return nil
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// SelectClineModel presents a menu to select a Cline model and applies the configuration.
|
|
79
|
+
func SelectClineModel(ctx context.Context, manager *task.Manager) error {
|
|
80
|
+
|
|
81
|
+
// Fetch models (uses OpenRouter-compatible format)
|
|
82
|
+
models, err := FetchClineModels(ctx, manager)
|
|
83
|
+
if err != nil {
|
|
84
|
+
return fmt.Errorf("failed to fetch Cline models: %w", err)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Convert to interface map for generic utilities
|
|
88
|
+
modelMap := ConvertOpenRouterModelsToInterface(models)
|
|
89
|
+
|
|
90
|
+
// Get model IDs as a sorted list
|
|
91
|
+
modelIDs := ConvertModelsMapToSlice(modelMap)
|
|
92
|
+
|
|
93
|
+
// Display selection menu
|
|
94
|
+
selectedModelID, err := DisplayModelSelectionMenu(modelIDs, "Cline")
|
|
95
|
+
if err != nil {
|
|
96
|
+
return fmt.Errorf("model selection failed: %w", err)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Get the selected model info
|
|
100
|
+
modelInfo := models[selectedModelID]
|
|
101
|
+
|
|
102
|
+
// Apply the configuration
|
|
103
|
+
if err := applyClineModelConfiguration(ctx, manager, selectedModelID, modelInfo); err != nil {
|
|
104
|
+
return err
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fmt.Println()
|
|
108
|
+
|
|
109
|
+
// Return to main auth menu after model selection
|
|
110
|
+
return HandleAuthMenuNoArgs(ctx)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// applyClineModelConfiguration applies a Cline model configuration to both Act and Plan modes using UpdateProviderPartial.
|
|
114
|
+
// Cline uses OpenRouter-compatible model format.
|
|
115
|
+
func applyClineModelConfiguration(ctx context.Context, manager *task.Manager, modelID string, modelInfo *cline.OpenRouterModelInfo) error {
|
|
116
|
+
provider := cline.ApiProvider_CLINE
|
|
117
|
+
|
|
118
|
+
updates := ProviderUpdatesPartial{
|
|
119
|
+
ModelID: &modelID,
|
|
120
|
+
ModelInfo: modelInfo,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return UpdateProviderPartial(ctx, manager, provider, updates, true)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func applyDefaultClineModel(ctx context.Context, manager *task.Manager, modelInfo *cline.OpenRouterModelInfo) error {
|
|
127
|
+
if err := applyClineModelConfiguration(ctx, manager, DefaultClineModelID, modelInfo); err != nil {
|
|
128
|
+
return err
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if err := setWelcomeViewCompletedWithManager(ctx, manager); err != nil {
|
|
132
|
+
verboseLog("Warning: Failed to mark welcome view as completed: %v", err)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return nil
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func setWelcomeViewCompletedWithManager(ctx context.Context, manager *task.Manager) error {
|
|
139
|
+
_, err := manager.GetClient().State.SetWelcomeViewCompleted(ctx, &cline.BooleanRequest{Value: true})
|
|
140
|
+
return err
|
|
141
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"sort"
|
|
8
|
+
|
|
9
|
+
"github.com/charmbracelet/huh"
|
|
10
|
+
"github.com/cline/cli/pkg/cli/task"
|
|
11
|
+
"github.com/cline/grpc-go/cline"
|
|
12
|
+
"golang.org/x/term"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// FetchOpenRouterModels fetches available OpenRouter models from Cline Core
|
|
16
|
+
func FetchOpenRouterModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OpenRouterModelInfo, error) {
|
|
17
|
+
resp, err := manager.GetClient().Models.RefreshOpenRouterModelsRpc(ctx, &cline.EmptyRequest{})
|
|
18
|
+
if err != nil {
|
|
19
|
+
return nil, fmt.Errorf("failed to fetch OpenRouter models: %w", err)
|
|
20
|
+
}
|
|
21
|
+
return resp.Models, nil
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// FetchOcaModels fetches available Oca models from Cline Core
|
|
25
|
+
func FetchOcaModels(ctx context.Context, manager *task.Manager) (map[string]*cline.OcaModelInfo, error) {
|
|
26
|
+
resp, err := manager.GetClient().Models.RefreshOcaModels(ctx, &cline.StringRequest{})
|
|
27
|
+
if err != nil {
|
|
28
|
+
return nil, fmt.Errorf("failed to fetch Oca models: %w", err)
|
|
29
|
+
}
|
|
30
|
+
return resp.Models, nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ConvertOpenRouterModelsToInterface converts OpenRouter model map to generic interface map.
|
|
34
|
+
// This allows OpenRouter and Cline models to be used with the generic fetching utilities.
|
|
35
|
+
func ConvertOpenRouterModelsToInterface(models map[string]*cline.OpenRouterModelInfo) map[string]interface{} {
|
|
36
|
+
result := make(map[string]interface{}, len(models))
|
|
37
|
+
for k, v := range models {
|
|
38
|
+
result[k] = v
|
|
39
|
+
}
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// FetchOpenAiModels fetches available OpenAI models from Cline Core
|
|
45
|
+
// Takes the API key and returns a list of model IDs
|
|
46
|
+
func FetchOpenAiModels(ctx context.Context, manager *task.Manager, baseURL, apiKey string) ([]string, error) {
|
|
47
|
+
req := &cline.OpenAiModelsRequest{
|
|
48
|
+
BaseUrl: baseURL,
|
|
49
|
+
ApiKey: apiKey,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
resp, err := manager.GetClient().Models.RefreshOpenAiModels(ctx, req)
|
|
53
|
+
if err != nil {
|
|
54
|
+
return nil, fmt.Errorf("failed to fetch OpenAI models: %w", err)
|
|
55
|
+
}
|
|
56
|
+
return resp.Values, nil
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// FetchOllamaModels fetches available Ollama models from Cline Core
|
|
60
|
+
// Takes the base URL (empty string for default) and returns a list of model IDs
|
|
61
|
+
func FetchOllamaModels(ctx context.Context, manager *task.Manager, baseURL string) ([]string, error) {
|
|
62
|
+
req := &cline.StringRequest{
|
|
63
|
+
Value: baseURL,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
resp, err := manager.GetClient().Models.GetOllamaModels(ctx, req)
|
|
67
|
+
if err != nil {
|
|
68
|
+
return nil, fmt.Errorf("failed to fetch Ollama models: %w", err)
|
|
69
|
+
}
|
|
70
|
+
return resp.Values, nil
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// DisplayModelSelectionMenu shows an interactive menu for selecting a model from a list.
|
|
74
|
+
// Models are displayed alphabetically. Uses model ID as the option value to avoid
|
|
75
|
+
// index-based bugs when list order changes.
|
|
76
|
+
// Returns the selected model ID.
|
|
77
|
+
func DisplayModelSelectionMenu(models []string, providerName string) (string, error) {
|
|
78
|
+
if len(models) == 0 {
|
|
79
|
+
return "", fmt.Errorf("no models available for selection")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Use model ID as the value (not index) to avoid positional coupling bugs
|
|
83
|
+
var selectedModel string
|
|
84
|
+
options := make([]huh.Option[string], len(models))
|
|
85
|
+
for i, model := range models {
|
|
86
|
+
options[i] = huh.NewOption(model, model)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
title := fmt.Sprintf("Select a %s model", providerName)
|
|
90
|
+
|
|
91
|
+
form := huh.NewForm(
|
|
92
|
+
huh.NewGroup(
|
|
93
|
+
huh.NewSelect[string]().
|
|
94
|
+
Title(title).
|
|
95
|
+
Options(options...).
|
|
96
|
+
Height(calculateSelectHeight()).
|
|
97
|
+
Filtering(true).
|
|
98
|
+
Value(&selectedModel),
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if err := form.Run(); err != nil {
|
|
103
|
+
return "", fmt.Errorf("failed to select model: %w", err)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return selectedModel, nil
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ConvertModelsMapToSlice converts a map of models to a sorted slice of model IDs.
|
|
110
|
+
// This is useful for displaying models in a consistent order in UI components.
|
|
111
|
+
func ConvertModelsMapToSlice(models map[string]interface{}) []string {
|
|
112
|
+
result := make([]string, 0, len(models))
|
|
113
|
+
for modelID := range models {
|
|
114
|
+
result = append(result, modelID)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Sort alphabetically for consistent display
|
|
118
|
+
sort.Strings(result)
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ConvertOcaModelsToInterface converts Oca model map to generic interface map.
|
|
124
|
+
// This allows Oca and Cline models to be used with the generic fetching utilities.
|
|
125
|
+
func ConvertOcaModelsToInterface(models map[string]*cline.OcaModelInfo) map[string]interface{} {
|
|
126
|
+
result := make(map[string]interface{}, len(models))
|
|
127
|
+
for k, v := range models {
|
|
128
|
+
result[k] = v
|
|
129
|
+
}
|
|
130
|
+
return result
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// getTerminalHeight returns the terminal height (rows)
|
|
134
|
+
func getTerminalHeight() int {
|
|
135
|
+
_, height, err := term.GetSize(int(os.Stdout.Fd()))
|
|
136
|
+
if err != nil || height <= 0 {
|
|
137
|
+
return 25 // safe fallback for non-TTY or errors
|
|
138
|
+
}
|
|
139
|
+
return height
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// calculateSelectHeight computes appropriate height for Select component
|
|
143
|
+
// Reserves space for title, search UI, and margins
|
|
144
|
+
func calculateSelectHeight() int {
|
|
145
|
+
height := getTerminalHeight()
|
|
146
|
+
// Reserve ~10 rows for UI chrome (title, search, margins)
|
|
147
|
+
visibleRows := height - 10
|
|
148
|
+
// Clamp between 8 (minimum usable) and 25 (maximum before unwieldy)
|
|
149
|
+
if visibleRows < 8 {
|
|
150
|
+
return 8
|
|
151
|
+
}
|
|
152
|
+
if visibleRows > 25 {
|
|
153
|
+
return 25
|
|
154
|
+
}
|
|
155
|
+
return visibleRows
|
|
156
|
+
}
|