@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,304 @@
|
|
|
1
|
+
package global
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"log"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/cline/cli/pkg/cli/sqlite"
|
|
12
|
+
"github.com/cline/cli/pkg/common"
|
|
13
|
+
"github.com/cline/grpc-go/client"
|
|
14
|
+
"github.com/cline/grpc-go/cline"
|
|
15
|
+
"github.com/cline/grpc-go/host"
|
|
16
|
+
"google.golang.org/grpc"
|
|
17
|
+
"google.golang.org/grpc/credentials/insecure"
|
|
18
|
+
"google.golang.org/grpc/health/grpc_health_v1"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// ClientRegistry manages Cline client connections using direct SQLite operations
|
|
22
|
+
type ClientRegistry struct {
|
|
23
|
+
lockManager *sqlite.LockManager
|
|
24
|
+
configPath string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// NewClientRegistry creates a new client registry
|
|
28
|
+
func NewClientRegistry(configPath string) *ClientRegistry {
|
|
29
|
+
lockManager, err := sqlite.NewLockManager(configPath)
|
|
30
|
+
if err != nil {
|
|
31
|
+
// Log error but continue - we can still function without SQLite
|
|
32
|
+
log.Fatalf("Warning: Failed to initialize SQLite lock manager: %v\n", err)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return &ClientRegistry{
|
|
36
|
+
lockManager: lockManager,
|
|
37
|
+
configPath: configPath,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// GetDefaultInstance returns the default instance address from settings file
|
|
42
|
+
func (r *ClientRegistry) GetDefaultInstance() string {
|
|
43
|
+
defaultAddr, err := sqlite.GetDefaultInstance(r.configPath)
|
|
44
|
+
if err != nil {
|
|
45
|
+
return ""
|
|
46
|
+
}
|
|
47
|
+
return defaultAddr
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// SetDefaultInstance sets the default instance (writes default.json)
|
|
51
|
+
func (r *ClientRegistry) SetDefaultInstance(address string) error {
|
|
52
|
+
// Verify the instance exists in SQLite
|
|
53
|
+
if r.lockManager != nil {
|
|
54
|
+
exists, err := r.lockManager.HasInstanceAtAddress(address)
|
|
55
|
+
if err != nil {
|
|
56
|
+
return fmt.Errorf("failed to check instance existence: %w", err)
|
|
57
|
+
}
|
|
58
|
+
if !exists {
|
|
59
|
+
return fmt.Errorf("instance %s not found in registry", address)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return sqlite.SetDefaultInstance(r.configPath, address)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// GetInstance returns instance information directly from SQLite
|
|
67
|
+
func (r *ClientRegistry) GetInstance(address string) (*common.CoreInstanceInfo, error) {
|
|
68
|
+
if r.lockManager == nil {
|
|
69
|
+
return nil, fmt.Errorf("lock manager not available")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return r.lockManager.GetInstanceInfo(address)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// GetClient returns a connected client for the given address (created on-demand)
|
|
76
|
+
func (r *ClientRegistry) GetClient(ctx context.Context, address string) (*client.ClineClient, error) {
|
|
77
|
+
// Verify instance exists in SQLite
|
|
78
|
+
if r.lockManager != nil {
|
|
79
|
+
exists, err := r.lockManager.HasInstanceAtAddress(address)
|
|
80
|
+
if err != nil {
|
|
81
|
+
return nil, fmt.Errorf("failed to check instance existence: %w", err)
|
|
82
|
+
}
|
|
83
|
+
if !exists {
|
|
84
|
+
return nil, fmt.Errorf("instance %s not found", address)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Create client on-demand (no caching)
|
|
89
|
+
target, err := common.NormalizeAddressForGRPC(address)
|
|
90
|
+
if err != nil {
|
|
91
|
+
return nil, fmt.Errorf("invalid address %s: %w", address, err)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
cl, err := client.NewClineClient(target)
|
|
95
|
+
if err != nil {
|
|
96
|
+
return nil, fmt.Errorf("failed to create client for %s: %w", target, err)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if err := cl.Connect(ctx); err != nil {
|
|
100
|
+
return nil, fmt.Errorf("failed to connect to %s: %w", target, err)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return cl, nil
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// GetDefaultClient returns a client for the default instance
|
|
107
|
+
func (r *ClientRegistry) GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
|
|
108
|
+
defaultAddr := r.GetDefaultInstance()
|
|
109
|
+
if defaultAddr == "" {
|
|
110
|
+
return nil, fmt.Errorf("no default instance configured")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check if the default instance actually exists in the database
|
|
114
|
+
if r.lockManager != nil {
|
|
115
|
+
exists, err := r.lockManager.HasInstanceAtAddress(defaultAddr)
|
|
116
|
+
if err != nil {
|
|
117
|
+
// Database is unavailable - Return error instead of attempting cleanup
|
|
118
|
+
return nil, fmt.Errorf("cannot verify default instance: database unavailable: %w", err)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if !exists {
|
|
122
|
+
// Instance doesn't exist in database but config file references it
|
|
123
|
+
// This is a stale config - remove it and try to find another instance
|
|
124
|
+
settingsPath := filepath.Join(r.configPath, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
|
|
125
|
+
if removeErr := os.Remove(settingsPath); removeErr != nil && !os.IsNotExist(removeErr) {
|
|
126
|
+
fmt.Printf("Warning: Failed to remove stale default instance config: %v\n", removeErr)
|
|
127
|
+
} else {
|
|
128
|
+
fmt.Printf("Removed stale default instance config (instance %s not found in database)\n", defaultAddr)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Try to find and set a new default instance
|
|
132
|
+
instances := r.ListInstances()
|
|
133
|
+
if len(instances) > 0 {
|
|
134
|
+
if err := r.EnsureDefaultInstance(instances); err != nil {
|
|
135
|
+
return nil, fmt.Errorf("failed to set new default instance: %w", err)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Retry with the new default
|
|
139
|
+
newDefaultAddr := r.GetDefaultInstance()
|
|
140
|
+
if newDefaultAddr != "" {
|
|
141
|
+
fmt.Printf("Set new default instance: %s\n", newDefaultAddr)
|
|
142
|
+
return r.GetClient(ctx, newDefaultAddr)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return nil, fmt.Errorf("no default instance configured")
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return r.GetClient(ctx, defaultAddr)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ListInstances returns all registered instances directly from SQLite
|
|
154
|
+
func (r *ClientRegistry) ListInstances() []*common.CoreInstanceInfo {
|
|
155
|
+
if r.lockManager == nil {
|
|
156
|
+
return []*common.CoreInstanceInfo{}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Use context with timeout for health checks
|
|
160
|
+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
161
|
+
defer cancel()
|
|
162
|
+
|
|
163
|
+
instances, err := r.lockManager.ListInstancesWithHealthCheck(ctx)
|
|
164
|
+
if err != nil {
|
|
165
|
+
fmt.Printf("Warning: Failed to list instances: %v\n", err)
|
|
166
|
+
return []*common.CoreInstanceInfo{}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return instances
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// HasInstanceAtAddress checks if an instance exists at the given address (delegates to SQLite)
|
|
173
|
+
func (r *ClientRegistry) HasInstanceAtAddress(address string) bool {
|
|
174
|
+
if r.lockManager == nil {
|
|
175
|
+
return false
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
exists, err := r.lockManager.HasInstanceAtAddress(address)
|
|
179
|
+
if err != nil {
|
|
180
|
+
fmt.Printf("Warning: Failed to check instance existence: %v\n", err)
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return exists
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// CleanupStaleInstances removes stale instances using direct SQLite operations
|
|
188
|
+
func (r *ClientRegistry) CleanupStaleInstances(ctx context.Context) error {
|
|
189
|
+
if r.lockManager == nil {
|
|
190
|
+
return nil
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Get all instances with health checks
|
|
194
|
+
instances, err := r.lockManager.ListInstancesWithHealthCheck(ctx)
|
|
195
|
+
if err != nil {
|
|
196
|
+
return fmt.Errorf("failed to list instances for cleanup: %w", err)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Clean up all stale instances
|
|
200
|
+
for _, instance := range instances {
|
|
201
|
+
if instance.Status != grpc_health_v1.HealthCheckResponse_SERVING {
|
|
202
|
+
// Try to gracefully shutdown the paired host process before cleanup
|
|
203
|
+
|
|
204
|
+
fmt.Printf("Attempting to shutdown dangling host service %s for stale cline core instance %s\n",
|
|
205
|
+
instance.HostServiceAddress, instance.Address)
|
|
206
|
+
r.tryShutdownHostProcess(instance.HostServiceAddress)
|
|
207
|
+
|
|
208
|
+
// Remove from SQLite database
|
|
209
|
+
if err := r.lockManager.RemoveInstanceLock(instance.Address); err != nil {
|
|
210
|
+
return fmt.Errorf("failed to remove stale instance %s: %w", instance.Address, err)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fmt.Printf("Removed stale instance: %s\n", instance.Address)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return nil
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// tryShutdownHostProcess attempts to gracefully shutdown a host process via RPC
|
|
221
|
+
// Best effort, don't throw errors i guess
|
|
222
|
+
func (r *ClientRegistry) tryShutdownHostProcess(hostServiceAddress string) {
|
|
223
|
+
err := common.RetryOperation(3, 2*time.Second, func() error {
|
|
224
|
+
// Create context with timeout
|
|
225
|
+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
226
|
+
defer cancel()
|
|
227
|
+
|
|
228
|
+
// Create gRPC connection to host bridge
|
|
229
|
+
conn, err := grpc.DialContext(ctx, hostServiceAddress,
|
|
230
|
+
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
231
|
+
grpc.WithBlock())
|
|
232
|
+
if err != nil {
|
|
233
|
+
return fmt.Errorf("connection failed: %w", err)
|
|
234
|
+
}
|
|
235
|
+
defer conn.Close()
|
|
236
|
+
|
|
237
|
+
// Create env service client and call shutdown
|
|
238
|
+
envClient := host.NewEnvServiceClient(conn)
|
|
239
|
+
_, err = envClient.Shutdown(ctx, &cline.EmptyRequest{})
|
|
240
|
+
if err != nil {
|
|
241
|
+
return fmt.Errorf("RPC failed: %w", err)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return nil
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
if err != nil {
|
|
248
|
+
fmt.Printf("Warning: Failed to request host bridge shutdown on port %s: %v\n", hostServiceAddress, err)
|
|
249
|
+
} else {
|
|
250
|
+
fmt.Printf("Host bridge shutdown requested successfully on port %s\n", hostServiceAddress)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ListInstancesCleaned performs cleanup and returns instances with health checks
|
|
255
|
+
func (r *ClientRegistry) ListInstancesCleaned(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
|
|
256
|
+
// 1. Clean up stale entries (best-effort)
|
|
257
|
+
_ = r.CleanupStaleInstances(ctx)
|
|
258
|
+
|
|
259
|
+
// 2. Get all instances with real-time health checks
|
|
260
|
+
instances := r.ListInstances()
|
|
261
|
+
|
|
262
|
+
// 3. Ensure default is set if instances exist
|
|
263
|
+
if err := r.EnsureDefaultInstance(instances); err != nil {
|
|
264
|
+
fmt.Printf("Warning: Failed to ensure default instance: %v\n", err)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return instances, nil
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// EnsureDefaultInstance ensures a default instance is set if instances exist but no default is configured
|
|
271
|
+
func (r *ClientRegistry) EnsureDefaultInstance(instances []*common.CoreInstanceInfo) error {
|
|
272
|
+
currentDefault := r.GetDefaultInstance()
|
|
273
|
+
|
|
274
|
+
// If we have no instances, clear any stale default and remove settings file
|
|
275
|
+
if len(instances) == 0 {
|
|
276
|
+
if currentDefault != "" {
|
|
277
|
+
// Remove the settings file since no instances exist
|
|
278
|
+
settingsPath := filepath.Join(r.configPath, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
|
|
279
|
+
_ = os.Remove(settingsPath)
|
|
280
|
+
}
|
|
281
|
+
return nil
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// If we have instances but no default, pick the first one
|
|
285
|
+
if currentDefault == "" {
|
|
286
|
+
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Validate current default still exists in the instances
|
|
290
|
+
defaultExists := false
|
|
291
|
+
for _, instance := range instances {
|
|
292
|
+
if instance.Address == currentDefault {
|
|
293
|
+
defaultExists = true
|
|
294
|
+
break
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if !defaultExists {
|
|
299
|
+
// Current default doesn't exist, pick a new one from available instances
|
|
300
|
+
return sqlite.SetDefaultInstance(r.configPath, instances[0].Address)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return nil
|
|
304
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
package handlers
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/cline/cli/pkg/cli/clerror"
|
|
9
|
+
"github.com/cline/cli/pkg/cli/output"
|
|
10
|
+
"github.com/cline/cli/pkg/cli/types"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// AskHandler handles ASK type messages
|
|
14
|
+
type AskHandler struct {
|
|
15
|
+
*BaseHandler
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// NewAskHandler creates a new ASK handler
|
|
19
|
+
func NewAskHandler() *AskHandler {
|
|
20
|
+
return &AskHandler{
|
|
21
|
+
BaseHandler: NewBaseHandler("ask", PriorityHigh),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// CanHandle returns true if this is an ASK message
|
|
26
|
+
func (h *AskHandler) CanHandle(msg *types.ClineMessage) bool {
|
|
27
|
+
return msg.IsAsk()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func (h *AskHandler) Handle(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
31
|
+
// Always display approval messages so user can see what they're approving
|
|
32
|
+
// The input handler will show the approval prompt form after the content is displayed
|
|
33
|
+
|
|
34
|
+
switch msg.Ask {
|
|
35
|
+
case string(types.AskTypeFollowup):
|
|
36
|
+
return h.handleFollowup(msg, dc)
|
|
37
|
+
case string(types.AskTypePlanModeRespond):
|
|
38
|
+
return h.handlePlanModeRespond(msg, dc)
|
|
39
|
+
case string(types.AskTypeCommand):
|
|
40
|
+
return h.handleCommand(msg, dc)
|
|
41
|
+
case string(types.AskTypeCommandOutput):
|
|
42
|
+
return h.handleCommandOutput(msg, dc)
|
|
43
|
+
case string(types.AskTypeCompletionResult):
|
|
44
|
+
return h.handleCompletionResult(msg, dc)
|
|
45
|
+
case string(types.AskTypeTool):
|
|
46
|
+
return h.handleTool(msg, dc)
|
|
47
|
+
case string(types.AskTypeAPIReqFailed):
|
|
48
|
+
return h.handleAPIReqFailed(msg, dc)
|
|
49
|
+
case string(types.AskTypeResumeTask):
|
|
50
|
+
return h.handleResumeTask(msg, dc)
|
|
51
|
+
case string(types.AskTypeResumeCompletedTask):
|
|
52
|
+
return h.handleResumeCompletedTask(msg, dc)
|
|
53
|
+
case string(types.AskTypeMistakeLimitReached):
|
|
54
|
+
return h.handleMistakeLimitReached(msg, dc)
|
|
55
|
+
case string(types.AskTypeBrowserActionLaunch):
|
|
56
|
+
return h.handleBrowserActionLaunch(msg, dc)
|
|
57
|
+
case string(types.AskTypeUseMcpServer):
|
|
58
|
+
return h.handleUseMcpServer(msg, dc)
|
|
59
|
+
case string(types.AskTypeNewTask):
|
|
60
|
+
return h.handleNewTask(msg, dc)
|
|
61
|
+
case string(types.AskTypeCondense):
|
|
62
|
+
return h.handleCondense(msg, dc)
|
|
63
|
+
case string(types.AskTypeReportBug):
|
|
64
|
+
return h.handleReportBug(msg, dc)
|
|
65
|
+
default:
|
|
66
|
+
return h.handleDefault(msg, dc)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// handleFollowup handles followup questions
|
|
71
|
+
func (h *AskHandler) handleFollowup(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
72
|
+
body := dc.ToolRenderer.GenerateAskFollowupBody(msg.Text)
|
|
73
|
+
|
|
74
|
+
if body == "" {
|
|
75
|
+
return nil
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if dc.IsStreamingMode {
|
|
79
|
+
// In streaming mode, header was already shown by partial stream
|
|
80
|
+
// Just render the body content
|
|
81
|
+
output.Print(body)
|
|
82
|
+
} else {
|
|
83
|
+
// Non-streaming mode: render header + body together
|
|
84
|
+
header := dc.ToolRenderer.GenerateAskFollowupHeader()
|
|
85
|
+
rendered := dc.Renderer.RenderMarkdown(header)
|
|
86
|
+
output.Print("\n")
|
|
87
|
+
output.Print(rendered)
|
|
88
|
+
output.Print("\n")
|
|
89
|
+
output.Print(body)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return nil
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// handlePlanModeRespond handles plan mode responses
|
|
96
|
+
func (h *AskHandler) handlePlanModeRespond(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
97
|
+
if dc.IsStreamingMode {
|
|
98
|
+
// In streaming mode, header was already shown by partial stream
|
|
99
|
+
// Just render the body content
|
|
100
|
+
body := dc.ToolRenderer.GeneratePlanModeRespondBody(msg.Text)
|
|
101
|
+
if body != "" {
|
|
102
|
+
output.Print(body)
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// In non-streaming mode, render header + body together
|
|
106
|
+
header := dc.ToolRenderer.GeneratePlanModeRespondHeader()
|
|
107
|
+
body := dc.ToolRenderer.GeneratePlanModeRespondBody(msg.Text)
|
|
108
|
+
|
|
109
|
+
if body == "" {
|
|
110
|
+
return nil
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Render header
|
|
114
|
+
rendered := dc.Renderer.RenderMarkdown(header)
|
|
115
|
+
output.Print("\n")
|
|
116
|
+
output.Print(rendered)
|
|
117
|
+
output.Print("\n")
|
|
118
|
+
|
|
119
|
+
// Render body
|
|
120
|
+
output.Print(body)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return nil
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// showApprovalHint displays a hint in non-interactive mode about how to approve/deny
|
|
127
|
+
func (h *AskHandler) showApprovalHint(dc *DisplayContext) {
|
|
128
|
+
if !dc.IsInteractive {
|
|
129
|
+
output.Printf("\n%s\n", dc.Renderer.Dim("Cline is requesting approval to use this tool"))
|
|
130
|
+
output.Printf("%s\n", dc.Renderer.Dim("Use cline task send --approve or --deny to respond"))
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// handleCommand handles command execution requests
|
|
135
|
+
func (h *AskHandler) handleCommand(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
136
|
+
if msg.Text == "" {
|
|
137
|
+
return nil
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check if this command was flagged despite auto-approval settings
|
|
141
|
+
autoApprovalConflict := strings.HasSuffix(msg.Text, "REQ_APP")
|
|
142
|
+
|
|
143
|
+
// Use unified ToolRenderer
|
|
144
|
+
rendered := dc.ToolRenderer.RenderCommandApprovalRequest(msg.Text, autoApprovalConflict)
|
|
145
|
+
output.Print(rendered)
|
|
146
|
+
|
|
147
|
+
h.showApprovalHint(dc)
|
|
148
|
+
return nil
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// handleCommandOutput handles command output requests
|
|
152
|
+
func (h *AskHandler) handleCommandOutput(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
153
|
+
if msg.Text == "" {
|
|
154
|
+
return nil
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
commandOutput := msg.Text
|
|
158
|
+
|
|
159
|
+
markdown := fmt.Sprintf("```\n%s\n```", commandOutput)
|
|
160
|
+
rendered := dc.Renderer.RenderMarkdown(markdown)
|
|
161
|
+
|
|
162
|
+
fmt.Printf("%s", rendered)
|
|
163
|
+
|
|
164
|
+
return nil
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// handleCompletionResult handles completion result requests
|
|
168
|
+
func (h *AskHandler) handleCompletionResult(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
169
|
+
return nil
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// handleTool handles tool execution requests
|
|
173
|
+
func (h *AskHandler) handleTool(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
174
|
+
// Parse tool message
|
|
175
|
+
var tool types.ToolMessage
|
|
176
|
+
if err := json.Unmarshal([]byte(msg.Text), &tool); err != nil {
|
|
177
|
+
// Fallback to simple display
|
|
178
|
+
return dc.Renderer.RenderMessage("TOOL", msg.Text, true)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if dc.IsStreamingMode {
|
|
182
|
+
// In streaming mode, header was already shown by partial stream
|
|
183
|
+
// Just render the content preview
|
|
184
|
+
contentPreview := dc.ToolRenderer.GenerateToolContentPreview(&tool)
|
|
185
|
+
if contentPreview != "" {
|
|
186
|
+
output.Print("\n")
|
|
187
|
+
output.Print(contentPreview)
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// Non-streaming mode: render full approval (header + preview)
|
|
191
|
+
rendered := dc.ToolRenderer.RenderToolApprovalRequest(&tool)
|
|
192
|
+
output.Print(rendered)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
h.showApprovalHint(dc)
|
|
196
|
+
return nil
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// handleAPIReqFailed handles API request failures
|
|
200
|
+
func (h *AskHandler) handleAPIReqFailed(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
201
|
+
// Try to parse as ClineError for better error display
|
|
202
|
+
clineErr, _ := clerror.ParseClineError(msg.Text)
|
|
203
|
+
if clineErr != nil {
|
|
204
|
+
if dc.SystemRenderer != nil {
|
|
205
|
+
// Render the error with system renderer
|
|
206
|
+
switch clineErr.GetErrorType() {
|
|
207
|
+
case clerror.ErrorTypeBalance:
|
|
208
|
+
dc.SystemRenderer.RenderBalanceError(clineErr)
|
|
209
|
+
case clerror.ErrorTypeAuth:
|
|
210
|
+
dc.SystemRenderer.RenderAuthError(clineErr)
|
|
211
|
+
case clerror.ErrorTypeRateLimit:
|
|
212
|
+
dc.SystemRenderer.RenderRateLimitError(clineErr)
|
|
213
|
+
default:
|
|
214
|
+
dc.SystemRenderer.RenderAPIError(clineErr)
|
|
215
|
+
}
|
|
216
|
+
return nil
|
|
217
|
+
}
|
|
218
|
+
// Fallback: render with basic renderer using parsed message
|
|
219
|
+
return dc.Renderer.RenderMessage("ERROR", fmt.Sprintf("API Request Failed: %s. Approve to retry request.", clineErr.Message), true)
|
|
220
|
+
}
|
|
221
|
+
// Last resort: display raw text if parsing completely failed
|
|
222
|
+
return dc.Renderer.RenderMessage("ERROR", fmt.Sprintf("API Request Failed: %s. Approve to retry request.", msg.Text), true)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// handleResumeTask handles resume task requests
|
|
226
|
+
func (h *AskHandler) handleResumeTask(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
227
|
+
// Don't render - this is metadata only, user already knows they're resuming
|
|
228
|
+
return nil
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// handleResumeCompletedTask handles resume completed task requests
|
|
232
|
+
func (h *AskHandler) handleResumeCompletedTask(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
233
|
+
// Don't render - this is metadata only, user already knows they're resuming
|
|
234
|
+
return nil
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// handleMistakeLimitReached handles mistake limit reached
|
|
238
|
+
func (h *AskHandler) handleMistakeLimitReached(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
239
|
+
if dc.SystemRenderer != nil {
|
|
240
|
+
details := make(map[string]string)
|
|
241
|
+
if msg.Text != "" {
|
|
242
|
+
details["details"] = msg.Text
|
|
243
|
+
}
|
|
244
|
+
dc.SystemRenderer.RenderError(
|
|
245
|
+
"critical",
|
|
246
|
+
"Mistake Limit Reached",
|
|
247
|
+
"Cline has made too many consecutive mistakes and needs your guidance to proceed.",
|
|
248
|
+
details,
|
|
249
|
+
)
|
|
250
|
+
fmt.Printf("\n**Approval required to continue.**\n")
|
|
251
|
+
return nil
|
|
252
|
+
}
|
|
253
|
+
return dc.Renderer.RenderMessage("ERROR", fmt.Sprintf("Mistake Limit Reached: %s. Approval required.", msg.Text), true)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// handleBrowserActionLaunch handles browser action launch requests
|
|
257
|
+
func (h *AskHandler) handleBrowserActionLaunch(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
258
|
+
url := strings.TrimSpace(msg.Text)
|
|
259
|
+
err := dc.Renderer.RenderMessage("BROWSER", fmt.Sprintf("Cline wants to launch browser and navigate to: %s. Approval required.", url), true)
|
|
260
|
+
h.showApprovalHint(dc)
|
|
261
|
+
return err
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// handleUseMcpServer handles MCP server usage requests
|
|
265
|
+
func (h *AskHandler) handleUseMcpServer(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
266
|
+
// Parse MCP server usage request
|
|
267
|
+
type McpServerRequest struct {
|
|
268
|
+
ServerName string `json:"serverName"`
|
|
269
|
+
Type string `json:"type"`
|
|
270
|
+
ToolName string `json:"toolName,omitempty"`
|
|
271
|
+
Arguments string `json:"arguments,omitempty"`
|
|
272
|
+
URI string `json:"uri,omitempty"`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
var mcpReq McpServerRequest
|
|
276
|
+
if err := json.Unmarshal([]byte(msg.Text), &mcpReq); err != nil {
|
|
277
|
+
return dc.Renderer.RenderMessage("MCP", msg.Text, true)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
var operation string
|
|
281
|
+
if mcpReq.Type == "access_mcp_resource" {
|
|
282
|
+
operation = "access a resource"
|
|
283
|
+
} else {
|
|
284
|
+
operation = fmt.Sprintf("use a tool (%s)", mcpReq.ToolName)
|
|
285
|
+
if mcpReq.Arguments != "" {
|
|
286
|
+
operation = fmt.Sprintf("%s with args (%s)", operation, mcpReq.Arguments)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
err := dc.Renderer.RenderMessage("MCP",
|
|
291
|
+
fmt.Sprintf("Cline wants to %s on the %s MCP server", operation, mcpReq.ServerName), true)
|
|
292
|
+
|
|
293
|
+
h.showApprovalHint(dc)
|
|
294
|
+
return err
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// handleNewTask handles new task creation requests
|
|
298
|
+
func (h *AskHandler) handleNewTask(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
299
|
+
return dc.Renderer.RenderMessage("NEW TASK", fmt.Sprintf("Cline wants to start a new task: %s. Approval required.", msg.Text), true)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// handleCondense handles conversation condensing requests
|
|
303
|
+
func (h *AskHandler) handleCondense(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
304
|
+
return dc.Renderer.RenderMessage("CONDENSE", fmt.Sprintf("Cline wants to condense the conversation: %s. Approval required.", msg.Text), true)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// handleReportBug handles bug report requests
|
|
308
|
+
func (h *AskHandler) handleReportBug(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
309
|
+
var bugData struct {
|
|
310
|
+
Title string `json:"title"`
|
|
311
|
+
WhatHappened string `json:"what_happened"`
|
|
312
|
+
StepsToReproduce string `json:"steps_to_reproduce"`
|
|
313
|
+
APIRequestOutput string `json:"api_request_output"`
|
|
314
|
+
AdditionalContext string `json:"additional_context"`
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if err := json.Unmarshal([]byte(msg.Text), &bugData); err != nil {
|
|
318
|
+
return dc.Renderer.RenderMessage("BUG REPORT", fmt.Sprintf("Cline wants to create a GitHub issue: %s. Approval required.", msg.Text), true)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
err := dc.Renderer.RenderMessage("BUG REPORT", "Cline wants to create a GitHub issue:", true)
|
|
322
|
+
if err != nil {
|
|
323
|
+
return fmt.Errorf("failed to render handleReportBug: %w", err)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fmt.Printf("\n**Title**: %s\n", bugData.Title)
|
|
327
|
+
fmt.Printf("**What Happened**: %s\n", bugData.WhatHappened)
|
|
328
|
+
fmt.Printf("**Steps to Reproduce**: %s\n", bugData.StepsToReproduce)
|
|
329
|
+
fmt.Printf("**API Request Output**: %s\n", bugData.APIRequestOutput)
|
|
330
|
+
fmt.Printf("**Additional Context**: %s\n", bugData.AdditionalContext)
|
|
331
|
+
fmt.Printf("\nApprove to create a GitHub issue.\n")
|
|
332
|
+
|
|
333
|
+
return nil
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// handleDefault handles unknown ASK message types
|
|
337
|
+
func (h *AskHandler) handleDefault(msg *types.ClineMessage, dc *DisplayContext) error {
|
|
338
|
+
return dc.Renderer.RenderMessage("ASK", msg.Text, true)
|
|
339
|
+
}
|