@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,351 @@
|
|
|
1
|
+
package hostbridge
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"io/ioutil"
|
|
7
|
+
"log"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strings"
|
|
11
|
+
"sync"
|
|
12
|
+
"sync/atomic"
|
|
13
|
+
|
|
14
|
+
proto "github.com/cline/grpc-go/host"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// diffSession represents an in-memory diff editing session
|
|
18
|
+
type diffSession struct {
|
|
19
|
+
originalPath string // File path from OpenDiff request
|
|
20
|
+
originalContent []byte // Original file content (for comparison)
|
|
21
|
+
currentContent []byte // Current modified content
|
|
22
|
+
lines []string // Current content split into lines
|
|
23
|
+
encoding string // File encoding (default: utf8)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// DiffService implements the proto.DiffServiceServer interface
|
|
27
|
+
type DiffService struct {
|
|
28
|
+
proto.UnimplementedDiffServiceServer
|
|
29
|
+
verbose bool
|
|
30
|
+
sessions *sync.Map // thread-safe: diffId -> *diffSession
|
|
31
|
+
counter *int64 // atomic counter for unique IDs
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// NewDiffService creates a new DiffService
|
|
35
|
+
func NewDiffService(verbose bool) *DiffService {
|
|
36
|
+
counter := int64(0)
|
|
37
|
+
return &DiffService{
|
|
38
|
+
verbose: verbose,
|
|
39
|
+
sessions: &sync.Map{},
|
|
40
|
+
counter: &counter,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// generateDiffID creates a unique diff ID
|
|
45
|
+
func (s *DiffService) generateDiffID() string {
|
|
46
|
+
id := atomic.AddInt64(s.counter, 1)
|
|
47
|
+
return fmt.Sprintf("diff_%d_%d", os.Getpid(), id)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// splitLines splits content into lines, preserving line ending information
|
|
51
|
+
func splitLines(content string) []string {
|
|
52
|
+
if content == "" {
|
|
53
|
+
return []string{}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
lines := []string{}
|
|
57
|
+
current := ""
|
|
58
|
+
|
|
59
|
+
for _, char := range content {
|
|
60
|
+
if char == '\n' {
|
|
61
|
+
lines = append(lines, current)
|
|
62
|
+
current = ""
|
|
63
|
+
} else if char != '\r' { // Skip \r characters, handle \r\n as \n
|
|
64
|
+
current += string(char)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add the last line if it doesn't end with newline
|
|
69
|
+
if current != "" {
|
|
70
|
+
lines = append(lines, current)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lines
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// joinLines joins lines back into content with newlines
|
|
77
|
+
func joinLines(lines []string) string {
|
|
78
|
+
if len(lines) == 0 {
|
|
79
|
+
return ""
|
|
80
|
+
}
|
|
81
|
+
return strings.Join(lines, "\n")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// OpenDiff opens a diff view for the specified file
|
|
85
|
+
func (s *DiffService) OpenDiff(ctx context.Context, req *proto.OpenDiffRequest) (*proto.OpenDiffResponse, error) {
|
|
86
|
+
if s.verbose {
|
|
87
|
+
log.Printf("OpenDiff called for path: %s", req.GetPath())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
diffID := s.generateDiffID()
|
|
91
|
+
|
|
92
|
+
var originalContent []byte
|
|
93
|
+
|
|
94
|
+
// Check if file exists and read original content
|
|
95
|
+
if req.GetPath() != "" {
|
|
96
|
+
if _, err := os.Stat(req.GetPath()); err == nil {
|
|
97
|
+
// File exists, read its content
|
|
98
|
+
var readErr error
|
|
99
|
+
originalContent, readErr = ioutil.ReadFile(req.GetPath())
|
|
100
|
+
if readErr != nil {
|
|
101
|
+
return nil, fmt.Errorf("failed to read original file: %w", readErr)
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
// File doesn't exist, use empty content
|
|
105
|
+
originalContent = []byte{}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Use provided content as the initial current content
|
|
110
|
+
currentContent := []byte(req.GetContent())
|
|
111
|
+
|
|
112
|
+
// Create the diff session
|
|
113
|
+
session := &diffSession{
|
|
114
|
+
originalPath: req.GetPath(),
|
|
115
|
+
originalContent: originalContent,
|
|
116
|
+
currentContent: currentContent,
|
|
117
|
+
lines: splitLines(req.GetContent()),
|
|
118
|
+
encoding: "utf8", // Default encoding
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Store the session
|
|
122
|
+
s.sessions.Store(diffID, session)
|
|
123
|
+
|
|
124
|
+
if s.verbose {
|
|
125
|
+
log.Printf("Created diff session: %s (original: %d bytes, current: %d bytes)",
|
|
126
|
+
diffID, len(originalContent), len(currentContent))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return &proto.OpenDiffResponse{
|
|
130
|
+
DiffId: &diffID,
|
|
131
|
+
}, nil
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// GetDocumentText returns the current content of the diff document
|
|
135
|
+
func (s *DiffService) GetDocumentText(ctx context.Context, req *proto.GetDocumentTextRequest) (*proto.GetDocumentTextResponse, error) {
|
|
136
|
+
if s.verbose {
|
|
137
|
+
log.Printf("GetDocumentText called for diff ID: %s", req.GetDiffId())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
sessionInterface, exists := s.sessions.Load(req.GetDiffId())
|
|
141
|
+
if !exists {
|
|
142
|
+
return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
session := sessionInterface.(*diffSession)
|
|
146
|
+
content := string(session.currentContent)
|
|
147
|
+
|
|
148
|
+
return &proto.GetDocumentTextResponse{
|
|
149
|
+
Content: &content,
|
|
150
|
+
}, nil
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ReplaceText replaces text in the diff document using line-based operations
|
|
154
|
+
func (s *DiffService) ReplaceText(ctx context.Context, req *proto.ReplaceTextRequest) (*proto.ReplaceTextResponse, error) {
|
|
155
|
+
if s.verbose {
|
|
156
|
+
log.Printf("ReplaceText called for diff ID: %s, lines %d-%d",
|
|
157
|
+
req.GetDiffId(), req.GetStartLine(), req.GetEndLine())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
sessionInterface, exists := s.sessions.Load(req.GetDiffId())
|
|
161
|
+
if !exists {
|
|
162
|
+
return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
session := sessionInterface.(*diffSession)
|
|
166
|
+
|
|
167
|
+
startLine := int(req.GetStartLine())
|
|
168
|
+
endLine := int(req.GetEndLine())
|
|
169
|
+
newContent := req.GetContent()
|
|
170
|
+
|
|
171
|
+
// Validate line ranges
|
|
172
|
+
if startLine < 0 {
|
|
173
|
+
startLine = 0
|
|
174
|
+
}
|
|
175
|
+
if endLine < startLine {
|
|
176
|
+
endLine = startLine
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Split new content into lines
|
|
180
|
+
newLines := splitLines(newContent)
|
|
181
|
+
|
|
182
|
+
// Ensure we have enough lines in the current content
|
|
183
|
+
for len(session.lines) < endLine {
|
|
184
|
+
session.lines = append(session.lines, "")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Replace the specified line range
|
|
188
|
+
if endLine > len(session.lines) {
|
|
189
|
+
// Extending beyond current content - append new lines
|
|
190
|
+
session.lines = append(session.lines[:startLine], newLines...)
|
|
191
|
+
} else {
|
|
192
|
+
// Replace within existing content
|
|
193
|
+
result := make([]string, 0, len(session.lines)-endLine+startLine+len(newLines))
|
|
194
|
+
result = append(result, session.lines[:startLine]...)
|
|
195
|
+
result = append(result, newLines...)
|
|
196
|
+
result = append(result, session.lines[endLine:]...)
|
|
197
|
+
session.lines = result
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Update current content
|
|
201
|
+
session.currentContent = []byte(joinLines(session.lines))
|
|
202
|
+
|
|
203
|
+
// Store the updated session
|
|
204
|
+
s.sessions.Store(req.GetDiffId(), session)
|
|
205
|
+
|
|
206
|
+
if s.verbose {
|
|
207
|
+
log.Printf("Updated diff session %s: %d lines, %d bytes",
|
|
208
|
+
req.GetDiffId(), len(session.lines), len(session.currentContent))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return &proto.ReplaceTextResponse{}, nil
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ScrollDiff scrolls the diff view to a specific line (no-op for CLI)
|
|
215
|
+
func (s *DiffService) ScrollDiff(ctx context.Context, req *proto.ScrollDiffRequest) (*proto.ScrollDiffResponse, error) {
|
|
216
|
+
if s.verbose {
|
|
217
|
+
log.Printf("ScrollDiff called for diff ID: %s, line: %d", req.GetDiffId(), req.GetLine())
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Verify session exists
|
|
221
|
+
if _, exists := s.sessions.Load(req.GetDiffId()); !exists {
|
|
222
|
+
return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// In a CLI implementation, scrolling is a no-op
|
|
226
|
+
// In a GUI implementation, this would scroll the view to the specified line
|
|
227
|
+
return &proto.ScrollDiffResponse{}, nil
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// TruncateDocument truncates the diff document at the specified line
|
|
231
|
+
func (s *DiffService) TruncateDocument(ctx context.Context, req *proto.TruncateDocumentRequest) (*proto.TruncateDocumentResponse, error) {
|
|
232
|
+
if s.verbose {
|
|
233
|
+
log.Printf("TruncateDocument called for diff ID: %s, end line: %d", req.GetDiffId(), req.GetEndLine())
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
sessionInterface, exists := s.sessions.Load(req.GetDiffId())
|
|
237
|
+
if !exists {
|
|
238
|
+
return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
session := sessionInterface.(*diffSession)
|
|
242
|
+
endLine := int(req.GetEndLine())
|
|
243
|
+
|
|
244
|
+
// Truncate lines at the specified position
|
|
245
|
+
if endLine >= 0 && endLine < len(session.lines) {
|
|
246
|
+
session.lines = session.lines[:endLine]
|
|
247
|
+
session.currentContent = []byte(joinLines(session.lines))
|
|
248
|
+
|
|
249
|
+
// Store the updated session
|
|
250
|
+
s.sessions.Store(req.GetDiffId(), session)
|
|
251
|
+
|
|
252
|
+
if s.verbose {
|
|
253
|
+
log.Printf("Truncated diff session %s to %d lines", req.GetDiffId(), len(session.lines))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return &proto.TruncateDocumentResponse{}, nil
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// SaveDocument saves the diff document to the original file
|
|
261
|
+
func (s *DiffService) SaveDocument(ctx context.Context, req *proto.SaveDocumentRequest) (*proto.SaveDocumentResponse, error) {
|
|
262
|
+
if s.verbose {
|
|
263
|
+
log.Printf("SaveDocument called for diff ID: %s", req.GetDiffId())
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
sessionInterface, exists := s.sessions.Load(req.GetDiffId())
|
|
267
|
+
if !exists {
|
|
268
|
+
return nil, fmt.Errorf("diff session not found: %s", req.GetDiffId())
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
session := sessionInterface.(*diffSession)
|
|
272
|
+
|
|
273
|
+
if session.originalPath == "" {
|
|
274
|
+
return nil, fmt.Errorf("no file path specified for diff session: %s", req.GetDiffId())
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Create parent directories if they don't exist
|
|
278
|
+
dir := filepath.Dir(session.originalPath)
|
|
279
|
+
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
280
|
+
return nil, fmt.Errorf("failed to create directories: %w", err)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Write the current content to the original file
|
|
284
|
+
if err := ioutil.WriteFile(session.originalPath, session.currentContent, 0644); err != nil {
|
|
285
|
+
return nil, fmt.Errorf("failed to save file: %w", err)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if s.verbose {
|
|
289
|
+
log.Printf("Saved diff session %s to file: %s (%d bytes)",
|
|
290
|
+
req.GetDiffId(), session.originalPath, len(session.currentContent))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return &proto.SaveDocumentResponse{}, nil
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// CloseAllDiffs closes all diff views and cleans up all sessions
|
|
297
|
+
func (s *DiffService) CloseAllDiffs(ctx context.Context, req *proto.CloseAllDiffsRequest) (*proto.CloseAllDiffsResponse, error) {
|
|
298
|
+
if s.verbose {
|
|
299
|
+
log.Printf("CloseAllDiffs called")
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
var count int64
|
|
303
|
+
|
|
304
|
+
s.sessions.Range(func(key, value any) bool {
|
|
305
|
+
// Optional: attempt to close if the value supports it
|
|
306
|
+
if c, ok := value.(interface{ Close() error }); ok {
|
|
307
|
+
_ = c.Close() // best-effort; ignore error
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
s.sessions.Delete(key)
|
|
311
|
+
atomic.AddInt64(&count, 1)
|
|
312
|
+
return true
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
if s.verbose {
|
|
316
|
+
log.Printf("Closed %d diff sessions", count)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return &proto.CloseAllDiffsResponse{}, nil
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// OpenMultiFileDiff displays a diff view comparing before/after states for multiple files
|
|
323
|
+
func (s *DiffService) OpenMultiFileDiff(ctx context.Context, req *proto.OpenMultiFileDiffRequest) (*proto.OpenMultiFileDiffResponse, error) {
|
|
324
|
+
if s.verbose {
|
|
325
|
+
log.Printf("OpenMultiFileDiff called with title: %s, %d files", req.GetTitle(), len(req.GetDiffs()))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// In a CLI implementation, we could display the diffs to console
|
|
329
|
+
// For now, we'll just log the information
|
|
330
|
+
title := req.GetTitle()
|
|
331
|
+
if title == "" {
|
|
332
|
+
title = "Multi-file diff"
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if s.verbose {
|
|
336
|
+
log.Printf("=== %s ===", title)
|
|
337
|
+
for i, diff := range req.GetDiffs() {
|
|
338
|
+
log.Printf("File %d: %s", i+1, diff.GetFilePath())
|
|
339
|
+
log.Printf(" Left content: %d bytes", len(diff.GetLeftContent()))
|
|
340
|
+
log.Printf(" Right content: %d bytes", len(diff.GetRightContent()))
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// In a more sophisticated CLI implementation, we could:
|
|
345
|
+
// 1. Use a diff library to generate unified diffs
|
|
346
|
+
// 2. Display them with colors
|
|
347
|
+
// 3. Allow navigation between files
|
|
348
|
+
// For now, this is a no-op that just acknowledges the request
|
|
349
|
+
|
|
350
|
+
return &proto.OpenMultiFileDiffResponse{}, nil
|
|
351
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package hostbridge
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"log"
|
|
5
|
+
|
|
6
|
+
"github.com/cline/grpc-go/host"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// WatchService implements the host.WatchServiceServer interface
|
|
10
|
+
type WatchService struct {
|
|
11
|
+
host.UnimplementedWatchServiceServer
|
|
12
|
+
coreAddress string
|
|
13
|
+
verbose bool
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// NewWatchService creates a new WatchService
|
|
17
|
+
func NewWatchService(coreAddress string, verbose bool) *WatchService {
|
|
18
|
+
return &WatchService{
|
|
19
|
+
coreAddress: coreAddress,
|
|
20
|
+
verbose: verbose,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// SubscribeToFile subscribes to file change notifications
|
|
25
|
+
func (s *WatchService) SubscribeToFile(req *host.SubscribeToFileRequest, stream host.WatchService_SubscribeToFileServer) error {
|
|
26
|
+
if s.verbose {
|
|
27
|
+
log.Printf("SubscribeToFile called for path: %s", req.GetPath())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// For console implementation, we'll just log that we would watch the file
|
|
31
|
+
// In a real implementation, we'd use fsnotify or similar to watch file changes
|
|
32
|
+
log.Printf("[Cline] Would watch file: %s", req.GetPath())
|
|
33
|
+
|
|
34
|
+
// Keep the stream open but don't send any events for now
|
|
35
|
+
// In a real implementation, we'd send FileChangeEvent messages when files change
|
|
36
|
+
<-stream.Context().Done()
|
|
37
|
+
|
|
38
|
+
return nil
|
|
39
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package hostbridge
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"log"
|
|
7
|
+
|
|
8
|
+
proto "github.com/cline/grpc-go/host"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// WindowService implements the proto.WindowServiceServer interface
|
|
12
|
+
type WindowService struct {
|
|
13
|
+
proto.UnimplementedWindowServiceServer
|
|
14
|
+
coreAddress string
|
|
15
|
+
verbose bool
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// NewWindowService creates a new WindowService
|
|
19
|
+
func NewWindowService(coreAddress string, verbose bool) *WindowService {
|
|
20
|
+
return &WindowService{
|
|
21
|
+
coreAddress: coreAddress,
|
|
22
|
+
verbose: verbose,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ShowTextDocument opens a text document for viewing/editing
|
|
27
|
+
func (s *WindowService) ShowTextDocument(ctx context.Context, req *proto.ShowTextDocumentRequest) (*proto.TextEditorInfo, error) {
|
|
28
|
+
if s.verbose {
|
|
29
|
+
log.Printf("ShowTextDocument called for path: %s", req.GetPath())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// For console implementation, we'll just log that we would open the document
|
|
33
|
+
fmt.Printf("[Cline] Would open document: %s\n", req.GetPath())
|
|
34
|
+
|
|
35
|
+
return &proto.TextEditorInfo{
|
|
36
|
+
DocumentPath: req.GetPath(),
|
|
37
|
+
IsActive: true,
|
|
38
|
+
}, nil
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ShowOpenDialogue shows a file open dialog
|
|
42
|
+
func (s *WindowService) ShowOpenDialogue(ctx context.Context, req *proto.ShowOpenDialogueRequest) (*proto.SelectedResources, error) {
|
|
43
|
+
if s.verbose {
|
|
44
|
+
log.Printf("ShowOpenDialogue called")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// For console implementation, return empty list (user cancelled)
|
|
48
|
+
return &proto.SelectedResources{
|
|
49
|
+
Paths: []string{},
|
|
50
|
+
}, nil
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ShowMessage displays a message to the user
|
|
54
|
+
func (s *WindowService) ShowMessage(ctx context.Context, req *proto.ShowMessageRequest) (*proto.SelectedResponse, error) {
|
|
55
|
+
if s.verbose {
|
|
56
|
+
log.Printf("ShowMessage called: %s", req.GetMessage())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Display message to console
|
|
60
|
+
fmt.Printf("[Cline] %s\n", req.GetMessage())
|
|
61
|
+
|
|
62
|
+
return &proto.SelectedResponse{}, nil
|
|
63
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package hostbridge
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"log"
|
|
6
|
+
"os"
|
|
7
|
+
|
|
8
|
+
"github.com/cline/grpc-go/host"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// WorkspaceService implements the host.WorkspaceServiceServer interface
|
|
12
|
+
type WorkspaceService struct {
|
|
13
|
+
host.UnimplementedWorkspaceServiceServer
|
|
14
|
+
coreAddress string
|
|
15
|
+
verbose bool
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// NewWorkspaceService creates a new WorkspaceService
|
|
19
|
+
func NewWorkspaceService(coreAddress string, verbose bool) *WorkspaceService {
|
|
20
|
+
return &WorkspaceService{
|
|
21
|
+
coreAddress: coreAddress,
|
|
22
|
+
verbose: verbose,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// GetWorkspacePaths returns the workspace directory paths
|
|
27
|
+
func (s *WorkspaceService) GetWorkspacePaths(ctx context.Context, req *host.GetWorkspacePathsRequest) (*host.GetWorkspacePathsResponse, error) {
|
|
28
|
+
if s.verbose {
|
|
29
|
+
log.Printf("GetWorkspacePaths called")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Get current working directory as the workspace
|
|
33
|
+
cwd, err := os.Getwd()
|
|
34
|
+
if err != nil {
|
|
35
|
+
return nil, err
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return &host.GetWorkspacePathsResponse{
|
|
39
|
+
Paths: []string{cwd},
|
|
40
|
+
}, nil
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// SaveOpenDocumentIfDirty saves an open document if it has unsaved changes
|
|
44
|
+
func (s *WorkspaceService) SaveOpenDocumentIfDirty(ctx context.Context, req *host.SaveOpenDocumentIfDirtyRequest) (*host.SaveOpenDocumentIfDirtyResponse, error) {
|
|
45
|
+
if s.verbose {
|
|
46
|
+
log.Printf("SaveOpenDocumentIfDirty called for path: %s", req.GetPath())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// For console implementation, we'll assume the document is already saved
|
|
50
|
+
// In a real implementation, we'd check if the file has unsaved changes
|
|
51
|
+
return &host.SaveOpenDocumentIfDirtyResponse{
|
|
52
|
+
WasSaved: false, // Assume no changes to save
|
|
53
|
+
}, nil
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// GetDiagnostics returns diagnostic information for a file
|
|
57
|
+
func (s *WorkspaceService) GetDiagnostics(ctx context.Context, req *host.GetDiagnosticsRequest) (*host.GetDiagnosticsResponse, error) {
|
|
58
|
+
if s.verbose {
|
|
59
|
+
log.Printf("GetDiagnostics called for path: %s", req.GetPath())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// For console implementation, return empty diagnostics
|
|
63
|
+
return &host.GetDiagnosticsResponse{
|
|
64
|
+
Diagnostics: []*host.Diagnostic{},
|
|
65
|
+
}, nil
|
|
66
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package hostbridge
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"log"
|
|
6
|
+
"os"
|
|
7
|
+
|
|
8
|
+
"github.com/atotto/clipboard"
|
|
9
|
+
"github.com/cline/cli/pkg/cli/global"
|
|
10
|
+
"github.com/cline/grpc-go/cline"
|
|
11
|
+
"github.com/cline/grpc-go/host"
|
|
12
|
+
"google.golang.org/protobuf/proto"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// Global shutdown channel - simple approach
|
|
16
|
+
var globalShutdownCh chan struct{}
|
|
17
|
+
|
|
18
|
+
func init() {
|
|
19
|
+
globalShutdownCh = make(chan struct{})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// EnvService implements the host.EnvServiceServer interface
|
|
23
|
+
type EnvService struct {
|
|
24
|
+
host.UnimplementedEnvServiceServer
|
|
25
|
+
verbose bool
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// NewEnvService creates a new EnvService
|
|
29
|
+
func NewEnvService(verbose bool) *EnvService {
|
|
30
|
+
return &EnvService{
|
|
31
|
+
verbose: verbose,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ClipboardWriteText writes text to the system clipboard
|
|
36
|
+
func (s *EnvService) ClipboardWriteText(ctx context.Context, req *cline.StringRequest) (*cline.Empty, error) {
|
|
37
|
+
if s.verbose {
|
|
38
|
+
log.Printf("ClipboardWriteText called with text length: %d", len(req.GetValue()))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
err := clipboard.WriteAll(req.GetValue())
|
|
42
|
+
if err != nil {
|
|
43
|
+
if s.verbose {
|
|
44
|
+
log.Printf("Failed to write to clipboard: %v", err)
|
|
45
|
+
}
|
|
46
|
+
// Don't fail if clipboard is not available (e.g., headless environment)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return &cline.Empty{}, nil
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ClipboardReadText reads text from the system clipboard
|
|
53
|
+
func (s *EnvService) ClipboardReadText(ctx context.Context, req *cline.EmptyRequest) (*cline.String, error) {
|
|
54
|
+
if s.verbose {
|
|
55
|
+
log.Printf("ClipboardReadText called")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
text, err := clipboard.ReadAll()
|
|
59
|
+
if err != nil {
|
|
60
|
+
if s.verbose {
|
|
61
|
+
log.Printf("Failed to read from clipboard: %v", err)
|
|
62
|
+
}
|
|
63
|
+
// Return empty string if clipboard is not available
|
|
64
|
+
text = ""
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return &cline.String{
|
|
68
|
+
Value: text,
|
|
69
|
+
}, nil
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// GetHostVersion returns the host platform name and version
|
|
73
|
+
func (s *EnvService) GetHostVersion(ctx context.Context, req *cline.EmptyRequest) (*host.GetHostVersionResponse, error) {
|
|
74
|
+
if s.verbose {
|
|
75
|
+
log.Printf("GetHostVersion called")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return &host.GetHostVersionResponse{
|
|
79
|
+
Platform: proto.String("Cline CLI"),
|
|
80
|
+
Version: proto.String(""),
|
|
81
|
+
ClineType: proto.String("CLI"),
|
|
82
|
+
ClineVersion: proto.String(global.CliVersion),
|
|
83
|
+
}, nil
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Shutdown initiates a graceful shutdown of the host bridge service
|
|
87
|
+
func (s *EnvService) Shutdown(ctx context.Context, req *cline.EmptyRequest) (*cline.Empty, error) {
|
|
88
|
+
if s.verbose {
|
|
89
|
+
log.Printf("Shutdown requested via RPC")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Trigger global shutdown signal
|
|
93
|
+
select {
|
|
94
|
+
case globalShutdownCh <- struct{}{}:
|
|
95
|
+
if s.verbose {
|
|
96
|
+
log.Printf("Shutdown signal sent successfully")
|
|
97
|
+
}
|
|
98
|
+
default:
|
|
99
|
+
if s.verbose {
|
|
100
|
+
log.Printf("Shutdown signal already pending")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return &cline.Empty{}, nil
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// GetTelemetrySettings returns the telemetry settings for CLI mode
|
|
108
|
+
func (s *EnvService) GetTelemetrySettings(ctx context.Context, req *cline.EmptyRequest) (*host.GetTelemetrySettingsResponse, error) {
|
|
109
|
+
if s.verbose {
|
|
110
|
+
log.Printf("GetTelemetrySettings called")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// In CLI mode, check the POSTHOG_TELEMETRY_ENABLED environment variable
|
|
114
|
+
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
|
|
115
|
+
|
|
116
|
+
var setting host.Setting
|
|
117
|
+
if telemetryEnabled {
|
|
118
|
+
setting = host.Setting_ENABLED
|
|
119
|
+
} else {
|
|
120
|
+
setting = host.Setting_DISABLED
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return &host.GetTelemetrySettingsResponse{
|
|
124
|
+
IsEnabled: setting,
|
|
125
|
+
}, nil
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// SubscribeToTelemetrySettings returns a stream of telemetry setting changes
|
|
129
|
+
// In CLI mode, telemetry settings don't change at runtime, so we just send
|
|
130
|
+
// the current state and keep the stream open
|
|
131
|
+
func (s *EnvService) SubscribeToTelemetrySettings(req *cline.EmptyRequest, stream host.EnvService_SubscribeToTelemetrySettingsServer) error {
|
|
132
|
+
if s.verbose {
|
|
133
|
+
log.Printf("SubscribeToTelemetrySettings called")
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Send initial telemetry state
|
|
137
|
+
telemetryEnabled := os.Getenv("POSTHOG_TELEMETRY_ENABLED") == "true"
|
|
138
|
+
|
|
139
|
+
var setting host.Setting
|
|
140
|
+
if telemetryEnabled {
|
|
141
|
+
setting = host.Setting_ENABLED
|
|
142
|
+
} else {
|
|
143
|
+
setting = host.Setting_DISABLED
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
event := &host.TelemetrySettingsEvent{
|
|
147
|
+
IsEnabled: setting,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if err := stream.Send(event); err != nil {
|
|
151
|
+
if s.verbose {
|
|
152
|
+
log.Printf("Failed to send telemetry settings event: %v", err)
|
|
153
|
+
}
|
|
154
|
+
return err
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Keep stream open until context is cancelled
|
|
158
|
+
// (In CLI mode, settings don't change dynamically)
|
|
159
|
+
<-stream.Context().Done()
|
|
160
|
+
|
|
161
|
+
if s.verbose {
|
|
162
|
+
log.Printf("SubscribeToTelemetrySettings stream closed")
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return nil
|
|
166
|
+
}
|