@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,285 @@
1
+ package auth
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "time"
7
+
8
+ "github.com/charmbracelet/huh"
9
+ "github.com/cline/cli/pkg/cli/global"
10
+ "github.com/cline/cli/pkg/cli/task"
11
+ "github.com/cline/grpc-go/cline"
12
+ )
13
+
14
+ var isSessionAuthenticated bool
15
+
16
+ // Cline provider specific code
17
+
18
+ func HandleClineAuth(ctx context.Context) error {
19
+ verboseLog("Authenticating with Cline...")
20
+
21
+ // Check if already authenticated
22
+ if IsAuthenticated(ctx) {
23
+ return signOutDialog(ctx)
24
+ }
25
+
26
+ // Perform sign in
27
+ if err := signIn(ctx); err != nil {
28
+ return err
29
+ }
30
+
31
+ fmt.Println()
32
+
33
+ verboseLog("✓ You are signed in!")
34
+
35
+
36
+ // Configure default Cline model after successful authentication
37
+ if err := configureDefaultClineModel(ctx); err != nil {
38
+ fmt.Printf("Warning: Could not configure default Cline model: %v\n", err)
39
+ fmt.Println("You can configure a model later with 'cline auth' and selecting 'Change Cline model'")
40
+ }
41
+
42
+ // Return to main auth menu after successful authentication
43
+ return HandleAuthMenuNoArgs(ctx)
44
+ }
45
+
46
+ func signOut(ctx context.Context) error {
47
+ client, err := global.GetDefaultClient(ctx)
48
+ if err != nil {
49
+ return err
50
+ }
51
+
52
+ if _, err = client.Account.AccountLogoutClicked(ctx, &cline.EmptyRequest{}); err != nil {
53
+ return err
54
+ }
55
+
56
+ isSessionAuthenticated = false
57
+ fmt.Println("You have been signed out of Cline.")
58
+ return nil
59
+ }
60
+
61
+ func signOutDialog(ctx context.Context) error {
62
+ var confirm bool
63
+ form := huh.NewForm(
64
+ huh.NewGroup(
65
+ huh.NewConfirm().
66
+ Title("You are already signed in to Cline.").
67
+ Description("Would you like to sign out?").
68
+ Value(&confirm),
69
+ ),
70
+ )
71
+
72
+ if err := form.Run(); err != nil {
73
+ return nil
74
+ }
75
+
76
+ if confirm {
77
+ if err := signOut(ctx); err != nil {
78
+ fmt.Printf("Failed to sign out: %v\n", err)
79
+ return err
80
+ }
81
+ }
82
+ return HandleAuthMenuNoArgs(ctx)
83
+ }
84
+
85
+ func signIn(ctx context.Context) error {
86
+ if IsAuthenticated(ctx) {
87
+ return nil
88
+ }
89
+
90
+ // Subscribe to auth updates before initiating login
91
+ verboseLog("Subscribing to auth status updates...")
92
+ listener, err := NewAuthStatusListener(ctx)
93
+ if err != nil {
94
+ verboseLog("Failed to subscribe to auth updates: %v", err)
95
+ return fmt.Errorf("failed to subscribe to auth updates: %w", err)
96
+ }
97
+ defer listener.Stop()
98
+
99
+ if err := listener.Start(); err != nil {
100
+ verboseLog("Failed to start auth listener: %v", err)
101
+ return fmt.Errorf("failed to start auth listener: %w", err)
102
+ }
103
+
104
+ // Initiate login (opens browser with callback URL from cline-core's AuthHandler)
105
+ verboseLog("Initiating login...")
106
+ client, err := global.GetDefaultClient(ctx)
107
+ if err != nil {
108
+ verboseLog("Failed to obtain client: %v", err)
109
+ return fmt.Errorf("failed to obtain client: %w", err)
110
+ }
111
+
112
+ response, err := client.Account.AccountLoginClicked(ctx, &cline.EmptyRequest{})
113
+ if err != nil {
114
+ verboseLog("Failed to initiate login: %v", err)
115
+ return fmt.Errorf("failed to initiate login: %w", err)
116
+ }
117
+
118
+ fmt.Println("\n Opening browser for authentication...")
119
+ if response != nil && response.Value != "" {
120
+ fmt.Printf(" If the browser doesn't open automatically, visit this URL:\n %s\n\n", response.Value)
121
+ }
122
+ fmt.Println(" Waiting for you to complete authentication in your browser...")
123
+ fmt.Println(" (This may take a few moments. Timeout: 5 minutes)")
124
+
125
+ // Wait for auth status update confirming success
126
+ verboseLog("Waiting for authentication to complete...")
127
+ if err := listener.WaitForAuthentication(5 * time.Minute); err != nil {
128
+ verboseLog("Authentication failed or timed out: %v", err)
129
+ fmt.Println("\n Authentication failed or timed out.")
130
+ fmt.Println(" Please try again with 'cline auth'")
131
+ return err
132
+ }
133
+
134
+ // Only NOW set the session flag after confirmed authentication
135
+ isSessionAuthenticated = true
136
+ verboseLog("Login successful")
137
+ return nil
138
+ }
139
+
140
+ func IsAuthenticated(ctx context.Context) bool {
141
+ if isSessionAuthenticated {
142
+ verboseLog("Session is already authenticated")
143
+ return true
144
+ }
145
+
146
+ verboseLog("Verifying authentication with server...")
147
+ client, err := global.GetDefaultClient(ctx)
148
+ if err != nil {
149
+ verboseLog("Failed to get client for auth check: %v", err)
150
+ return false
151
+ }
152
+
153
+ _, err = client.Account.GetUserCredits(ctx, &cline.EmptyRequest{})
154
+ if err == nil {
155
+ // Update session variable for future fast-path checks
156
+ verboseLog("Server verification successful, updating session flag")
157
+ isSessionAuthenticated = true
158
+ return true
159
+ }
160
+
161
+ verboseLog("Server verification failed: %v", err)
162
+ return false
163
+ }
164
+
165
+ // HandleChangeClineModel allows Cline-authenticated users to change their Cline model selection. Hidden when not authenticated.
166
+ func HandleChangeClineModel(ctx context.Context) error {
167
+ // Ensure user is authenticated
168
+ if !IsAuthenticated(ctx) {
169
+ return fmt.Errorf("you must be authenticated with Cline to change models. Run 'cline auth' to sign in")
170
+ }
171
+
172
+ // Get task manager
173
+ manager, err := createTaskManager(ctx)
174
+ if err != nil {
175
+ return fmt.Errorf("failed to create task manager: %w", err)
176
+ }
177
+
178
+ // Launch Cline model selection
179
+ return SelectClineModel(ctx, manager)
180
+ }
181
+
182
+ // configureDefaultClineModel configures the default Cline model after authentication
183
+ func configureDefaultClineModel(ctx context.Context) error {
184
+ verboseLog("Configuring default Cline model...")
185
+
186
+ // Create task manager
187
+ manager, err := task.NewManagerForDefault(ctx)
188
+ if err != nil {
189
+ return fmt.Errorf("failed to create task manager: %w", err)
190
+ }
191
+
192
+ // Set default Cline model
193
+ return SetDefaultClineModel(ctx, manager)
194
+ }
195
+
196
+ // HandleSelectOrganization allows Cline-authenticated users to select which organization to use
197
+ func HandleSelectOrganization(ctx context.Context) error {
198
+ // Ensure user is authenticated
199
+ if !IsAuthenticated(ctx) {
200
+ return fmt.Errorf("you must be authenticated with Cline to select an organization. Run 'cline auth' to sign in")
201
+ }
202
+
203
+ // Get client
204
+ client, err := global.GetDefaultClient(ctx)
205
+ if err != nil {
206
+ return fmt.Errorf("failed to get client: %w", err)
207
+ }
208
+
209
+ // Fetch user organizations
210
+ orgsResponse, err := client.Account.GetUserOrganizations(ctx, &cline.EmptyRequest{})
211
+ if err != nil {
212
+ return fmt.Errorf("failed to fetch organizations: %w", err)
213
+ }
214
+
215
+ organizations := orgsResponse.GetOrganizations()
216
+ if len(organizations) == 0 {
217
+ fmt.Println("You don't have any organizations yet.")
218
+ fmt.Println("Visit https://app.cline.bot/dashboard to create an organization.")
219
+ return HandleAuthMenuNoArgs(ctx)
220
+ }
221
+
222
+ // Build options list: Personal + Organizations
223
+ var options []huh.Option[string]
224
+ options = append(options, huh.NewOption("Personal", "personal"))
225
+
226
+ for _, org := range organizations {
227
+ displayName := org.Name
228
+ // Show active indicator
229
+ if org.Active {
230
+ displayName = fmt.Sprintf("%s (active)", displayName)
231
+ }
232
+ options = append(options, huh.NewOption(displayName, org.OrganizationId))
233
+ }
234
+
235
+ options = append(options, huh.NewOption("(Cancel)", "cancel"))
236
+
237
+ // Show selection menu
238
+ var selected string
239
+ form := huh.NewForm(
240
+ huh.NewGroup(
241
+ huh.NewSelect[string]().
242
+ Title("Select which account to use").
243
+ Options(options...).
244
+ Value(&selected),
245
+ ),
246
+ )
247
+
248
+ if err := form.Run(); err != nil {
249
+ return fmt.Errorf("failed to select organization: %w", err)
250
+ }
251
+
252
+ if selected == "cancel" {
253
+ return HandleAuthMenuNoArgs(ctx)
254
+ }
255
+
256
+ // Set the organization
257
+ var orgId *string
258
+ if selected != "personal" {
259
+ orgId = &selected
260
+ }
261
+
262
+ req := &cline.UserOrganizationUpdateRequest{
263
+ OrganizationId: orgId,
264
+ }
265
+
266
+ if _, err := client.Account.SetUserOrganization(ctx, req); err != nil {
267
+ return fmt.Errorf("failed to set organization: %w", err)
268
+ }
269
+
270
+ if selected == "personal" {
271
+ fmt.Println("✓ Switched to personal account")
272
+ } else {
273
+ // Find the org name to display
274
+ var orgName string
275
+ for _, org := range organizations {
276
+ if org.OrganizationId == selected {
277
+ orgName = org.Name
278
+ break
279
+ }
280
+ }
281
+ fmt.Printf("✓ Switched to organization: %s\n", orgName)
282
+ }
283
+
284
+ return HandleAuthMenuNoArgs(ctx)
285
+ }
@@ -0,0 +1,323 @@
1
+ package auth
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+
7
+ "github.com/charmbracelet/huh"
8
+ "github.com/cline/cli/pkg/cli/display"
9
+ "github.com/cline/cli/pkg/cli/global"
10
+ "github.com/cline/cli/pkg/cli/task"
11
+ "github.com/cline/grpc-go/cline"
12
+ )
13
+
14
+ // contextKey is a distinct type for context keys to avoid collisions
15
+ type contextKey string
16
+
17
+ const authInstanceAddressKey contextKey = "authInstanceAddress"
18
+
19
+ // AuthAction represents the type of authentication action
20
+ type AuthAction string
21
+
22
+ const (
23
+ AuthActionClineLogin AuthAction = "cline_login"
24
+ AuthActionBYOSetup AuthAction = "provider_setup"
25
+ AuthActionChangeClineModel AuthAction = "change_cline_model"
26
+ AuthActionSelectOrganization AuthAction = "select_organization"
27
+ AuthActionSelectProvider AuthAction = "select_provider"
28
+ AuthActionExit AuthAction = "exit_wizard"
29
+ )
30
+
31
+ // Cline Auth Menu
32
+ // Example Layout
33
+ //
34
+ // ┃ Cline Account: <authenticated/not authenticated>
35
+ // ┃ Active Provider: <provider name or none configured>
36
+ // ┃ Active Model: <model name or none configured>
37
+ // ┃
38
+ // ┃ What would you like to do?
39
+ // ┃ Change Cline model (only if authenticated) - hidden if not authenticated
40
+ // ┃ Authenticate with Cline account / Sign out of Cline - changes based on auth status
41
+ // ┃ Select active provider (Cline or BYO) - always shown. Used to switch between Cline and BYO providers
42
+ // ┃ Configure BYO API providers - always shown. Launches provider setup wizard
43
+ // ┃ Exit authorization wizard - always shown. Exits the auth menu
44
+
45
+ // RunAuthFlow is the entry point for the entire auth flow with instance management
46
+ // It spawns a fresh instance for auth operations and cleans it up when done
47
+ func RunAuthFlow(ctx context.Context, args []string) error {
48
+ // Spawn a fresh instance for auth operations
49
+ instanceInfo, err := global.Clients.StartNewInstance(ctx)
50
+ if err != nil {
51
+ return fmt.Errorf("failed to start auth instance: %w", err)
52
+ }
53
+
54
+ // Cleanup when done (success, error, or panic)
55
+ defer func() {
56
+ verboseLog("Shutting down auth instance at %s", instanceInfo.Address)
57
+ if err := global.KillInstanceByAddress(context.Background(), global.Clients.GetRegistry(), instanceInfo.Address); err != nil {
58
+ verboseLog("Warning: Failed to kill auth instance: %v", err)
59
+ }
60
+ }()
61
+
62
+ // Store instance address in context for all auth handlers to use
63
+ authCtx := context.WithValue(ctx, authInstanceAddressKey, instanceInfo.Address)
64
+
65
+ // Route to existing auth flow
66
+ return HandleAuthCommand(authCtx, args)
67
+ }
68
+
69
+ // Main entry point for handling the `cline auth` command
70
+ // HandleAuthCommand routes the auth command based on the number of arguments
71
+ func HandleAuthCommand(ctx context.Context, args []string) error {
72
+
73
+ // Check if flags are provided for quick setup
74
+ if QuickProvider != "" || QuickAPIKey != "" || QuickModelID != "" || QuickBaseURL != "" {
75
+ if QuickProvider == "" || QuickAPIKey == "" || QuickModelID == "" {
76
+ return fmt.Errorf("quick setup requires --provider, --apikey, and --modelid flags. Use 'cline auth --help' for more information")
77
+ }
78
+ return QuickSetupFromFlags(ctx, QuickProvider, QuickAPIKey, QuickModelID, QuickBaseURL)
79
+ }
80
+
81
+ switch len(args) {
82
+ case 0:
83
+ // No args: Show uth wizard
84
+ return HandleAuthMenuNoArgs(ctx)
85
+ case 1, 2, 3, 4:
86
+ fmt.Println("Invalid positional arguments. Correct usage:")
87
+ fmt.Println(" cline auth --provider <provider> --apikey <key> --modelid <model> --baseurl <optional>")
88
+ return nil
89
+ default:
90
+ return fmt.Errorf("too many arguments. Use flags for quick setup: --provider, --apikey, --modelid --baseurl(optional)")
91
+ }
92
+ }
93
+
94
+ // getAuthInstanceAddress retrieves the auth instance address from context
95
+ // Returns empty string if not found (falls back to default behavior)
96
+ func getAuthInstanceAddress(ctx context.Context) string {
97
+ if addr, ok := ctx.Value(authInstanceAddressKey).(string); ok {
98
+ return addr
99
+ }
100
+ return ""
101
+ }
102
+
103
+ // HandleAuthMenuNoArgs prepares the auth menu when no arguments are provided
104
+ func HandleAuthMenuNoArgs(ctx context.Context) error {
105
+ // Check if Cline is authenticated
106
+ isClineAuth := IsAuthenticated(ctx)
107
+
108
+ // Get current provider config for display
109
+ var currentProvider string
110
+ var currentModel string
111
+ if manager, err := createTaskManager(ctx); err == nil {
112
+ if providerList, err := GetProviderConfigurations(ctx, manager); err == nil {
113
+ if providerList.ActProvider != nil {
114
+ currentProvider = GetProviderDisplayName(providerList.ActProvider.Provider)
115
+ currentModel = providerList.ActProvider.ModelID
116
+ }
117
+ }
118
+ }
119
+
120
+ // Fetch organizations if authenticated
121
+ var hasOrganizations bool
122
+ if isClineAuth {
123
+ if client, err := global.GetDefaultClient(ctx); err == nil {
124
+ if orgsResponse, err := client.Account.GetUserOrganizations(ctx, &cline.EmptyRequest{}); err == nil {
125
+ hasOrganizations = len(orgsResponse.GetOrganizations()) > 0
126
+ }
127
+ }
128
+ }
129
+
130
+ action, err := ShowAuthMenuWithStatus(isClineAuth, hasOrganizations, currentProvider, currentModel)
131
+ if err != nil {
132
+ // Check if user cancelled - propagate for clean exit
133
+ if err == huh.ErrUserAborted {
134
+ return huh.ErrUserAborted
135
+ }
136
+ return err
137
+ }
138
+
139
+ switch action {
140
+ case AuthActionClineLogin:
141
+ return HandleClineAuth(ctx)
142
+ case AuthActionBYOSetup:
143
+ return HandleAPIProviderSetup(ctx)
144
+ case AuthActionChangeClineModel:
145
+ return HandleChangeClineModel(ctx)
146
+ case AuthActionSelectOrganization:
147
+ return HandleSelectOrganization(ctx)
148
+ case AuthActionSelectProvider:
149
+ return HandleSelectProvider(ctx)
150
+ case AuthActionExit:
151
+ return nil
152
+ default:
153
+ return fmt.Errorf("invalid action")
154
+ }
155
+ }
156
+
157
+ // ShowAuthMenuWithStatus displays the main auth menu with Cline + provider status
158
+ func ShowAuthMenuWithStatus(isClineAuthenticated bool, hasOrganizations bool, currentProvider, currentModel string) (AuthAction, error) {
159
+ var action AuthAction
160
+ var options []huh.Option[AuthAction]
161
+
162
+ // Build menu options based on authentication status
163
+ if isClineAuthenticated {
164
+ options = []huh.Option[AuthAction]{
165
+ huh.NewOption("Change Cline model", AuthActionChangeClineModel),
166
+ }
167
+
168
+ // Add organization selection if user has organizations
169
+ if hasOrganizations {
170
+ options = append(options, huh.NewOption("Select organization", AuthActionSelectOrganization))
171
+ }
172
+
173
+ options = append(options,
174
+ huh.NewOption("Sign out of Cline", AuthActionClineLogin),
175
+ huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
176
+ huh.NewOption("Configure BYO API providers", AuthActionBYOSetup),
177
+ huh.NewOption("Exit authorization wizard", AuthActionExit),
178
+ )
179
+ } else {
180
+ options = []huh.Option[AuthAction]{
181
+ huh.NewOption("Authenticate with Cline account", AuthActionClineLogin),
182
+ huh.NewOption("Select active provider (Cline or BYO)", AuthActionSelectProvider),
183
+ huh.NewOption("Configure BYO API providers", AuthActionBYOSetup),
184
+ huh.NewOption("Exit authorization wizard", AuthActionExit),
185
+ }
186
+ }
187
+
188
+ // Determine menu title based on status
189
+ var title string
190
+ renderer := display.NewRenderer(global.Config.OutputFormat)
191
+
192
+ // Always show Cline authentication status
193
+ if isClineAuthenticated {
194
+ title = fmt.Sprintf("Cline Account: %s Authenticated\n", renderer.Green("✓"))
195
+ } else {
196
+ title = fmt.Sprintf("Cline Account: %s Not authenticated\n", renderer.Red("✗"))
197
+ }
198
+
199
+ // Show active provider and model if configured (regardless of Cline auth status)
200
+ if currentProvider != "" && currentModel != "" {
201
+ title += fmt.Sprintf("Active Provider: %s\nActive Model: %s\n",
202
+ renderer.White(currentProvider),
203
+ renderer.White(currentModel))
204
+ }
205
+
206
+ // Always end with a huh?
207
+ title += "\nWhat would you like to do?"
208
+
209
+ form := huh.NewForm(
210
+ huh.NewGroup(
211
+ huh.NewSelect[AuthAction]().
212
+ Title(title).
213
+ Options(options...).
214
+ Value(&action),
215
+ ),
216
+ )
217
+
218
+ if err := form.Run(); err != nil {
219
+ // Check if user cancelled with Control-C
220
+ if err == huh.ErrUserAborted {
221
+ // Return the error to allow deferred cleanup to run
222
+ return "", huh.ErrUserAborted
223
+ }
224
+ return "", fmt.Errorf("failed to get menu choice: %w", err)
225
+ }
226
+
227
+ return action, nil
228
+ }
229
+
230
+ // HandleAPIProviderSetup launches the API provider configuration wizard
231
+ func HandleAPIProviderSetup(ctx context.Context) error {
232
+ wizard, err := NewProviderWizard(ctx)
233
+ if err != nil {
234
+ return fmt.Errorf("failed to create provider wizard: %w", err)
235
+ }
236
+
237
+ return wizard.Run()
238
+ }
239
+
240
+ // HandleSelectProvider allows users to switch between Cline provider and BYO providers
241
+ func HandleSelectProvider(ctx context.Context) error {
242
+ // Get task manager
243
+ manager, err := createTaskManager(ctx)
244
+ if err != nil {
245
+ return fmt.Errorf("failed to create task manager: %w", err)
246
+ }
247
+
248
+ // Detect all providers with valid configurations (is an API key present)
249
+ availableProviders, err := DetectAllConfiguredProviders(ctx, manager)
250
+ if err != nil {
251
+ return fmt.Errorf("failed to detect configured providers: %w", err)
252
+ }
253
+
254
+ // Build list of available providers
255
+ var providerOptions []huh.Option[string]
256
+ var providerMapping = make(map[string]cline.ApiProvider)
257
+
258
+ // Add each configured provider to the selection menu
259
+ for _, provider := range availableProviders {
260
+ providerName := GetProviderDisplayName(provider)
261
+ providerKey := fmt.Sprintf("provider_%d", provider)
262
+ providerOptions = append(providerOptions, huh.NewOption(providerName, providerKey))
263
+ providerMapping[providerKey] = provider
264
+ }
265
+
266
+ if len(providerOptions) == 0 {
267
+ fmt.Println("No providers available. Please configure a provider first.")
268
+ return HandleAuthMenuNoArgs(ctx)
269
+ }
270
+
271
+ providerOptions = append(providerOptions, huh.NewOption("(Cancel)", "cancel"))
272
+
273
+ // Show selection menu
274
+ var selected string
275
+ form := huh.NewForm(
276
+ huh.NewGroup(
277
+ huh.NewSelect[string]().
278
+ Title("Select which provider to use").
279
+ Options(providerOptions...).
280
+ Value(&selected),
281
+ ),
282
+ )
283
+
284
+ if err := form.Run(); err != nil {
285
+ // Check if user cancelled with Control-C
286
+ if err == huh.ErrUserAborted {
287
+ return huh.ErrUserAborted
288
+ }
289
+ return fmt.Errorf("failed to select provider: %w", err)
290
+ }
291
+
292
+ if selected == "cancel" {
293
+ return HandleAuthMenuNoArgs(ctx)
294
+ }
295
+
296
+ // Get the selected provider
297
+ selectedProvider := providerMapping[selected]
298
+
299
+ // Apply the selected provider
300
+ if selectedProvider == cline.ApiProvider_CLINE {
301
+ // Configure Cline as the active provider
302
+ return SelectClineModel(ctx, manager)
303
+ } else {
304
+ // Switch to the selected BYO provider
305
+ return SwitchToBYOProvider(ctx, manager, selectedProvider)
306
+ }
307
+ }
308
+
309
+ // createTaskManager is a helper to create a task manager (avoids import cycles)
310
+ // Uses the auth instance address from context if available, otherwise falls back to default
311
+ func createTaskManager(ctx context.Context) (*task.Manager, error) {
312
+ authAddr := getAuthInstanceAddress(ctx)
313
+ if authAddr != "" {
314
+ return task.NewManagerForAddress(ctx, authAddr)
315
+ }
316
+ return task.NewManagerForDefault(ctx)
317
+ }
318
+
319
+ func verboseLog(format string, args ...interface{}) {
320
+ if global.Config != nil && global.Config.Verbose {
321
+ fmt.Printf("[VERBOSE] "+format+"\n", args...)
322
+ }
323
+ }