@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,497 @@
1
+ package output
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "os/exec"
7
+ "strings"
8
+
9
+ "github.com/charmbracelet/bubbles/textarea"
10
+ tea "github.com/charmbracelet/bubbletea"
11
+ "github.com/charmbracelet/lipgloss"
12
+ )
13
+
14
+ // InputType represents the type of input being collected
15
+ type InputType int
16
+
17
+ const INPUT_WIDTH = 46
18
+
19
+ const (
20
+ InputTypeMessage InputType = iota
21
+ InputTypeApproval
22
+ InputTypeFeedback
23
+ )
24
+
25
+ // InputSubmitMsg is sent when the user submits input
26
+ type InputSubmitMsg struct {
27
+ Value string
28
+ InputType InputType
29
+ Approved bool // For approval type
30
+ NeedsFeedback bool // For approval type
31
+ NoAskAgain bool // For approval type - indicates "don't ask again" was selected
32
+ }
33
+
34
+ // InputCancelMsg is sent when the user cancels input (Ctrl+C)
35
+ type InputCancelMsg struct{}
36
+
37
+ // ChangeInputTypeMsg changes the current input type
38
+ type ChangeInputTypeMsg struct {
39
+ InputType InputType
40
+ Title string
41
+ Placeholder string
42
+ }
43
+
44
+ // editorFinishedMsg is sent when the external editor finishes
45
+ type editorFinishedMsg struct {
46
+ content []byte
47
+ err error
48
+ }
49
+
50
+ // InputModel is the bubbletea model for interactive input
51
+ type InputModel struct {
52
+ textarea textarea.Model
53
+ suspended bool
54
+ savedValue string
55
+ inputType InputType
56
+ title string
57
+ placeholder string
58
+ currentMode string // "plan" or "act"
59
+ width int
60
+ lastHeight int // Track height for cleanup on submit
61
+
62
+ // For approval type
63
+ approvalOptions []string
64
+ selectedOption int
65
+ pendingApproval bool // Stores approval decision when transitioning to feedback input
66
+
67
+ // Styles (huh-inspired theme)
68
+ styles fieldStyles
69
+ }
70
+
71
+ // fieldStyles holds the styling for the input field
72
+ type fieldStyles struct {
73
+ base lipgloss.Style
74
+ title lipgloss.Style
75
+ textArea lipgloss.Style
76
+ cursor lipgloss.Style
77
+ placeholder lipgloss.Style
78
+ selector lipgloss.Style
79
+ selectedOption lipgloss.Style
80
+ option lipgloss.Style
81
+ }
82
+
83
+ // newFieldStyles creates huh-inspired styles (Charm theme)
84
+ func newFieldStyles() fieldStyles {
85
+ // Charm theme colors
86
+ indigo := lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"}
87
+ fuchsia := lipgloss.Color("#F780E2")
88
+ normalFg := lipgloss.AdaptiveColor{Light: "235", Dark: "252"}
89
+ green := lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"}
90
+
91
+ return fieldStyles{
92
+ base: lipgloss.NewStyle().
93
+ PaddingLeft(1).
94
+ BorderStyle(lipgloss.ThickBorder()).
95
+ BorderLeft(true).
96
+ BorderForeground(lipgloss.Color("238")),
97
+ title: lipgloss.NewStyle().
98
+ Foreground(indigo).
99
+ Bold(true),
100
+ textArea: lipgloss.NewStyle().
101
+ Foreground(normalFg),
102
+ cursor: lipgloss.NewStyle().
103
+ Foreground(green),
104
+ placeholder: lipgloss.NewStyle().
105
+ Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"}),
106
+ selector: lipgloss.NewStyle().
107
+ Foreground(fuchsia).
108
+ SetString("> "),
109
+ selectedOption: lipgloss.NewStyle().
110
+ Foreground(normalFg),
111
+ option: lipgloss.NewStyle().
112
+ Foreground(normalFg),
113
+ }
114
+ }
115
+
116
+ // NewInputModel creates a new input model
117
+ func NewInputModel(inputType InputType, title, placeholder, currentMode string) InputModel {
118
+ ta := textarea.New()
119
+ ta.Placeholder = placeholder
120
+ ta.Focus()
121
+ ta.CharLimit = 0
122
+ ta.ShowLineNumbers = false
123
+ ta.Prompt = "" // Remove prompt prefix (this is what adds the inner border!)
124
+ ta.SetHeight(5)
125
+ // Don't set width here - let WindowSizeMsg handle it
126
+ ta.SetWidth(INPUT_WIDTH)
127
+
128
+ // Configure keybindings like huh does:
129
+ // alt+enter and ctrl+j for newlines (textarea will handle these)
130
+ ta.KeyMap.InsertNewline.SetKeys("alt+enter", "ctrl+j")
131
+
132
+ // Apply huh-like styling
133
+ styles := newFieldStyles()
134
+
135
+ // Set cursor color based on mode
136
+ cursorColor := lipgloss.Color("3") // Yellow for plan
137
+ if currentMode == "act" {
138
+ cursorColor = lipgloss.Color("39") // Blue for act
139
+ }
140
+
141
+ ta.FocusedStyle.CursorLine = lipgloss.NewStyle() // No cursor line highlighting
142
+ ta.FocusedStyle.EndOfBuffer = lipgloss.NewStyle() // No end-of-buffer styling
143
+ ta.FocusedStyle.Placeholder = styles.placeholder
144
+ ta.FocusedStyle.Text = styles.textArea
145
+ ta.FocusedStyle.Prompt = lipgloss.NewStyle() // No prompt styling
146
+ ta.Cursor.Style = lipgloss.NewStyle().Foreground(cursorColor)
147
+ ta.Cursor.TextStyle = styles.textArea
148
+
149
+ m := InputModel{
150
+ textarea: ta,
151
+ inputType: inputType,
152
+ title: title,
153
+ placeholder: placeholder,
154
+ currentMode: currentMode,
155
+ width: 0, // Will be set by first WindowSizeMsg
156
+ styles: styles,
157
+ }
158
+
159
+ // For approval type, set up options
160
+ if inputType == InputTypeApproval {
161
+ m.approvalOptions = []string{
162
+ "Yes",
163
+ "Yes, and don't ask again for this task",
164
+ "No, with feedback",
165
+ }
166
+ m.selectedOption = 0
167
+ }
168
+
169
+ return m
170
+ }
171
+
172
+ // Init initializes the model
173
+ func (m *InputModel) Init() tea.Cmd {
174
+ return textarea.Blink
175
+ }
176
+
177
+ // Update handles messages
178
+ func (m *InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
179
+ var cmd tea.Cmd
180
+
181
+ switch msg := msg.(type) {
182
+ case editorFinishedMsg:
183
+ // External editor finished
184
+ if msg.err == nil && len(msg.content) > 0 {
185
+ m.textarea.SetValue(string(msg.content))
186
+ }
187
+ return m, nil
188
+
189
+ case SuspendInputMsg:
190
+ // Save current value and suspend
191
+ m.savedValue = m.textarea.Value()
192
+ m.suspended = true
193
+ return m, tea.ClearScreen
194
+
195
+ case ResumeInputMsg:
196
+ // Restore value and resume
197
+ m.textarea.SetValue(m.savedValue)
198
+ m.suspended = false
199
+ return m, nil
200
+
201
+ case ChangeInputTypeMsg:
202
+ // Change input type (e.g., from approval to feedback)
203
+ m.inputType = msg.InputType
204
+ m.title = msg.Title
205
+ m.placeholder = msg.Placeholder
206
+ m.textarea.Placeholder = msg.Placeholder
207
+ m.textarea.SetValue("")
208
+ m.textarea.Focus()
209
+
210
+ if msg.InputType == InputTypeApproval {
211
+ m.approvalOptions = []string{
212
+ "Yes",
213
+ "Yes, and don't ask again for this task",
214
+ "No, with feedback",
215
+ }
216
+ m.selectedOption = 0
217
+ }
218
+ return m, nil
219
+
220
+ default:
221
+ // Forward all other messages to textarea (including blink ticks)
222
+ if !m.suspended && (m.inputType == InputTypeMessage || m.inputType == InputTypeFeedback) {
223
+ m.textarea, cmd = m.textarea.Update(msg)
224
+ return m, cmd
225
+ }
226
+
227
+ case tea.KeyMsg:
228
+ if m.suspended {
229
+ return m, nil
230
+ }
231
+
232
+ // Handle keys for text input types (Message/Feedback)
233
+ if m.inputType == InputTypeMessage || m.inputType == InputTypeFeedback {
234
+ switch msg.String() {
235
+ case "ctrl+c":
236
+ return m, func() tea.Msg { return InputCancelMsg{} }
237
+
238
+ case "ctrl+e":
239
+ // Open external editor (like huh does)
240
+ return m, m.openEditor()
241
+
242
+ case "enter":
243
+ // Intercept enter for submit (textarea handles alt+enter and ctrl+j for newlines)
244
+ return m.handleSubmit()
245
+
246
+ case "up", "down", "left", "right":
247
+ // Let textarea handle navigation
248
+ m.textarea, cmd = m.textarea.Update(msg)
249
+ return m, cmd
250
+ }
251
+
252
+ // Pass all other keys to textarea (including alt+enter, ctrl+j for newlines)
253
+ m.textarea, cmd = m.textarea.Update(msg)
254
+ return m, cmd
255
+ }
256
+
257
+ // Handle keys for approval type
258
+ if m.inputType == InputTypeApproval {
259
+ switch msg.String() {
260
+ case "ctrl+c":
261
+ return m, func() tea.Msg { return InputCancelMsg{} }
262
+
263
+ case "enter":
264
+ return m.handleSubmit()
265
+
266
+ case "up":
267
+ if m.selectedOption > 0 {
268
+ m.selectedOption--
269
+ }
270
+ return m, nil
271
+
272
+ case "down":
273
+ if m.selectedOption < len(m.approvalOptions)-1 {
274
+ m.selectedOption++
275
+ }
276
+ return m, nil
277
+ }
278
+ }
279
+ }
280
+
281
+ return m, nil
282
+ }
283
+
284
+ // handleSubmit handles submission based on input type
285
+ func (m *InputModel) handleSubmit() (tea.Model, tea.Cmd) {
286
+ switch m.inputType {
287
+ case InputTypeMessage:
288
+ value := strings.TrimSpace(m.textarea.Value())
289
+ return m, func() tea.Msg {
290
+ return InputSubmitMsg{
291
+ Value: value,
292
+ InputType: InputTypeMessage,
293
+ }
294
+ }
295
+
296
+ case InputTypeApproval:
297
+ selected := m.approvalOptions[m.selectedOption]
298
+ approved := strings.HasPrefix(selected, "Yes")
299
+ needsFeedback := strings.Contains(selected, "feedback")
300
+ noAskAgain := strings.Contains(selected, "don't ask again")
301
+
302
+ if needsFeedback {
303
+ // Store the approval decision before switching to feedback input
304
+ m.pendingApproval = approved
305
+ // Switch to feedback input
306
+ return m, func() tea.Msg {
307
+ return ChangeInputTypeMsg{
308
+ InputType: InputTypeFeedback,
309
+ Title: "Your feedback",
310
+ Placeholder: "/plan or /act to switch modes\nctrl+e to open editor",
311
+ }
312
+ }
313
+ }
314
+
315
+ return m, func() tea.Msg {
316
+ return InputSubmitMsg{
317
+ Value: "",
318
+ InputType: InputTypeApproval,
319
+ Approved: approved,
320
+ NeedsFeedback: false,
321
+ NoAskAgain: noAskAgain,
322
+ }
323
+ }
324
+
325
+ case InputTypeFeedback:
326
+ value := strings.TrimSpace(m.textarea.Value())
327
+ return m, func() tea.Msg {
328
+ return InputSubmitMsg{
329
+ Value: value,
330
+ InputType: InputTypeFeedback,
331
+ Approved: m.pendingApproval, // Pass the stored approval decision
332
+ }
333
+ }
334
+ }
335
+
336
+ return m, nil
337
+ }
338
+
339
+ // View renders the model
340
+ func (m *InputModel) View() string {
341
+ if m.suspended {
342
+ return ""
343
+ }
344
+
345
+ var parts []string
346
+
347
+ // Render title with mode indicator
348
+ yellow := lipgloss.Color("3")
349
+ blue := lipgloss.Color("39")
350
+
351
+ modeStyle := lipgloss.NewStyle().Bold(true)
352
+ if m.currentMode == "plan" {
353
+ modeStyle = modeStyle.Foreground(yellow)
354
+ } else {
355
+ modeStyle = modeStyle.Foreground(blue)
356
+ }
357
+
358
+ modeIndicator := modeStyle.Render(fmt.Sprintf("[%s mode]", m.currentMode))
359
+ titleText := m.styles.title.Render(m.title)
360
+ fullTitle := fmt.Sprintf("%s %s", modeIndicator, titleText)
361
+ parts = append(parts, fullTitle)
362
+
363
+ // Render based on input type
364
+ switch m.inputType {
365
+ case InputTypeMessage, InputTypeFeedback:
366
+ parts = append(parts, m.textarea.View())
367
+
368
+ case InputTypeApproval:
369
+ var options []string
370
+ for i, option := range m.approvalOptions {
371
+ if i == m.selectedOption {
372
+ options = append(options, m.styles.selector.Render("")+m.styles.selectedOption.Render(option))
373
+ } else {
374
+ options = append(options, " "+m.styles.option.Render(option))
375
+ }
376
+ }
377
+ parts = append(parts, strings.Join(options, "\n"))
378
+ }
379
+
380
+ // Wrap everything in the base style with border
381
+ content := strings.Join(parts, "\n")
382
+ rendered := m.styles.base.Render(content)
383
+
384
+ // Add newline before the form (outside the border)
385
+ rendered = "\n" + rendered
386
+
387
+ // Track height for cleanup
388
+ m.lastHeight = lipgloss.Height(rendered)
389
+
390
+ return rendered
391
+ }
392
+
393
+ // ClearScreen returns the ANSI codes to clear the input from the terminal
394
+ // This is used when submitting to remove the form cleanly
395
+ func (m *InputModel) ClearScreen() string {
396
+ if m.lastHeight == 0 {
397
+ return ""
398
+ }
399
+
400
+ // Move cursor up by lastHeight lines and clear from cursor to end of screen
401
+ return fmt.Sprintf("\033[%dA\033[J", m.lastHeight)
402
+ }
403
+
404
+ // Clone creates a deep copy of the InputModel with all state preserved
405
+ func (m *InputModel) Clone() *InputModel {
406
+ // Create new textarea with same configuration
407
+ ta := textarea.New()
408
+ ta.SetValue(m.textarea.Value())
409
+ ta.Placeholder = m.placeholder
410
+ ta.CharLimit = 0
411
+ ta.ShowLineNumbers = false
412
+ ta.Prompt = ""
413
+ ta.SetHeight(5)
414
+ ta.SetWidth(INPUT_WIDTH)
415
+ ta.Focus()
416
+
417
+ // Configure keybindings
418
+ ta.KeyMap.InsertNewline.SetKeys("alt+enter", "ctrl+j")
419
+
420
+ // Apply styles (including mode-based cursor color)
421
+ cursorColor := lipgloss.Color("3") // Yellow for plan
422
+ if m.currentMode == "act" {
423
+ cursorColor = lipgloss.Color("39") // Blue for act
424
+ }
425
+
426
+ ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
427
+ ta.FocusedStyle.EndOfBuffer = lipgloss.NewStyle()
428
+ ta.FocusedStyle.Placeholder = m.styles.placeholder
429
+ ta.FocusedStyle.Text = m.styles.textArea
430
+ ta.FocusedStyle.Prompt = lipgloss.NewStyle()
431
+ ta.Cursor.Style = lipgloss.NewStyle().Foreground(cursorColor)
432
+ ta.Cursor.TextStyle = m.styles.textArea
433
+
434
+ // Create cloned model
435
+ clone := &InputModel{
436
+ textarea: ta,
437
+ suspended: false, // New program starts unsuspended
438
+ savedValue: m.savedValue,
439
+ inputType: m.inputType,
440
+ title: m.title,
441
+ placeholder: m.placeholder,
442
+ currentMode: m.currentMode,
443
+ width: m.width,
444
+ lastHeight: m.lastHeight,
445
+ approvalOptions: m.approvalOptions,
446
+ selectedOption: m.selectedOption,
447
+ pendingApproval: m.pendingApproval, // Preserve approval decision
448
+ styles: m.styles,
449
+ }
450
+
451
+ return clone
452
+ }
453
+
454
+ // openEditor opens an external editor for composing the message
455
+ func (m *InputModel) openEditor() tea.Cmd {
456
+ // Get editor from environment or use nano as default
457
+ editorCmd := "nano"
458
+ editorArgs := []string{}
459
+
460
+ if editor := os.Getenv("EDITOR"); editor != "" {
461
+ editorFields := strings.Fields(editor)
462
+ if len(editorFields) > 0 {
463
+ editorCmd = editorFields[0]
464
+ if len(editorFields) > 1 {
465
+ editorArgs = editorFields[1:]
466
+ }
467
+ }
468
+ }
469
+
470
+ // Create temp file with current content
471
+ tmpFile, err := os.CreateTemp(os.TempDir(), "*.md")
472
+ if err != nil {
473
+ return func() tea.Msg {
474
+ return editorFinishedMsg{err: err}
475
+ }
476
+ }
477
+
478
+ // Write current textarea value to temp file
479
+ if err := os.WriteFile(tmpFile.Name(), []byte(m.textarea.Value()), 0o644); err != nil {
480
+ return func() tea.Msg {
481
+ return editorFinishedMsg{err: err}
482
+ }
483
+ }
484
+
485
+ // Open the editor
486
+ cmd := exec.Command(editorCmd, append(editorArgs, tmpFile.Name())...)
487
+ return tea.ExecProcess(cmd, func(err error) tea.Msg {
488
+ content, readErr := os.ReadFile(tmpFile.Name())
489
+ _ = os.Remove(tmpFile.Name())
490
+
491
+ if readErr != nil {
492
+ return editorFinishedMsg{err: readErr}
493
+ }
494
+
495
+ return editorFinishedMsg{content: content, err: err}
496
+ })
497
+ }