@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,366 @@
|
|
|
1
|
+
package sqlite
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"database/sql"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"fmt"
|
|
8
|
+
"net"
|
|
9
|
+
"os"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
"github.com/cline/cli/pkg/common"
|
|
14
|
+
_ "github.com/glebarez/go-sqlite"
|
|
15
|
+
"google.golang.org/grpc/health/grpc_health_v1"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// normalizeAddressVariants returns address variants to try when querying SQLite.
|
|
19
|
+
// Handles localhost/127.0.0.1 equivalence by returning both forms.
|
|
20
|
+
func normalizeAddressVariants(address string) []string {
|
|
21
|
+
variants := []string{address}
|
|
22
|
+
|
|
23
|
+
// Extract host and port
|
|
24
|
+
host, port, err := net.SplitHostPort(address)
|
|
25
|
+
if err != nil {
|
|
26
|
+
return variants
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Add the alternate form for localhost/127.0.0.1
|
|
30
|
+
if host == "localhost" {
|
|
31
|
+
variants = append(variants, net.JoinHostPort("127.0.0.1", port))
|
|
32
|
+
} else if host == "127.0.0.1" {
|
|
33
|
+
variants = append(variants, net.JoinHostPort("localhost", port))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return variants
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// LockManager provides access to the SQLite locks database
|
|
40
|
+
type LockManager struct {
|
|
41
|
+
dbPath string
|
|
42
|
+
db *sql.DB
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// NewLockManager creates a new lock manager
|
|
46
|
+
func NewLockManager(clineDir string) (*LockManager, error) {
|
|
47
|
+
dbPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "locks.db")
|
|
48
|
+
|
|
49
|
+
// Ensure the directory exists (for future DB creation by cline-core)
|
|
50
|
+
dbDir := filepath.Dir(dbPath)
|
|
51
|
+
if err := os.MkdirAll(dbDir, 0755); err != nil {
|
|
52
|
+
return nil, fmt.Errorf("failed to create database directory: %w", err)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if database exists
|
|
56
|
+
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
|
57
|
+
// Database doesn't exist - return manager with nil db
|
|
58
|
+
// All methods already handle this gracefully!
|
|
59
|
+
return &LockManager{dbPath: dbPath, db: nil}, nil
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Database exists - open it normally (no schema creation)
|
|
63
|
+
db, err := sql.Open("sqlite", dbPath)
|
|
64
|
+
if err != nil {
|
|
65
|
+
// If we can't open existing database, return nil db manager
|
|
66
|
+
return &LockManager{dbPath: dbPath, db: nil}, nil
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Test the connection
|
|
70
|
+
if err := db.Ping(); err != nil {
|
|
71
|
+
db.Close()
|
|
72
|
+
// If connection fails, return nil db manager
|
|
73
|
+
return &LockManager{dbPath: dbPath, db: nil}, nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return &LockManager{
|
|
77
|
+
dbPath: dbPath,
|
|
78
|
+
db: db,
|
|
79
|
+
}, nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ensureConnection attempts to establish a database connection if one doesn't exist
|
|
83
|
+
func (lm *LockManager) ensureConnection() error {
|
|
84
|
+
// If we already have a connection, we're done
|
|
85
|
+
if lm.db != nil {
|
|
86
|
+
return nil
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check if database exists now (created by cline-core)
|
|
90
|
+
if _, err := os.Stat(lm.dbPath); os.IsNotExist(err) {
|
|
91
|
+
return fmt.Errorf("database not available")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Database exists, try to connect
|
|
95
|
+
db, err := sql.Open("sqlite", lm.dbPath)
|
|
96
|
+
if err != nil {
|
|
97
|
+
return fmt.Errorf("failed to connect to database: %w", err)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if err := db.Ping(); err != nil {
|
|
101
|
+
db.Close()
|
|
102
|
+
return fmt.Errorf("database connection failed: %w", err)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Success! Update our connection permanently
|
|
106
|
+
lm.db = db
|
|
107
|
+
return nil
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Close closes the database connection
|
|
111
|
+
func (lm *LockManager) Close() error {
|
|
112
|
+
if lm.db != nil {
|
|
113
|
+
return lm.db.Close()
|
|
114
|
+
}
|
|
115
|
+
return nil
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// GetInstanceLocks returns all instance locks
|
|
119
|
+
func (lm *LockManager) GetInstanceLocks() ([]common.LockRow, error) {
|
|
120
|
+
if err := lm.ensureConnection(); err != nil {
|
|
121
|
+
return []common.LockRow{}, nil
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
query := common.SelectInstanceLocksSQL
|
|
125
|
+
|
|
126
|
+
rows, err := lm.db.Query(query)
|
|
127
|
+
if err != nil {
|
|
128
|
+
return nil, fmt.Errorf("failed to query instance locks: %w", err)
|
|
129
|
+
}
|
|
130
|
+
defer rows.Close()
|
|
131
|
+
|
|
132
|
+
var locks []common.LockRow
|
|
133
|
+
for rows.Next() {
|
|
134
|
+
var lock common.LockRow
|
|
135
|
+
err := rows.Scan(&lock.ID, &lock.HeldBy, &lock.LockType, &lock.LockTarget, &lock.LockedAt)
|
|
136
|
+
if err != nil {
|
|
137
|
+
return nil, fmt.Errorf("failed to scan lock row: %w", err)
|
|
138
|
+
}
|
|
139
|
+
locks = append(locks, lock)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return locks, nil
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// RemoveInstanceLock removes an instance lock by address
|
|
146
|
+
func (lm *LockManager) RemoveInstanceLock(address string) error {
|
|
147
|
+
if err := lm.ensureConnection(); err != nil {
|
|
148
|
+
return nil // Gracefully handle missing database for cleanup operations
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
query := common.DeleteInstanceLockSQL
|
|
152
|
+
_, err := lm.db.Exec(query, address)
|
|
153
|
+
if err != nil {
|
|
154
|
+
return fmt.Errorf("failed to remove instance lock: %w", err)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return nil
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// HasInstanceAtAddress checks if an instance exists at the given address
|
|
161
|
+
func (lm *LockManager) HasInstanceAtAddress(address string) (bool, error) {
|
|
162
|
+
if err := lm.ensureConnection(); err != nil {
|
|
163
|
+
return false, err
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
query := common.CountInstanceLockSQL
|
|
167
|
+
var count int
|
|
168
|
+
err := lm.db.QueryRow(query, address).Scan(&count)
|
|
169
|
+
if err != nil {
|
|
170
|
+
return false, fmt.Errorf("failed to check instance existence: %w", err)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return count > 0, nil
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// GetInstanceInfo returns instance information directly from SQLite.
|
|
177
|
+
// Handles localhost/127.0.0.1 equivalence by trying both variants.
|
|
178
|
+
func (lm *LockManager) GetInstanceInfo(address string) (*common.CoreInstanceInfo, error) {
|
|
179
|
+
if err := lm.ensureConnection(); err != nil {
|
|
180
|
+
return nil, err
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
query := common.SelectInstanceLockByHolderSQL
|
|
184
|
+
variants := normalizeAddressVariants(address)
|
|
185
|
+
|
|
186
|
+
var heldBy, lockTarget string
|
|
187
|
+
var lockedAt int64
|
|
188
|
+
var lastErr error
|
|
189
|
+
|
|
190
|
+
// Try each address variant (e.g., localhost:50607 and 127.0.0.1:50607)
|
|
191
|
+
for _, variant := range variants {
|
|
192
|
+
err := lm.db.QueryRow(query, variant).Scan(&heldBy, &lockTarget, &lockedAt)
|
|
193
|
+
if err == nil {
|
|
194
|
+
// Found it!
|
|
195
|
+
return &common.CoreInstanceInfo{
|
|
196
|
+
Address: heldBy,
|
|
197
|
+
HostServiceAddress: lockTarget,
|
|
198
|
+
Status: grpc_health_v1.HealthCheckResponse_UNKNOWN,
|
|
199
|
+
LastSeen: time.Unix(lockedAt/1000, 0),
|
|
200
|
+
}, nil
|
|
201
|
+
}
|
|
202
|
+
if err != sql.ErrNoRows {
|
|
203
|
+
// Real error (not just "not found"), save it
|
|
204
|
+
lastErr = err
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// None of the variants were found
|
|
209
|
+
if lastErr != nil {
|
|
210
|
+
return nil, fmt.Errorf("failed to query instance: %w", lastErr)
|
|
211
|
+
}
|
|
212
|
+
return nil, fmt.Errorf("instance %s not found", address)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ListInstancesWithHealthCheck returns all instances with real-time health checks
|
|
216
|
+
func (lm *LockManager) ListInstancesWithHealthCheck(ctx context.Context) ([]*common.CoreInstanceInfo, error) {
|
|
217
|
+
if err := lm.ensureConnection(); err != nil {
|
|
218
|
+
return []*common.CoreInstanceInfo{}, nil
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Get all instance locks
|
|
222
|
+
locks, err := lm.GetInstanceLocks()
|
|
223
|
+
if err != nil {
|
|
224
|
+
return nil, fmt.Errorf("failed to get instance locks: %w", err)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
var instances []*common.CoreInstanceInfo
|
|
228
|
+
|
|
229
|
+
for _, lock := range locks {
|
|
230
|
+
// Create instance info using actual SQLite data
|
|
231
|
+
status, err := common.PerformHealthCheck(ctx, lock.HeldBy)
|
|
232
|
+
if status != grpc_health_v1.HealthCheckResponse_SERVING || err != nil {
|
|
233
|
+
time.Sleep(1 * time.Second)
|
|
234
|
+
status, err = common.PerformHealthCheck(ctx, lock.HeldBy)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
info := &common.CoreInstanceInfo{
|
|
238
|
+
Address: lock.HeldBy,
|
|
239
|
+
HostServiceAddress: lock.LockTarget,
|
|
240
|
+
Status: status,
|
|
241
|
+
LastSeen: time.Unix(lock.LockedAt/1000, 0),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
instances = append(instances, info)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return instances, nil
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// GetDefaultInstance reads the default instance from the settings file
|
|
251
|
+
func GetDefaultInstance(clineDir string) (string, error) {
|
|
252
|
+
settingsPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
|
|
253
|
+
|
|
254
|
+
data, err := os.ReadFile(settingsPath)
|
|
255
|
+
if err != nil {
|
|
256
|
+
if os.IsNotExist(err) {
|
|
257
|
+
return "", nil
|
|
258
|
+
}
|
|
259
|
+
return "", fmt.Errorf("failed to read default instance file: %w", err)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
var defaultInstance common.DefaultCoreInstance
|
|
263
|
+
if err := json.Unmarshal(data, &defaultInstance); err != nil {
|
|
264
|
+
return "", fmt.Errorf("failed to parse default instance JSON: %w", err)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if defaultInstance.Address == "" {
|
|
268
|
+
return "", fmt.Errorf("default instance not set in settings file")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return defaultInstance.Address, nil
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// SetDefaultInstance writes the default instance to the settings file with proper locking
|
|
275
|
+
func SetDefaultInstance(clineDir, address string) error {
|
|
276
|
+
// Create lock manager for this operation
|
|
277
|
+
lockManager, err := NewLockManager(clineDir)
|
|
278
|
+
if err != nil {
|
|
279
|
+
return fmt.Errorf("Warning: SQLite unavailable, writing without lock: %v\n", err)
|
|
280
|
+
}
|
|
281
|
+
defer lockManager.Close()
|
|
282
|
+
|
|
283
|
+
settingsPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
|
|
284
|
+
|
|
285
|
+
// Generate a unique identifier for this CLI process
|
|
286
|
+
heldBy := fmt.Sprintf("cli-process-%d", os.Getpid())
|
|
287
|
+
|
|
288
|
+
// Use file lock for the write operation
|
|
289
|
+
return lockManager.WithFileLock(settingsPath, heldBy, func() error {
|
|
290
|
+
return writeDefaultInstanceJSONToDisk(clineDir, address)
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
func writeDefaultInstanceJSONToDisk(clineDir, address string) error {
|
|
295
|
+
settingsDir := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings")
|
|
296
|
+
if err := os.MkdirAll(settingsDir, 0755); err != nil {
|
|
297
|
+
return fmt.Errorf("failed to create settings directory: %w", err)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
settingsPath := filepath.Join(settingsDir, "cli-default-instance.json")
|
|
301
|
+
|
|
302
|
+
payload := common.DefaultCoreInstance{
|
|
303
|
+
Address: address,
|
|
304
|
+
LastUpdated: time.Now().Format(time.RFC3339),
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
data, err := json.MarshalIndent(payload, "", " ")
|
|
308
|
+
if err != nil {
|
|
309
|
+
return fmt.Errorf("failed to marshal default instance JSON: %w", err)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
|
|
313
|
+
return fmt.Errorf("failed to write default instance file: %w", err)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return nil
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// AcquireFileLock attempts to acquire a file lock
|
|
320
|
+
func (lm *LockManager) AcquireFileLock(filePath, heldBy string) error {
|
|
321
|
+
if err := lm.ensureConnection(); err != nil {
|
|
322
|
+
return err
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
now := time.Now().Unix() * 1000 // Convert to milliseconds
|
|
326
|
+
|
|
327
|
+
query := common.InsertFileLockSQL
|
|
328
|
+
|
|
329
|
+
_, err := lm.db.Exec(query, heldBy, filePath, now)
|
|
330
|
+
if err != nil {
|
|
331
|
+
return fmt.Errorf("failed to acquire file lock for %s: %w", filePath, err)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return nil
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ReleaseFileLock releases a file lock
|
|
338
|
+
func (lm *LockManager) ReleaseFileLock(filePath, heldBy string) error {
|
|
339
|
+
if lm.db == nil {
|
|
340
|
+
return nil
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
query := common.DeleteFileLockSQL
|
|
344
|
+
|
|
345
|
+
_, err := lm.db.Exec(query, heldBy, filePath)
|
|
346
|
+
if err != nil {
|
|
347
|
+
return fmt.Errorf("failed to release file lock for %s: %w", filePath, err)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return nil
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// WithFileLock executes a function while holding a file lock
|
|
354
|
+
func (lm *LockManager) WithFileLock(filePath, heldBy string, fn func() error) error {
|
|
355
|
+
if err := lm.AcquireFileLock(filePath, heldBy); err != nil {
|
|
356
|
+
return err
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
defer func() {
|
|
360
|
+
if releaseErr := lm.ReleaseFileLock(filePath, heldBy); releaseErr != nil {
|
|
361
|
+
fmt.Printf("Warning: Failed to release file lock for %s: %v\n", filePath, releaseErr)
|
|
362
|
+
}
|
|
363
|
+
}()
|
|
364
|
+
|
|
365
|
+
return fn()
|
|
366
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
package task
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"sort"
|
|
9
|
+
|
|
10
|
+
"github.com/cline/cli/pkg/cli/display"
|
|
11
|
+
"github.com/cline/cli/pkg/cli/global"
|
|
12
|
+
"github.com/cline/cli/pkg/cli/types"
|
|
13
|
+
"github.com/cline/grpc-go/cline"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
// ListTasksFromDisk reads task history directly from disk
|
|
17
|
+
func ListTasksFromDisk() error {
|
|
18
|
+
// Get the task history file path
|
|
19
|
+
homeDir, err := os.UserHomeDir()
|
|
20
|
+
if err != nil {
|
|
21
|
+
return fmt.Errorf("failed to get home directory: %w", err)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
filePath := filepath.Join(homeDir, ".cline", "data", "state", "taskHistory.json")
|
|
25
|
+
|
|
26
|
+
// Read the file
|
|
27
|
+
data, err := os.ReadFile(filePath)
|
|
28
|
+
if err != nil {
|
|
29
|
+
if os.IsNotExist(err) {
|
|
30
|
+
fmt.Println("No task history found.")
|
|
31
|
+
return nil
|
|
32
|
+
}
|
|
33
|
+
return fmt.Errorf("failed to read task history: %w", err)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse JSON into intermediate struct
|
|
37
|
+
var historyItems []types.HistoryItem
|
|
38
|
+
if err := json.Unmarshal(data, &historyItems); err != nil {
|
|
39
|
+
return fmt.Errorf("failed to parse task history: %w", err)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if len(historyItems) == 0 {
|
|
43
|
+
fmt.Println("No task history found.")
|
|
44
|
+
return nil
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Sort by timestamp ascending (oldest first, newest last)
|
|
48
|
+
sort.Slice(historyItems, func(i, j int) bool {
|
|
49
|
+
return historyItems[i].Ts < historyItems[j].Ts
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Convert to protobuf TaskItem format for rendering
|
|
53
|
+
tasks := make([]*cline.TaskItem, len(historyItems))
|
|
54
|
+
for i, item := range historyItems {
|
|
55
|
+
tasks[i] = &cline.TaskItem{
|
|
56
|
+
Id: item.Id,
|
|
57
|
+
Task: item.Task,
|
|
58
|
+
Ts: item.Ts,
|
|
59
|
+
IsFavorited: item.IsFavorited,
|
|
60
|
+
Size: item.Size,
|
|
61
|
+
TotalCost: item.TotalCost,
|
|
62
|
+
TokensIn: item.TokensIn,
|
|
63
|
+
TokensOut: item.TokensOut,
|
|
64
|
+
CacheWrites: item.CacheWrites,
|
|
65
|
+
CacheReads: item.CacheReads,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Use existing renderer
|
|
70
|
+
renderer := display.NewRenderer(global.Config.OutputFormat)
|
|
71
|
+
return renderer.RenderTaskList(tasks)
|
|
72
|
+
}
|