@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,193 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/charmbracelet/huh"
|
|
9
|
+
"github.com/cline/cli/pkg/cli/task"
|
|
10
|
+
"github.com/cline/grpc-go/cline"
|
|
11
|
+
"google.golang.org/protobuf/proto"
|
|
12
|
+
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// BedrockConfig holds all AWS Bedrock-specific configuration fields
|
|
16
|
+
type BedrockConfig struct {
|
|
17
|
+
// Profile authentication fields
|
|
18
|
+
UseProfile bool // Always true for successful config
|
|
19
|
+
Profile string // Optional: AWS profile name (empty = default)
|
|
20
|
+
Region string // Required: AWS region
|
|
21
|
+
Endpoint string // Optional: Custom VPC endpoint URL
|
|
22
|
+
|
|
23
|
+
// Optional features
|
|
24
|
+
UseCrossRegionInference bool // Optional: Enable cross-region inference
|
|
25
|
+
UseGlobalInference bool // Optional: Use global inference endpoint
|
|
26
|
+
UsePromptCache bool // Optional: Enable prompt caching
|
|
27
|
+
|
|
28
|
+
// Authentication method (always "profile")
|
|
29
|
+
Authentication string // Always set to "profile"
|
|
30
|
+
|
|
31
|
+
// Legacy fields (no longer used in profile-only flow)
|
|
32
|
+
AccessKey string // No longer used
|
|
33
|
+
SecretKey string // No longer used
|
|
34
|
+
SessionToken string // No longer used
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// PromptForBedrockConfig displays a profile-first authentication form for Bedrock configuration
|
|
38
|
+
func PromptForBedrockConfig(ctx context.Context, manager *task.Manager) (*BedrockConfig, error) {
|
|
39
|
+
config := &BedrockConfig{}
|
|
40
|
+
|
|
41
|
+
// First, ask if user wants to use AWS profile authentication
|
|
42
|
+
var useProfile bool
|
|
43
|
+
profileQuestion := huh.NewForm(
|
|
44
|
+
huh.NewGroup(
|
|
45
|
+
huh.NewConfirm().
|
|
46
|
+
Title("Do you want to use an AWS profile for authentication?").
|
|
47
|
+
Description("AWS profiles are managed via 'aws configure'").
|
|
48
|
+
Value(&useProfile).
|
|
49
|
+
Affirmative("Yes").
|
|
50
|
+
Negative("No").
|
|
51
|
+
Inline(true),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if err := profileQuestion.Run(); err != nil {
|
|
56
|
+
return nil, fmt.Errorf("failed to get authentication method: %w", err)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If user declines profile authentication, show message and return error
|
|
60
|
+
if !useProfile {
|
|
61
|
+
fmt.Println("\nAWS profile authentication is currently the only supported method in the CLI.")
|
|
62
|
+
fmt.Println("Please configure an AWS profile using 'aws configure' and try again.")
|
|
63
|
+
return nil, fmt.Errorf("user declined profile authentication")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// User wants profile auth - collect profile configuration
|
|
67
|
+
config.UseProfile = true
|
|
68
|
+
config.Authentication = "profile"
|
|
69
|
+
|
|
70
|
+
// Collect profile name, region, and optional settings
|
|
71
|
+
configForm := huh.NewForm(
|
|
72
|
+
huh.NewGroup(
|
|
73
|
+
huh.NewInput().
|
|
74
|
+
Title("AWS Profile Name (optional, press Enter for default profile)").
|
|
75
|
+
Value(&config.Profile).
|
|
76
|
+
Description("Leave empty to use default AWS profile"),
|
|
77
|
+
|
|
78
|
+
huh.NewInput().
|
|
79
|
+
Title("AWS Region (required, e.g., us-east-1)").
|
|
80
|
+
Value(&config.Region).
|
|
81
|
+
Validate(func(s string) error {
|
|
82
|
+
if strings.TrimSpace(s) == "" {
|
|
83
|
+
return fmt.Errorf("AWS Region is required")
|
|
84
|
+
}
|
|
85
|
+
return nil
|
|
86
|
+
}),
|
|
87
|
+
|
|
88
|
+
huh.NewInput().
|
|
89
|
+
Title("Custom VPC Endpoint URL (optional)").
|
|
90
|
+
Value(&config.Endpoint).
|
|
91
|
+
Description("Press Enter to skip"),
|
|
92
|
+
|
|
93
|
+
huh.NewConfirm().
|
|
94
|
+
Title("Enable Prompt Cache? ").
|
|
95
|
+
Value(&config.UsePromptCache).
|
|
96
|
+
Affirmative("Yes").
|
|
97
|
+
Negative("No").
|
|
98
|
+
Inline(true),
|
|
99
|
+
|
|
100
|
+
huh.NewConfirm().
|
|
101
|
+
Title("Enable Cross-Region Inference? ").
|
|
102
|
+
Value(&config.UseCrossRegionInference).
|
|
103
|
+
Affirmative("Yes").
|
|
104
|
+
Negative("No").
|
|
105
|
+
Inline(true),
|
|
106
|
+
|
|
107
|
+
huh.NewConfirm().
|
|
108
|
+
Title("Use Global Inference Endpoint? ").
|
|
109
|
+
Value(&config.UseGlobalInference).
|
|
110
|
+
Affirmative("Yes").
|
|
111
|
+
Negative("No").
|
|
112
|
+
Inline(true),
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if err := configForm.Run(); err != nil {
|
|
117
|
+
return nil, fmt.Errorf("failed to get Bedrock configuration: %w", err)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Trim whitespace from string fields
|
|
121
|
+
config.Profile = strings.TrimSpace(config.Profile)
|
|
122
|
+
config.Region = strings.TrimSpace(config.Region)
|
|
123
|
+
config.Endpoint = strings.TrimSpace(config.Endpoint)
|
|
124
|
+
|
|
125
|
+
return config, nil
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ApplyBedrockConfig applies Bedrock configuration using partial updates (profile-only)
|
|
129
|
+
func ApplyBedrockConfig(ctx context.Context, manager *task.Manager, config *BedrockConfig, modelID string, modelInfo interface{}) error {
|
|
130
|
+
// Build the API configuration with all Bedrock fields
|
|
131
|
+
apiConfig := &cline.ModelsApiConfiguration{}
|
|
132
|
+
|
|
133
|
+
// Set model ID fields
|
|
134
|
+
apiConfig.PlanModeApiModelId = proto.String(modelID)
|
|
135
|
+
apiConfig.ActModeApiModelId = proto.String(modelID)
|
|
136
|
+
apiConfig.PlanModeAwsBedrockCustomModelBaseId = proto.String(modelID)
|
|
137
|
+
apiConfig.ActModeAwsBedrockCustomModelBaseId = proto.String(modelID)
|
|
138
|
+
|
|
139
|
+
// Set profile authentication fields (always required)
|
|
140
|
+
optionalFields := &BedrockOptionalFields{}
|
|
141
|
+
optionalFields.Authentication = proto.String("profile")
|
|
142
|
+
optionalFields.UseProfile = proto.Bool(true)
|
|
143
|
+
optionalFields.Region = proto.String(config.Region)
|
|
144
|
+
|
|
145
|
+
// Set profile name (can be empty for default profile)
|
|
146
|
+
if config.Profile != "" {
|
|
147
|
+
optionalFields.Profile = proto.String(config.Profile)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Set optional fields if provided
|
|
151
|
+
if config.Endpoint != "" {
|
|
152
|
+
optionalFields.Endpoint = proto.String(config.Endpoint)
|
|
153
|
+
}
|
|
154
|
+
if config.UseCrossRegionInference {
|
|
155
|
+
optionalFields.UseCrossRegionInference = proto.Bool(true)
|
|
156
|
+
}
|
|
157
|
+
if config.UseGlobalInference {
|
|
158
|
+
optionalFields.UseGlobalInference = proto.Bool(true)
|
|
159
|
+
}
|
|
160
|
+
if config.UsePromptCache {
|
|
161
|
+
optionalFields.UsePromptCache = proto.Bool(true)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Apply all fields to the config
|
|
165
|
+
setBedrockOptionalFields(apiConfig, optionalFields)
|
|
166
|
+
|
|
167
|
+
// Build field mask including all fields we're setting (excluding access keys)
|
|
168
|
+
fieldPaths := []string{
|
|
169
|
+
"planModeApiModelId",
|
|
170
|
+
"actModeApiModelId",
|
|
171
|
+
"planModeAwsBedrockCustomModelBaseId",
|
|
172
|
+
"actModeAwsBedrockCustomModelBaseId",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Add profile authentication field paths
|
|
176
|
+
optionalPaths := buildBedrockOptionalFieldMask(optionalFields)
|
|
177
|
+
fieldPaths = append(fieldPaths, optionalPaths...)
|
|
178
|
+
|
|
179
|
+
// Create field mask
|
|
180
|
+
fieldMask := &fieldmaskpb.FieldMask{Paths: fieldPaths}
|
|
181
|
+
|
|
182
|
+
// Apply the partial update
|
|
183
|
+
request := &cline.UpdateApiConfigurationPartialRequest{
|
|
184
|
+
ApiConfiguration: apiConfig,
|
|
185
|
+
UpdateMask: fieldMask,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
|
|
189
|
+
return fmt.Errorf("failed to apply Bedrock configuration: %w", err)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return nil
|
|
193
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
package auth
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"strings"
|
|
8
|
+
"sync"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/charmbracelet/huh"
|
|
12
|
+
"github.com/cline/cli/pkg/cli/global"
|
|
13
|
+
"github.com/cline/cli/pkg/cli/task"
|
|
14
|
+
"github.com/cline/grpc-go/cline"
|
|
15
|
+
"google.golang.org/protobuf/proto"
|
|
16
|
+
"google.golang.org/protobuf/types/known/fieldmaskpb"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// OcaConfig holds Oracle Code Assist (OCA) configuration fields
|
|
20
|
+
type OcaConfig struct {
|
|
21
|
+
BaseURL string
|
|
22
|
+
Mode string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// PromptForOcaConfig displays a form for OCA configuration (base URL and mode)
|
|
26
|
+
func PromptForOcaConfig(ctx context.Context, manager *task.Manager) (*OcaConfig, error) {
|
|
27
|
+
config := &OcaConfig{}
|
|
28
|
+
var mode string
|
|
29
|
+
|
|
30
|
+
// Collect optional settings
|
|
31
|
+
configForm := huh.NewForm(
|
|
32
|
+
huh.NewGroup(
|
|
33
|
+
huh.NewInput().
|
|
34
|
+
Title("Base URL").
|
|
35
|
+
Value(&config.BaseURL).
|
|
36
|
+
Description("Leave empty to use default Base URL"),
|
|
37
|
+
|
|
38
|
+
huh.NewSelect[string]().
|
|
39
|
+
Title("Choose OCA mode (used for authentication)").
|
|
40
|
+
Description("Select 'Internal' to use Cline's internal OCA, or 'External' for your own OCA instance").
|
|
41
|
+
Options(
|
|
42
|
+
huh.NewOption("Internal", "internal"),
|
|
43
|
+
huh.NewOption("External", "external"),
|
|
44
|
+
).
|
|
45
|
+
Value(&mode),
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if err := configForm.Run(); err != nil {
|
|
50
|
+
return nil, fmt.Errorf("failed to get OCA configuration: %w", err)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Trim whitespace from string fields
|
|
54
|
+
config.BaseURL = strings.TrimSpace(config.BaseURL)
|
|
55
|
+
config.Mode = strings.TrimSpace(mode)
|
|
56
|
+
|
|
57
|
+
return config, nil
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ApplyOcaConfig applies OCA configuration using partial updates
|
|
61
|
+
func ApplyOcaConfig(ctx context.Context, manager *task.Manager, config *OcaConfig) error {
|
|
62
|
+
// Build the API configuration with all OCA fields
|
|
63
|
+
apiConfig := &cline.ModelsApiConfiguration{}
|
|
64
|
+
|
|
65
|
+
// Set profile authentication fields (always required)
|
|
66
|
+
optionalFields := &OcaOptionalFields{}
|
|
67
|
+
|
|
68
|
+
// Set profile name (can be empty for default profile)
|
|
69
|
+
if config.BaseURL != "" {
|
|
70
|
+
optionalFields.BaseURL = proto.String(config.BaseURL)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Set optional fields if provided
|
|
74
|
+
if config.Mode != "" {
|
|
75
|
+
optionalFields.Mode = proto.String(config.Mode)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Apply all fields to the config
|
|
79
|
+
setOcaOptionalFields(apiConfig, optionalFields)
|
|
80
|
+
|
|
81
|
+
// Add profile authentication field paths
|
|
82
|
+
optionalPaths := buildOcaOptionalFieldMask(optionalFields)
|
|
83
|
+
|
|
84
|
+
// Create field mask
|
|
85
|
+
fieldMask := &fieldmaskpb.FieldMask{Paths: optionalPaths}
|
|
86
|
+
|
|
87
|
+
// Apply the partial update
|
|
88
|
+
request := &cline.UpdateApiConfigurationPartialRequest{
|
|
89
|
+
ApiConfiguration: apiConfig,
|
|
90
|
+
UpdateMask: fieldMask,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if err := updateApiConfigurationPartial(ctx, manager, request); err != nil {
|
|
94
|
+
return fmt.Errorf("failed to apply OCA configuration: %w", err)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return nil
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ===========================
|
|
101
|
+
// OCA Auth Listener Singleton
|
|
102
|
+
// ===========================
|
|
103
|
+
|
|
104
|
+
type ocaAuthStream interface {
|
|
105
|
+
Recv() (*cline.OcaAuthState, error)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// OcaAuthStatusListener manages subscription to OCA auth status updates
|
|
109
|
+
type OcaAuthStatusListener struct {
|
|
110
|
+
stream ocaAuthStream
|
|
111
|
+
updatesCh chan *cline.OcaAuthState
|
|
112
|
+
errCh chan error
|
|
113
|
+
ctx context.Context
|
|
114
|
+
cancel context.CancelFunc
|
|
115
|
+
mu sync.RWMutex
|
|
116
|
+
lastState *cline.OcaAuthState
|
|
117
|
+
firstEventCh chan struct{}
|
|
118
|
+
firstEventOnce sync.Once
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// NewOcaAuthStatusListener creates a new OCA auth status listener
|
|
122
|
+
func NewOcaAuthStatusListener(parentCtx context.Context) (*OcaAuthStatusListener, error) {
|
|
123
|
+
client, err := global.GetDefaultClient(parentCtx)
|
|
124
|
+
if err != nil {
|
|
125
|
+
return nil, fmt.Errorf("failed to get client: %w", err)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Keep the listener alive independently of short-lived caller contexts
|
|
129
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
130
|
+
|
|
131
|
+
// Subscribe to OCA auth status updates
|
|
132
|
+
stream, err := client.Ocaaccount.OcaSubscribeToAuthStatusUpdate(ctx, &cline.EmptyRequest{})
|
|
133
|
+
if err != nil {
|
|
134
|
+
cancel()
|
|
135
|
+
return nil, fmt.Errorf("failed to subscribe to OCA auth updates: %w", err)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return &OcaAuthStatusListener{
|
|
139
|
+
stream: stream,
|
|
140
|
+
updatesCh: make(chan *cline.OcaAuthState, 10),
|
|
141
|
+
errCh: make(chan error, 1),
|
|
142
|
+
ctx: ctx,
|
|
143
|
+
cancel: cancel,
|
|
144
|
+
firstEventCh: make(chan struct{}),
|
|
145
|
+
}, nil
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Start begins listening to the auth status update stream
|
|
149
|
+
func (l *OcaAuthStatusListener) Start() error {
|
|
150
|
+
go l.readStream()
|
|
151
|
+
return nil
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func (l *OcaAuthStatusListener) readStream() {
|
|
155
|
+
defer close(l.updatesCh)
|
|
156
|
+
defer close(l.errCh)
|
|
157
|
+
|
|
158
|
+
for {
|
|
159
|
+
select {
|
|
160
|
+
case <-l.ctx.Done():
|
|
161
|
+
return
|
|
162
|
+
default:
|
|
163
|
+
state, err := l.stream.Recv()
|
|
164
|
+
if err != nil {
|
|
165
|
+
// Propagate error and exit
|
|
166
|
+
if err == io.EOF {
|
|
167
|
+
// Treat as error to notify waiters
|
|
168
|
+
err = fmt.Errorf("OCA auth status stream closed")
|
|
169
|
+
}
|
|
170
|
+
select {
|
|
171
|
+
case l.errCh <- err:
|
|
172
|
+
case <-l.ctx.Done():
|
|
173
|
+
}
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
l.mu.Lock()
|
|
178
|
+
l.lastState = state
|
|
179
|
+
l.mu.Unlock()
|
|
180
|
+
|
|
181
|
+
// Notify first event waiters
|
|
182
|
+
l.firstEventOnce.Do(func() { close(l.firstEventCh) })
|
|
183
|
+
|
|
184
|
+
select {
|
|
185
|
+
case l.updatesCh <- state:
|
|
186
|
+
case <-l.ctx.Done():
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// WaitForFirstEvent blocks until the first event is received or timeout occurs
|
|
194
|
+
func (l *OcaAuthStatusListener) WaitForFirstEvent(timeout time.Duration) error {
|
|
195
|
+
// Fast-path if already have a state
|
|
196
|
+
l.mu.RLock()
|
|
197
|
+
ready := l.lastState != nil
|
|
198
|
+
l.mu.RUnlock()
|
|
199
|
+
if ready {
|
|
200
|
+
return nil
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
timer := time.NewTimer(timeout)
|
|
204
|
+
defer timer.Stop()
|
|
205
|
+
|
|
206
|
+
select {
|
|
207
|
+
case <-l.firstEventCh:
|
|
208
|
+
return nil
|
|
209
|
+
case <-timer.C:
|
|
210
|
+
return fmt.Errorf("timeout waiting for initial OCA auth event")
|
|
211
|
+
case <-l.ctx.Done():
|
|
212
|
+
return fmt.Errorf("OCA auth listener cancelled")
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// IsAuthenticated returns true if the last known OCA auth state is authenticated
|
|
217
|
+
func (l *OcaAuthStatusListener) IsAuthenticated() bool {
|
|
218
|
+
l.mu.RLock()
|
|
219
|
+
defer l.mu.RUnlock()
|
|
220
|
+
return isOCAStateAuthenticated(l.lastState)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// WaitForAuthentication waits until OCA authentication succeeds or timeout occurs
|
|
224
|
+
func (l *OcaAuthStatusListener) WaitForAuthentication(timeout time.Duration) error {
|
|
225
|
+
timer := time.NewTimer(timeout)
|
|
226
|
+
defer timer.Stop()
|
|
227
|
+
|
|
228
|
+
// If already authenticated, return immediately
|
|
229
|
+
if l.IsAuthenticated() {
|
|
230
|
+
return nil
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for {
|
|
234
|
+
select {
|
|
235
|
+
case <-timer.C:
|
|
236
|
+
return fmt.Errorf("OCA authentication timeout after %v - please try again", timeout)
|
|
237
|
+
case <-l.ctx.Done():
|
|
238
|
+
return fmt.Errorf("OCA authentication cancelled")
|
|
239
|
+
case err := <-l.errCh:
|
|
240
|
+
return fmt.Errorf("OCA authentication stream error: %w", err)
|
|
241
|
+
case state := <-l.updatesCh:
|
|
242
|
+
if isOCAStateAuthenticated(state) {
|
|
243
|
+
return nil
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Stop closes the stream and cleans up resources
|
|
250
|
+
func (l *OcaAuthStatusListener) Stop() {
|
|
251
|
+
l.cancel()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
func isOCAStateAuthenticated(state *cline.OcaAuthState) bool {
|
|
255
|
+
return state != nil && state.User != nil
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Singleton holder
|
|
259
|
+
var (
|
|
260
|
+
ocaListener *OcaAuthStatusListener
|
|
261
|
+
ocaListenerOnce sync.Once
|
|
262
|
+
ocaListenerErr error
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
// GetOcaAuthListener returns the OCA auth listener singleton
|
|
266
|
+
func GetOcaAuthListener(ctx context.Context) (*OcaAuthStatusListener, error) {
|
|
267
|
+
// Allow optional ctx: if nil, use context.TODO(). If already initialized, return singleton.
|
|
268
|
+
if ctx == nil {
|
|
269
|
+
ctx = context.TODO()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
ocaListenerOnce.Do(func() {
|
|
273
|
+
l, err := NewOcaAuthStatusListener(ctx)
|
|
274
|
+
if err != nil {
|
|
275
|
+
ocaListenerErr = err
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
if err := l.Start(); err != nil {
|
|
279
|
+
ocaListenerErr = err
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
ocaListener = l
|
|
283
|
+
})
|
|
284
|
+
return ocaListener, ocaListenerErr
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// IsOCAAuthenticated returns true if the global OCA auth status is authenticated.
|
|
288
|
+
// It attempts a brief wait for the first event to avoid stale reads.
|
|
289
|
+
func IsOCAAuthenticated(ctx context.Context) bool {
|
|
290
|
+
l, err := GetOcaAuthListener(ctx)
|
|
291
|
+
if err != nil {
|
|
292
|
+
return false
|
|
293
|
+
}
|
|
294
|
+
_ = l.WaitForFirstEvent(1 * time.Second) // best-effort
|
|
295
|
+
return l.IsAuthenticated()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// LatestState returns the last received OCA auth state (may be nil)
|
|
299
|
+
func (l *OcaAuthStatusListener) LatestState() *cline.OcaAuthState {
|
|
300
|
+
l.mu.RLock()
|
|
301
|
+
defer l.mu.RUnlock()
|
|
302
|
+
return l.lastState
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// GetLatestOCAState returns the latest known OCA auth state, optionally waiting for the first event
|
|
306
|
+
func GetLatestOCAState(ctx context.Context, timeout time.Duration) (*cline.OcaAuthState, error) {
|
|
307
|
+
l, err := GetOcaAuthListener(ctx)
|
|
308
|
+
if err != nil {
|
|
309
|
+
return nil, err
|
|
310
|
+
}
|
|
311
|
+
if timeout > 0 {
|
|
312
|
+
if err := l.WaitForFirstEvent(timeout); err != nil {
|
|
313
|
+
return nil, err
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return l.LatestState(), nil
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ensureOcaAuthenticated initiates OCA login (if needed) and waits for success using the singleton listener
|
|
320
|
+
func ensureOcaAuthenticated(ctx context.Context) error {
|
|
321
|
+
// Ensure listener exists
|
|
322
|
+
listener, err := GetOcaAuthListener(ctx)
|
|
323
|
+
if err != nil {
|
|
324
|
+
return fmt.Errorf("failed to initialize OCA auth listener: %w", err)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Briefly wait for first event to know current state
|
|
328
|
+
_ = listener.WaitForFirstEvent(1 * time.Second)
|
|
329
|
+
|
|
330
|
+
// If already authenticated, nothing to do
|
|
331
|
+
if listener.IsAuthenticated() {
|
|
332
|
+
fmt.Println("✓ OCA authentication already active.")
|
|
333
|
+
return nil
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Create gRPC client for initiating login
|
|
337
|
+
client, err := global.GetDefaultClient(ctx)
|
|
338
|
+
if err != nil {
|
|
339
|
+
return fmt.Errorf("failed to obtain client: %w", err)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Start login and wait for authentication
|
|
343
|
+
waitCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
|
344
|
+
defer cancel()
|
|
345
|
+
|
|
346
|
+
// Initiate login (opens the browser with a callback URL from Cline Core)
|
|
347
|
+
response, err := client.Ocaaccount.OcaAccountLoginClicked(waitCtx, &cline.EmptyRequest{})
|
|
348
|
+
if err != nil {
|
|
349
|
+
return fmt.Errorf("failed to initiate OCA login: %w", err)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
fmt.Println("\nOpening browser for OCA authentication...")
|
|
353
|
+
if response != nil && response.Value != "" {
|
|
354
|
+
fmt.Printf("If the browser doesn't open automatically, visit this URL:\n%s\n\n", response.Value)
|
|
355
|
+
}
|
|
356
|
+
fmt.Println("Waiting for you to complete OCA authentication in your browser...")
|
|
357
|
+
fmt.Println("(This may take a few moments. Timeout: 5 minutes)")
|
|
358
|
+
|
|
359
|
+
// Block until authenticated or timeout
|
|
360
|
+
if err := listener.WaitForAuthentication(5 * time.Minute); err != nil {
|
|
361
|
+
return err
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fmt.Println("✓ OCA authentication successful!")
|
|
365
|
+
return nil
|
|
366
|
+
}
|
package/pkg/cli/auth.go
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package cli
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"github.com/cline/cli/pkg/cli/auth"
|
|
5
|
+
"github.com/spf13/cobra"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func NewAuthCommand() *cobra.Command {
|
|
9
|
+
cmd := &cobra.Command{
|
|
10
|
+
Use: "auth",
|
|
11
|
+
Short: "Authenticate a provider and configure what model is used",
|
|
12
|
+
Long: `Authenticate a provider and configure what model is used
|
|
13
|
+
|
|
14
|
+
Interactive Mode:
|
|
15
|
+
Run without flags to open an interactive menu where you can:
|
|
16
|
+
- Sign in to your Cline account
|
|
17
|
+
- Configure other LLM providers (Anthropic, OpenAI, etc.)
|
|
18
|
+
- Select and switch between AI models
|
|
19
|
+
- Manage provider settings
|
|
20
|
+
|
|
21
|
+
Quick Setup Mode:
|
|
22
|
+
Use flags to quickly configure a BYO provider non-interactively:
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
cline auth --provider openai-native --apikey sk-xxx --modelid gpt-5
|
|
26
|
+
cline auth -p anthropic -k sk-ant-xxx -m claude-sonnet-4-5-20250929
|
|
27
|
+
cline auth -p openai-compatible -k xxx -m gpt-4 -b https://api.example.com/v1
|
|
28
|
+
|
|
29
|
+
Supported providers: openai-native, openai, anthropic, gemini, openrouter, xai, cerebras, ollama
|
|
30
|
+
Note: Bedrock provider requires interactive setup due to complex auth fields`,
|
|
31
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
32
|
+
return auth.RunAuthFlow(cmd.Context(), args)
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Add flags for quick setup mode
|
|
37
|
+
cmd.Flags().StringVarP(&auth.QuickProvider, "provider", "p", "", "Provider ID for quick setup (e.g., openai-native, anthropic)")
|
|
38
|
+
cmd.Flags().StringVarP(&auth.QuickAPIKey, "apikey", "k", "", "API key for the provider")
|
|
39
|
+
cmd.Flags().StringVarP(&auth.QuickModelID, "modelid", "m", "", "Model ID to configure (e.g., gpt-4o, claude-sonnet-4-5-20250929)")
|
|
40
|
+
cmd.Flags().StringVarP(&auth.QuickBaseURL, "baseurl", "b", "", "Base URL (optional, only for openai provider)")
|
|
41
|
+
|
|
42
|
+
return cmd
|
|
43
|
+
}
|