@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,187 @@
|
|
|
1
|
+
package clerror
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// ClineErrorType represents the category of error
|
|
10
|
+
type ClineErrorType string
|
|
11
|
+
|
|
12
|
+
const (
|
|
13
|
+
ErrorTypeAuth ClineErrorType = "auth"
|
|
14
|
+
ErrorTypeNetwork ClineErrorType = "network"
|
|
15
|
+
ErrorTypeRateLimit ClineErrorType = "rateLimit"
|
|
16
|
+
ErrorTypeBalance ClineErrorType = "balance"
|
|
17
|
+
ErrorTypeUnknown ClineErrorType = "unknown"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// ClineError represents a parsed error from Cline API
|
|
21
|
+
type ClineError struct {
|
|
22
|
+
Message string `json:"message"`
|
|
23
|
+
Status int `json:"status"`
|
|
24
|
+
RequestID string `json:"request_id"`
|
|
25
|
+
Code interface{} `json:"code"` // Can be string or int
|
|
26
|
+
ModelID string `json:"modelId"`
|
|
27
|
+
ProviderID string `json:"providerId"`
|
|
28
|
+
Details map[string]interface{} `json:"details"`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// GetCodeString returns the code as a string regardless of its type
|
|
32
|
+
func (e *ClineError) GetCodeString() string {
|
|
33
|
+
if e == nil || e.Code == nil {
|
|
34
|
+
return ""
|
|
35
|
+
}
|
|
36
|
+
switch v := e.Code.(type) {
|
|
37
|
+
case string:
|
|
38
|
+
return v
|
|
39
|
+
case float64:
|
|
40
|
+
return fmt.Sprintf("%.0f", v)
|
|
41
|
+
case int:
|
|
42
|
+
return fmt.Sprintf("%d", v)
|
|
43
|
+
default:
|
|
44
|
+
return fmt.Sprintf("%v", v)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Rate limit patterns from webview
|
|
49
|
+
var rateLimitPatterns = []string{
|
|
50
|
+
"status code 429",
|
|
51
|
+
"rate limit",
|
|
52
|
+
"too many requests",
|
|
53
|
+
"quota exceeded",
|
|
54
|
+
"resource exhausted",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ParseClineError parses a JSON error string into a ClineError
|
|
58
|
+
func ParseClineError(errorJSON string) (*ClineError, error) {
|
|
59
|
+
if errorJSON == "" {
|
|
60
|
+
return nil, nil
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
var err ClineError
|
|
64
|
+
if parseErr := json.Unmarshal([]byte(errorJSON), &err); parseErr != nil {
|
|
65
|
+
// If JSON parsing fails, create a simple error with the message
|
|
66
|
+
return &ClineError{
|
|
67
|
+
Message: errorJSON,
|
|
68
|
+
}, nil
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return &err, nil
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// GetErrorType determines the type of error based on code, status, and message
|
|
75
|
+
func (e *ClineError) GetErrorType() ClineErrorType {
|
|
76
|
+
if e == nil {
|
|
77
|
+
return ErrorTypeUnknown
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check balance error first (most specific)
|
|
81
|
+
codeStr := e.GetCodeString()
|
|
82
|
+
if codeStr == "insufficient_credits" {
|
|
83
|
+
return ErrorTypeBalance
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check auth errors
|
|
87
|
+
if codeStr == "ERR_BAD_REQUEST" || e.Status == 401 {
|
|
88
|
+
return ErrorTypeAuth
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check for auth message
|
|
92
|
+
if strings.Contains(e.Message, "Authentication required") ||
|
|
93
|
+
strings.Contains(e.Message, "Invalid API key") ||
|
|
94
|
+
strings.Contains(e.Message, "Unauthorized") {
|
|
95
|
+
return ErrorTypeAuth
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check rate limit patterns
|
|
99
|
+
messageLower := strings.ToLower(e.Message)
|
|
100
|
+
for _, pattern := range rateLimitPatterns {
|
|
101
|
+
if strings.Contains(messageLower, pattern) {
|
|
102
|
+
return ErrorTypeRateLimit
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return ErrorTypeUnknown
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// IsBalanceError returns true if this is a balance/credits error
|
|
110
|
+
func (e *ClineError) IsBalanceError() bool {
|
|
111
|
+
return e.GetErrorType() == ErrorTypeBalance
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// IsAuthError returns true if this is an authentication error
|
|
115
|
+
func (e *ClineError) IsAuthError() bool {
|
|
116
|
+
return e.GetErrorType() == ErrorTypeAuth
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// IsRateLimitError returns true if this is a rate limit error
|
|
120
|
+
func (e *ClineError) IsRateLimitError() bool {
|
|
121
|
+
return e.GetErrorType() == ErrorTypeRateLimit
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// GetCurrentBalance returns the current balance if available
|
|
125
|
+
func (e *ClineError) GetCurrentBalance() *float64 {
|
|
126
|
+
if e == nil || e.Details == nil {
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if balance, ok := e.Details["current_balance"].(float64); ok {
|
|
131
|
+
return &balance
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return nil
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// GetBuyCreditsURL returns the URL to buy credits if available
|
|
138
|
+
func (e *ClineError) GetBuyCreditsURL() string {
|
|
139
|
+
if e == nil || e.Details == nil {
|
|
140
|
+
return ""
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if url, ok := e.Details["buy_credits_url"].(string); ok {
|
|
144
|
+
return url
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return ""
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// GetTotalSpent returns the total spent amount if available
|
|
151
|
+
func (e *ClineError) GetTotalSpent() *float64 {
|
|
152
|
+
if e == nil || e.Details == nil {
|
|
153
|
+
return nil
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if spent, ok := e.Details["total_spent"].(float64); ok {
|
|
157
|
+
return &spent
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return nil
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// GetTotalPromotions returns the total promotions amount if available
|
|
164
|
+
func (e *ClineError) GetTotalPromotions() *float64 {
|
|
165
|
+
if e == nil || e.Details == nil {
|
|
166
|
+
return nil
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if promos, ok := e.Details["total_promotions"].(float64); ok {
|
|
170
|
+
return &promos
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return nil
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// GetDetailMessage returns the detail message from error.details if available
|
|
177
|
+
func (e *ClineError) GetDetailMessage() string {
|
|
178
|
+
if e == nil || e.Details == nil {
|
|
179
|
+
return ""
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if msg, ok := e.Details["message"].(string); ok {
|
|
183
|
+
return msg
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return ""
|
|
187
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
package config
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
"github.com/cline/cli/pkg/cli/global"
|
|
10
|
+
"github.com/cline/grpc-go/client"
|
|
11
|
+
"github.com/cline/grpc-go/cline"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
type Manager struct {
|
|
15
|
+
client *client.ClineClient
|
|
16
|
+
clientAddress string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func NewManager(ctx context.Context, address string) (*Manager, error) {
|
|
20
|
+
var c *client.ClineClient
|
|
21
|
+
var err error
|
|
22
|
+
|
|
23
|
+
if address != "" {
|
|
24
|
+
c, err = global.GetClientForAddress(ctx, address)
|
|
25
|
+
} else {
|
|
26
|
+
c, err = global.GetDefaultClient(ctx)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if err != nil {
|
|
30
|
+
return nil, fmt.Errorf("failed to get client: %w", err)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Get the actual address being used
|
|
34
|
+
clientAddress := address
|
|
35
|
+
if address == "" && global.Clients != nil {
|
|
36
|
+
clientAddress = global.Clients.GetRegistry().GetDefaultInstance()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return &Manager{
|
|
40
|
+
client: c,
|
|
41
|
+
clientAddress: clientAddress,
|
|
42
|
+
}, nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// GetCurrentInstance returns the address of the current instance
|
|
46
|
+
func (m *Manager) GetCurrentInstance() string {
|
|
47
|
+
return m.clientAddress
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func (m *Manager) UpdateSettings(ctx context.Context, settings *cline.Settings, secrets *cline.Secrets) error {
|
|
51
|
+
request := &cline.UpdateSettingsRequestCli{
|
|
52
|
+
Metadata: &cline.Metadata{},
|
|
53
|
+
Settings: settings,
|
|
54
|
+
Secrets: secrets,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Call the updateSettingsCli RPC
|
|
58
|
+
_, err := m.client.State.UpdateSettingsCli(ctx, request)
|
|
59
|
+
if err != nil {
|
|
60
|
+
return fmt.Errorf("failed to update settings: %w", err)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fmt.Println("Settings updated successfully")
|
|
64
|
+
fmt.Printf("Instance: %s\n", m.clientAddress)
|
|
65
|
+
return nil
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func (m *Manager) GetState(ctx context.Context) (map[string]interface{}, error) {
|
|
69
|
+
state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{})
|
|
70
|
+
if err != nil {
|
|
71
|
+
return nil, fmt.Errorf("failed to get state: %w", err)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var stateData map[string]interface{}
|
|
75
|
+
if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil {
|
|
76
|
+
return nil, fmt.Errorf("failed to parse state: %w", err)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return stateData, nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func (m *Manager) ListSettings(ctx context.Context) error {
|
|
83
|
+
// Get state
|
|
84
|
+
stateData, err := m.GetState(ctx)
|
|
85
|
+
if err != nil {
|
|
86
|
+
return err
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Subset of fields we will print the values for
|
|
90
|
+
settingsFields := []string{
|
|
91
|
+
"apiConfiguration",
|
|
92
|
+
"telemetrySetting",
|
|
93
|
+
"planActSeparateModelsSetting",
|
|
94
|
+
"enableCheckpointsSetting",
|
|
95
|
+
"mcpMarketplaceEnabled",
|
|
96
|
+
"shellIntegrationTimeout",
|
|
97
|
+
"terminalReuseEnabled",
|
|
98
|
+
"mcpResponsesCollapsed",
|
|
99
|
+
"mcpDisplayMode",
|
|
100
|
+
"terminalOutputLineLimit",
|
|
101
|
+
"mode",
|
|
102
|
+
"preferredLanguage",
|
|
103
|
+
"openaiReasoningEffort",
|
|
104
|
+
"strictPlanModeEnabled",
|
|
105
|
+
"focusChainSettings",
|
|
106
|
+
"useAutoCondense",
|
|
107
|
+
"customPrompt",
|
|
108
|
+
"browserSettings",
|
|
109
|
+
"defaultTerminalProfile",
|
|
110
|
+
"yoloModeToggled",
|
|
111
|
+
"dictationSettings",
|
|
112
|
+
"autoCondenseThreshold",
|
|
113
|
+
"autoApprovalSettings",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Render each field using the renderer
|
|
117
|
+
for _, field := range settingsFields {
|
|
118
|
+
if value, ok := stateData[field]; ok {
|
|
119
|
+
if err := RenderField(field, value, true); err != nil {
|
|
120
|
+
fmt.Printf("Error rendering %s: %v\n", field, err)
|
|
121
|
+
}
|
|
122
|
+
fmt.Println()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return nil
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func (m *Manager) GetSetting(ctx context.Context, key string) error {
|
|
130
|
+
// Get state
|
|
131
|
+
stateData, err := m.GetState(ctx)
|
|
132
|
+
if err != nil {
|
|
133
|
+
return err
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Convert kebab-case to camelCase path
|
|
137
|
+
parts := kebabToCamelPath(key)
|
|
138
|
+
rootField := parts[0]
|
|
139
|
+
|
|
140
|
+
// Get the value
|
|
141
|
+
value, found := getNestedValue(stateData, parts)
|
|
142
|
+
if !found {
|
|
143
|
+
return fmt.Errorf("setting '%s' not found", key)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Render the value
|
|
147
|
+
if len(parts) == 1 {
|
|
148
|
+
// Top-level field: use RenderField for nice formatting
|
|
149
|
+
return RenderField(rootField, value, false)
|
|
150
|
+
} else {
|
|
151
|
+
// Nested field: simple print
|
|
152
|
+
fmt.Printf("%s: %s\n", key, formatValue(value, rootField, true))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return nil
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// kebabToCamelPath converts a kebab-case path to camelCase
|
|
159
|
+
// e.g., "auto-approval-settings.actions.read-files" -> "autoApprovalSettings.actions.readFiles"
|
|
160
|
+
func kebabToCamelPath(path string) []string {
|
|
161
|
+
parts := strings.Split(path, ".")
|
|
162
|
+
for i, part := range parts {
|
|
163
|
+
parts[i] = kebabToCamel(part)
|
|
164
|
+
}
|
|
165
|
+
return parts
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// kebabToCamel converts a single kebab-case string to camelCase
|
|
169
|
+
// e.g., "auto-approval-settings" -> "autoApprovalSettings"
|
|
170
|
+
func kebabToCamel(s string) string {
|
|
171
|
+
if s == "" {
|
|
172
|
+
return s
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
parts := strings.Split(s, "-")
|
|
176
|
+
if len(parts) == 1 {
|
|
177
|
+
return s
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// First part stays lowercase, rest are capitalized
|
|
181
|
+
result := parts[0]
|
|
182
|
+
for i := 1; i < len(parts); i++ {
|
|
183
|
+
if parts[i] != "" {
|
|
184
|
+
result += strings.ToUpper(parts[i][:1]) + parts[i][1:]
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// getNestedValue retrieves a value from a nested map using dot notation
|
|
191
|
+
// e.g., "autoApprovalSettings.actions.readFiles"
|
|
192
|
+
func getNestedValue(data map[string]interface{}, parts []string) (interface{}, bool) {
|
|
193
|
+
current := interface{}(data)
|
|
194
|
+
|
|
195
|
+
for _, part := range parts {
|
|
196
|
+
// Try to access as map
|
|
197
|
+
if m, ok := current.(map[string]interface{}); ok {
|
|
198
|
+
if val, exists := m[part]; exists {
|
|
199
|
+
current = val
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
return nil, false
|
|
203
|
+
}
|
|
204
|
+
return nil, false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return current, true
|
|
208
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
package config
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// sensitiveKeywords defines field name patterns that should be censored
|
|
9
|
+
var sensitiveKeywords = []string{"key", "secret", "password", "cline-account-id"}
|
|
10
|
+
|
|
11
|
+
// camelToKebab converts camelCase to kebab-case
|
|
12
|
+
// e.g., "autoApprovalSettings" -> "auto-approval-settings"
|
|
13
|
+
func camelToKebab(s string) string {
|
|
14
|
+
if s == "" {
|
|
15
|
+
return s
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var result []rune
|
|
19
|
+
for i, r := range s {
|
|
20
|
+
if i > 0 && r >= 'A' && r <= 'Z' {
|
|
21
|
+
result = append(result, '-')
|
|
22
|
+
}
|
|
23
|
+
result = append(result, r|32) // Convert to lowercase (works for A-Z)
|
|
24
|
+
}
|
|
25
|
+
return string(result)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// isSensitiveField checks if a field name contains sensitive keywords
|
|
29
|
+
func isSensitiveField(fieldName string) bool {
|
|
30
|
+
if fieldName == "" {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
lowerName := strings.ToLower(fieldName)
|
|
35
|
+
for _, keyword := range sensitiveKeywords {
|
|
36
|
+
if strings.Contains(lowerName, keyword) {
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// formatValue formats a value for display, handling empty strings and censoring sensitive fields
|
|
45
|
+
func formatValue(val interface{}, fieldName string, censor bool) string {
|
|
46
|
+
// Handle empty strings specifically
|
|
47
|
+
if str, ok := val.(string); ok && str == "" {
|
|
48
|
+
return "''"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if censor && isSensitiveField(fieldName) {
|
|
52
|
+
valStr := fmt.Sprintf("%v", val)
|
|
53
|
+
if valStr != "" && valStr != "''" {
|
|
54
|
+
return "********"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return fmt.Sprintf("%v", val)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// RenderField renders a single config field with proper formatting
|
|
62
|
+
func RenderField(key string, value interface{}, censor bool) error {
|
|
63
|
+
switch key {
|
|
64
|
+
// Nested objects - render with header + nested fields
|
|
65
|
+
case "apiConfiguration":
|
|
66
|
+
return renderApiConfiguration(value, censor)
|
|
67
|
+
case "browserSettings":
|
|
68
|
+
return renderBrowserSettings(value, censor)
|
|
69
|
+
case "focusChainSettings":
|
|
70
|
+
return renderFocusChainSettings(value, censor)
|
|
71
|
+
case "dictationSettings":
|
|
72
|
+
return renderDictationSettings(value, censor)
|
|
73
|
+
case "autoApprovalSettings":
|
|
74
|
+
return renderAutoApprovalSettings(value, censor)
|
|
75
|
+
|
|
76
|
+
// Simple values - just print key: value
|
|
77
|
+
case "mode", "telemetrySetting", "preferredLanguage", "customPrompt",
|
|
78
|
+
"defaultTerminalProfile", "mcpDisplayMode", "openaiReasoningEffort",
|
|
79
|
+
"planActSeparateModelsSetting", "enableCheckpointsSetting",
|
|
80
|
+
"mcpMarketplaceEnabled", "terminalReuseEnabled",
|
|
81
|
+
"mcpResponsesCollapsed", "strictPlanModeEnabled",
|
|
82
|
+
"useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout",
|
|
83
|
+
"terminalOutputLineLimit", "autoCondenseThreshold":
|
|
84
|
+
fmt.Printf("%s: %s\n", camelToKebab(key), formatValue(value, key, censor))
|
|
85
|
+
return nil
|
|
86
|
+
|
|
87
|
+
default:
|
|
88
|
+
return fmt.Errorf("unknown config field: %s", key)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// renderApiConfiguration renders the API configuration object
|
|
93
|
+
func renderApiConfiguration(value interface{}, censor bool) error {
|
|
94
|
+
fmt.Println("api-configuration:")
|
|
95
|
+
|
|
96
|
+
configMap, ok := value.(map[string]interface{})
|
|
97
|
+
if !ok {
|
|
98
|
+
return fmt.Errorf("invalid api-configuration format")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Print each field directly
|
|
102
|
+
for key, val := range configMap {
|
|
103
|
+
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return nil
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// renderBrowserSettings renders browser settings
|
|
110
|
+
func renderBrowserSettings(value interface{}, censor bool) error {
|
|
111
|
+
fmt.Println("browser-settings:")
|
|
112
|
+
|
|
113
|
+
settingsMap, ok := value.(map[string]interface{})
|
|
114
|
+
if !ok {
|
|
115
|
+
return fmt.Errorf("invalid browser-settings format")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handle nested viewport if present
|
|
119
|
+
if viewport, ok := settingsMap["viewport"].(map[string]interface{}); ok {
|
|
120
|
+
fmt.Println(" viewport:")
|
|
121
|
+
for key, val := range viewport {
|
|
122
|
+
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Print other fields
|
|
127
|
+
for key, val := range settingsMap {
|
|
128
|
+
if key != "viewport" {
|
|
129
|
+
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return nil
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// renderFocusChainSettings renders focus chain settings
|
|
137
|
+
func renderFocusChainSettings(value interface{}, censor bool) error {
|
|
138
|
+
fmt.Println("focus-chain-settings:")
|
|
139
|
+
|
|
140
|
+
settingsMap, ok := value.(map[string]interface{})
|
|
141
|
+
if !ok {
|
|
142
|
+
return fmt.Errorf("invalid focus-chain-settings format")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for key, val := range settingsMap {
|
|
146
|
+
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return nil
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// renderDictationSettings renders dictation settings
|
|
153
|
+
func renderDictationSettings(value interface{}, censor bool) error {
|
|
154
|
+
fmt.Println("dictation-settings:")
|
|
155
|
+
|
|
156
|
+
settingsMap, ok := value.(map[string]interface{})
|
|
157
|
+
if !ok {
|
|
158
|
+
return fmt.Errorf("invalid dictation-settings format")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for key, val := range settingsMap {
|
|
162
|
+
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return nil
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// renderAutoApprovalSettings renders auto approval settings
|
|
169
|
+
func renderAutoApprovalSettings(value interface{}, censor bool) error {
|
|
170
|
+
fmt.Println("auto-approval-settings:")
|
|
171
|
+
|
|
172
|
+
settingsMap, ok := value.(map[string]interface{})
|
|
173
|
+
if !ok {
|
|
174
|
+
return fmt.Errorf("invalid auto-approval-settings format")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Print top-level fields (skip version, handle actions specially)
|
|
178
|
+
for key, val := range settingsMap {
|
|
179
|
+
if key == "version" {
|
|
180
|
+
continue // Skip version
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if key == "actions" {
|
|
184
|
+
// Handle nested actions with double indentation
|
|
185
|
+
fmt.Println(" actions:")
|
|
186
|
+
if actionsMap, ok := val.(map[string]interface{}); ok {
|
|
187
|
+
for actionKey, actionVal := range actionsMap {
|
|
188
|
+
fmt.Printf(" %s: %s\n", camelToKebab(actionKey), formatValue(actionVal, actionKey, censor))
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// Print other fields normally (enabled, enableNotifications, favorites)
|
|
193
|
+
fmt.Printf(" %s: %s\n", camelToKebab(key), formatValue(val, key, censor))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return nil
|
|
198
|
+
}
|