@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.
Files changed (85) hide show
  1. package/.npmrc.tmp +2 -0
  2. package/README.md +72 -0
  3. package/cmd/cline/main.go +348 -0
  4. package/cmd/cline-host/main.go +71 -0
  5. package/e2e/default_update_test.go +154 -0
  6. package/e2e/helpers_test.go +378 -0
  7. package/e2e/main_test.go +47 -0
  8. package/e2e/mixed_stress_test.go +120 -0
  9. package/e2e/sqlite_helper.go +161 -0
  10. package/e2e/start_list_test.go +178 -0
  11. package/go.mod +64 -0
  12. package/go.sum +162 -0
  13. package/man/cline.1 +331 -0
  14. package/man/cline.1.md +332 -0
  15. package/package.json +54 -0
  16. package/pkg/cli/auth/auth_cline_provider.go +285 -0
  17. package/pkg/cli/auth/auth_menu.go +323 -0
  18. package/pkg/cli/auth/auth_subscription.go +130 -0
  19. package/pkg/cli/auth/byo_quick_setup.go +247 -0
  20. package/pkg/cli/auth/models_cline.go +141 -0
  21. package/pkg/cli/auth/models_list_fetch.go +156 -0
  22. package/pkg/cli/auth/models_list_static.go +69 -0
  23. package/pkg/cli/auth/providers_byo.go +184 -0
  24. package/pkg/cli/auth/providers_list.go +517 -0
  25. package/pkg/cli/auth/update_api_configurations.go +647 -0
  26. package/pkg/cli/auth/wizard_byo.go +764 -0
  27. package/pkg/cli/auth/wizard_byo_bedrock.go +193 -0
  28. package/pkg/cli/auth/wizard_byo_oca.go +366 -0
  29. package/pkg/cli/auth.go +43 -0
  30. package/pkg/cli/clerror/cline_error.go +187 -0
  31. package/pkg/cli/config/manager.go +208 -0
  32. package/pkg/cli/config/settings_renderer.go +198 -0
  33. package/pkg/cli/config.go +152 -0
  34. package/pkg/cli/display/ansi.go +27 -0
  35. package/pkg/cli/display/banner.go +211 -0
  36. package/pkg/cli/display/deduplicator.go +95 -0
  37. package/pkg/cli/display/markdown_renderer.go +139 -0
  38. package/pkg/cli/display/renderer.go +304 -0
  39. package/pkg/cli/display/segment_streamer.go +212 -0
  40. package/pkg/cli/display/streaming.go +134 -0
  41. package/pkg/cli/display/system_renderer.go +269 -0
  42. package/pkg/cli/display/tool_renderer.go +455 -0
  43. package/pkg/cli/display/tool_result_parser.go +371 -0
  44. package/pkg/cli/display/typewriter.go +210 -0
  45. package/pkg/cli/doctor.go +65 -0
  46. package/pkg/cli/global/cline-clients.go +501 -0
  47. package/pkg/cli/global/global.go +113 -0
  48. package/pkg/cli/global/registry.go +304 -0
  49. package/pkg/cli/handlers/ask_handlers.go +339 -0
  50. package/pkg/cli/handlers/handler.go +130 -0
  51. package/pkg/cli/handlers/say_handlers.go +521 -0
  52. package/pkg/cli/instances.go +506 -0
  53. package/pkg/cli/logs.go +382 -0
  54. package/pkg/cli/output/coordinator.go +167 -0
  55. package/pkg/cli/output/input_model.go +497 -0
  56. package/pkg/cli/sqlite/locks.go +366 -0
  57. package/pkg/cli/task/history_handler.go +72 -0
  58. package/pkg/cli/task/input_handler.go +577 -0
  59. package/pkg/cli/task/manager.go +1283 -0
  60. package/pkg/cli/task/settings_parser.go +754 -0
  61. package/pkg/cli/task/stream_coordinator.go +60 -0
  62. package/pkg/cli/task.go +675 -0
  63. package/pkg/cli/terminal/keyboard.go +695 -0
  64. package/pkg/cli/tui/HELP_WANTED.md +1 -0
  65. package/pkg/cli/types/history.go +17 -0
  66. package/pkg/cli/types/messages.go +329 -0
  67. package/pkg/cli/types/state.go +59 -0
  68. package/pkg/cli/updater/updater.go +409 -0
  69. package/pkg/cli/version.go +43 -0
  70. package/pkg/common/constants.go +6 -0
  71. package/pkg/common/schema.go +54 -0
  72. package/pkg/common/types.go +54 -0
  73. package/pkg/common/utils.go +185 -0
  74. package/pkg/generated/field_overrides.go +39 -0
  75. package/pkg/generated/providers.go +1584 -0
  76. package/pkg/hostbridge/diff.go +351 -0
  77. package/pkg/hostbridge/disabled/watch.go +39 -0
  78. package/pkg/hostbridge/disabled/window.go +63 -0
  79. package/pkg/hostbridge/disabled/workspace.go +66 -0
  80. package/pkg/hostbridge/env.go +166 -0
  81. package/pkg/hostbridge/grpc_server.go +113 -0
  82. package/pkg/hostbridge/simple.go +43 -0
  83. package/pkg/hostbridge/simple_workspace.go +85 -0
  84. package/pkg/hostbridge/window.go +129 -0
  85. 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
+ }