@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.
Files changed (85) hide show
  1. package/.npmrc.tmp +2 -0
  2. package/README.md +72 -0
  3. package/cmd/cline/main.go +348 -0
  4. package/cmd/cline-host/main.go +71 -0
  5. package/e2e/default_update_test.go +154 -0
  6. package/e2e/helpers_test.go +378 -0
  7. package/e2e/main_test.go +47 -0
  8. package/e2e/mixed_stress_test.go +120 -0
  9. package/e2e/sqlite_helper.go +161 -0
  10. package/e2e/start_list_test.go +178 -0
  11. package/go.mod +64 -0
  12. package/go.sum +162 -0
  13. package/man/cline.1 +331 -0
  14. package/man/cline.1.md +332 -0
  15. package/package.json +54 -0
  16. package/pkg/cli/auth/auth_cline_provider.go +285 -0
  17. package/pkg/cli/auth/auth_menu.go +323 -0
  18. package/pkg/cli/auth/auth_subscription.go +130 -0
  19. package/pkg/cli/auth/byo_quick_setup.go +247 -0
  20. package/pkg/cli/auth/models_cline.go +141 -0
  21. package/pkg/cli/auth/models_list_fetch.go +156 -0
  22. package/pkg/cli/auth/models_list_static.go +69 -0
  23. package/pkg/cli/auth/providers_byo.go +184 -0
  24. package/pkg/cli/auth/providers_list.go +517 -0
  25. package/pkg/cli/auth/update_api_configurations.go +647 -0
  26. package/pkg/cli/auth/wizard_byo.go +764 -0
  27. package/pkg/cli/auth/wizard_byo_bedrock.go +193 -0
  28. package/pkg/cli/auth/wizard_byo_oca.go +366 -0
  29. package/pkg/cli/auth.go +43 -0
  30. package/pkg/cli/clerror/cline_error.go +187 -0
  31. package/pkg/cli/config/manager.go +208 -0
  32. package/pkg/cli/config/settings_renderer.go +198 -0
  33. package/pkg/cli/config.go +152 -0
  34. package/pkg/cli/display/ansi.go +27 -0
  35. package/pkg/cli/display/banner.go +211 -0
  36. package/pkg/cli/display/deduplicator.go +95 -0
  37. package/pkg/cli/display/markdown_renderer.go +139 -0
  38. package/pkg/cli/display/renderer.go +304 -0
  39. package/pkg/cli/display/segment_streamer.go +212 -0
  40. package/pkg/cli/display/streaming.go +134 -0
  41. package/pkg/cli/display/system_renderer.go +269 -0
  42. package/pkg/cli/display/tool_renderer.go +455 -0
  43. package/pkg/cli/display/tool_result_parser.go +371 -0
  44. package/pkg/cli/display/typewriter.go +210 -0
  45. package/pkg/cli/doctor.go +65 -0
  46. package/pkg/cli/global/cline-clients.go +501 -0
  47. package/pkg/cli/global/global.go +113 -0
  48. package/pkg/cli/global/registry.go +304 -0
  49. package/pkg/cli/handlers/ask_handlers.go +339 -0
  50. package/pkg/cli/handlers/handler.go +130 -0
  51. package/pkg/cli/handlers/say_handlers.go +521 -0
  52. package/pkg/cli/instances.go +506 -0
  53. package/pkg/cli/logs.go +382 -0
  54. package/pkg/cli/output/coordinator.go +167 -0
  55. package/pkg/cli/output/input_model.go +497 -0
  56. package/pkg/cli/sqlite/locks.go +366 -0
  57. package/pkg/cli/task/history_handler.go +72 -0
  58. package/pkg/cli/task/input_handler.go +577 -0
  59. package/pkg/cli/task/manager.go +1283 -0
  60. package/pkg/cli/task/settings_parser.go +754 -0
  61. package/pkg/cli/task/stream_coordinator.go +60 -0
  62. package/pkg/cli/task.go +675 -0
  63. package/pkg/cli/terminal/keyboard.go +695 -0
  64. package/pkg/cli/tui/HELP_WANTED.md +1 -0
  65. package/pkg/cli/types/history.go +17 -0
  66. package/pkg/cli/types/messages.go +329 -0
  67. package/pkg/cli/types/state.go +59 -0
  68. package/pkg/cli/updater/updater.go +409 -0
  69. package/pkg/cli/version.go +43 -0
  70. package/pkg/common/constants.go +6 -0
  71. package/pkg/common/schema.go +54 -0
  72. package/pkg/common/types.go +54 -0
  73. package/pkg/common/utils.go +185 -0
  74. package/pkg/generated/field_overrides.go +39 -0
  75. package/pkg/generated/providers.go +1584 -0
  76. package/pkg/hostbridge/diff.go +351 -0
  77. package/pkg/hostbridge/disabled/watch.go +39 -0
  78. package/pkg/hostbridge/disabled/window.go +63 -0
  79. package/pkg/hostbridge/disabled/workspace.go +66 -0
  80. package/pkg/hostbridge/env.go +166 -0
  81. package/pkg/hostbridge/grpc_server.go +113 -0
  82. package/pkg/hostbridge/simple.go +43 -0
  83. package/pkg/hostbridge/simple_workspace.go +85 -0
  84. package/pkg/hostbridge/window.go +129 -0
  85. 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
+ }