diff --git a/cmd/chat.go b/cmd/chat.go index d70b3ef..4a9a334 100644 --- a/cmd/chat.go +++ b/cmd/chat.go @@ -1,185 +1,43 @@ package cmd import ( + "bufio" + "fmt" "os" "strings" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/bubbles/textinput" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/fatih/color" "github.com/spf13/cobra" - "github.com/spf13/viper" "gmgauthier.com/grokkit/internal/grok" ) -type ( - Message struct { - Role string - Content string - } - model struct { - messages []Message - history []map[string]string - input textinput.Model - spinner spinner.Model - viewport viewport.Model - streamCh <-chan string - streaming bool - client *grok.Client - gmodel string - } - streamTickMsg struct{} -) - -var ( - userStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("172")). - Padding(0, 1) - grokStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("45")). - Padding(0, 1) - headerStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("99")). - Bold(true) -) - -func initialModel() *model { - systemContent := "You are Grok, a helpful and maximally truthful AI built by xAI, not based on any other companies and their models." - system := Message{Role: "system", Content: systemContent} - h := []map[string]string{{"role": "system", "content": systemContent}} - ti := textinput.New() - ti.Placeholder = "Type message (Enter send, q/Ctrl+C quit)" - ti.Focus() - sp := spinner.New() - sp.Spinner = spinner.Dot - sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) - vp := viewport.New(0, 0) - vp.MouseWheelEnabled = true - vp.GotoBottom() - return &model{ - messages: []Message{system}, - history: h, - input: ti, - spinner: sp, - viewport: vp, - client: grok.NewClient(), - gmodel: viper.GetString("model"), - } -} - -func (m model) renderMessages() string { - var lines []string - for _, msg := range m.messages { - content := strings.ReplaceAll(msg.Content, "\n", "\n ") - switch msg.Role { - case "user": - lines = append(lines, userStyle.Render("You: "+content)) - case "assistant": - lines = append(lines, grokStyle.Render("Grok: "+content)) - case "system": - lines = append(lines, content) - } - lines = append(lines, "") - } - return strings.Join(lines, "\n") -} - -func streamTickMsgCmd() tea.Cmd { - return func() tea.Msg { - return streamTickMsg{} - } -} - -func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.viewport.Width = msg.Width - if msg.Height > 3 { - m.viewport.Height = msg.Height - 3 - } - m.viewport.SetContent(m.renderMessages()) - return m, nil - case tea.KeyMsg: - if m.streaming { - if msg.String() == "q" || msg.String() == "ctrl+c" || msg.String() == "esc" { - return m, tea.Quit - } - return m, nil - } - switch msg.String() { - case "q", "ctrl+c", "esc": - return m, tea.Quit - case "enter": - text := m.input.Value() - if text == "" { - return m, nil - } - userMsg := Message{Role: "user", Content: text} - m.messages = append(m.messages, userMsg) - m.history = append(m.history, map[string]string{"role": "user", "content": text}) - assMsg := Message{Role: "assistant", Content: ""} - m.messages = append(m.messages, assMsg) - m.input.Reset() - m.streaming = true - m.streamCh = m.client.StreamChan(m.history, m.gmodel) - m.viewport.SetContent(m.renderMessages()) - cmds = append(cmds, streamTickMsgCmd()) - return m, tea.Batch(cmds...) - default: - var cmd tea.Cmd - m.input, cmd = m.input.Update(msg) - return m, cmd - } - case streamTickMsg: - if !m.streaming || m.streamCh == nil { - return m, nil - } - select { - case chunk, ok := <-m.streamCh: - if !ok { - m.streaming = false - m.streamCh = nil - m.viewport.SetContent(m.renderMessages()) - return m, nil - } - m.messages[len(m.messages)-1].Content += chunk - m.viewport.SetContent(m.renderMessages()) - m.viewport.GotoBottom() - return m, streamTickMsgCmd() - default: - return m, streamTickMsgCmd() - } - } - return m, tea.Batch(cmds...) -} - -func (m model) View() string { - header := headerStyle.Render("=== Grokkit TUI Chat === q/Ctrl+C to quit") - vpView := m.viewport.View() - spinnerLine := "" - if m.streaming { - spinnerLine = m.spinner.View() + " Grok typing..." - } - inputLine := m.input.View() - return lipgloss.JoinVertical(lipgloss.Left, header, vpView, spinnerLine, inputLine) -} - -func (m model) Init() tea.Cmd { - return nil -} - var chatCmd = &cobra.Command{ Use: "chat", - Short: "Interactive TUI chat with Grok", + Short: "Interactive streaming chat with Grok", Run: func(cmd *cobra.Command, args []string) { - p := tea.NewProgram(initialModel(), tea.WithAltScreen(), tea.WithMouseCellMotion()) - if _, err := p.Run(); err != nil { - color.Red("Chat failed: %v", err) - os.Exit(1) + client := grok.NewClient() + color.Cyan("Grokkit Chat — type /quit or Ctrl+C to exit\n") + + history := []map[string]string{} + scanner := bufio.NewScanner(os.Stdin) + + for { + fmt.Print(color.YellowString("You: ")) + if !scanner.Scan() { + break + } + input := strings.TrimSpace(scanner.Text()) + if input == "" { + continue + } + if input == "/quit" || input == "/q" { + break + } + + history = append(history, map[string]string{"role": "user", "content": input}) + color.Green("Grok: ") + reply := client.Stream(history, "grok-4") + history = append(history, map[string]string{"role": "assistant", "content": reply}) } }, } diff --git a/cmd/commit.go b/cmd/commit.go index f871bf5..5ab7ccf 100644 --- a/cmd/commit.go +++ b/cmd/commit.go @@ -2,13 +2,10 @@ package cmd import ( "fmt" - "os" "os/exec" - "strings" "github.com/fatih/color" "github.com/spf13/cobra" - "github.com/spf13/viper" "gmgauthier.com/grokkit/internal/git" "gmgauthier.com/grokkit/internal/grok" ) @@ -17,63 +14,32 @@ var commitCmd = &cobra.Command{ Use: "commit", Short: "Generate message and commit staged changes", Run: func(cmd *cobra.Command, args []string) { - model := viper.GetString("model") - - client := grok.NewClient() - - if !git.IsRepo() { - color.Red("Error: Not inside a git repository") - os.Exit(1) - } - - diffOutput := git.Run([]string{"diff", "--cached"}) - - if diffOutput == "" { - color.Yellow("No staged changes. Stage files with `git add` first.") + diff := git.Run([]string{"diff", "--cached", "--no-color"}) + if diff == "" { + color.Yellow("No staged changes!") return } - - systemPrompt := `You are an expert at writing conventional commit messages. - -Analyze the code diff below and generate a concise commit message: - -- Prefix: feat|fix|docs|style|refactor|perf|test|ci|chore -- Imperative mood (e.g., 'Add', 'Fix'), capitalize first letter -- Max 72 chars for subject line -- Optional body lines wrapped at 72 chars -- No trailing period on subject` - + client := grok.NewClient() messages := []map[string]string{ - {"role": "system", "content": systemPrompt}, - {"role": "user", "content": diffOutput}, + {"role": "system", "content": "Return ONLY a conventional commit message (type(scope): subject\n\nbody)."}, + {"role": "user", "content": fmt.Sprintf("Staged changes:\n%s", diff)}, } + color.Yellow("Generating commit message...") + msg := client.Stream(messages, "grok-4") - commitMsg := client.Stream(messages, model) - - fmt.Println(color.YellowString("Suggested commit message:")) - fmt.Println() - fmt.Print(color.CyanString(commitMsg)) - fmt.Println() - - fmt.Print(color.YellowString("Commit now? (y/N): ")) + color.Cyan("\nProposed commit message:\n%s", msg) var confirm string + color.Yellow("Commit with this message? (y/n): ") fmt.Scanln(&confirm) - if strings.ToLower(strings.TrimSpace(confirm)) != "y" { + if confirm != "y" && confirm != "Y" { color.Yellow("Aborted.") return } - cmdOut, err := exec.Command("git", "commit", "-m", commitMsg).CombinedOutput() - if err != nil { - color.Red("Commit failed: %%v\\nOutput: %%s", err, cmdOut) - os.Exit(1) - } - - outputStr := strings.TrimSpace(string(cmdOut)) - if len(outputStr) > 0 { - fmt.Println(color.GreenString("Commit output: ") + outputStr) + if err := exec.Command("git", "commit", "-m", msg).Run(); err != nil { + color.Red("Git commit failed") } else { - color.Green("Committed successfully!") + color.Green("✅ Committed successfully!") } }, } diff --git a/cmd/commitmsg.go b/cmd/commitmsg.go index d1a51c2..658a740 100644 --- a/cmd/commitmsg.go +++ b/cmd/commitmsg.go @@ -2,11 +2,9 @@ package cmd import ( "fmt" - "os" "github.com/fatih/color" "github.com/spf13/cobra" - "github.com/spf13/viper" "gmgauthier.com/grokkit/internal/git" "gmgauthier.com/grokkit/internal/grok" ) @@ -15,42 +13,17 @@ var commitMsgCmd = &cobra.Command{ Use: "commit-msg", Short: "Generate conventional commit message from staged changes", Run: func(cmd *cobra.Command, args []string) { - model := viper.GetString("model") - + diff := git.Run([]string{"diff", "--cached", "--no-color"}) + if diff == "" { + color.Yellow("No staged changes!") + return + } client := grok.NewClient() - - if !git.IsRepo() { - color.Red("Error: Not inside a git repository") - os.Exit(1) - } - - diffOutput := git.Run([]string{"diff", "--cached"}) - - if diffOutput == "" { - color.Red("Error: No staged changes. Stage files with `git add` first.") - os.Exit(1) - } - - systemPrompt := `You are an expert at writing conventional commit messages. - -Analyze the code diff below and generate a concise commit message: - -- Prefix: feat|fix|docs|style|refactor|perf|test|ci|chore -- Imperative mood (e.g., 'Add', 'Fix'), capitalize first letter -- Max 72 chars for subject line -- Optional body lines wrapped at 72 chars -- No trailing period on subject` - messages := []map[string]string{ - {"role": "system", "content": systemPrompt}, - {"role": "user", "content": diffOutput}, + {"role": "system", "content": "Return ONLY a conventional commit message (type(scope): subject\n\nbody)."}, + {"role": "user", "content": fmt.Sprintf("Staged changes:\n%s", diff)}, } - - commitMsg := client.Stream(messages, model) - - fmt.Println(color.YellowString("Suggested commit message:")) - fmt.Println() - fmt.Print(color.CyanString(commitMsg)) - fmt.Println() + color.Yellow("Generating commit message...") + client.Stream(messages, "grok-4") }, } diff --git a/cmd/edit.go b/cmd/edit.go index f81bd24..d9b4a57 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -4,10 +4,6 @@ import ( "fmt" "os" "path/filepath" - "strings" - - "github.com/spf13/viper" - "gmgauthier.com/grokkit/internal/git" "github.com/fatih/color" "github.com/spf13/cobra" @@ -16,114 +12,42 @@ import ( var editCmd = &cobra.Command{ Use: "edit FILE INSTRUCTION", - Short: "Edit a file in-place with Grok", + Short: "Edit a file in-place with Grok (safe preview + backup)", + Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { - if len(args) < 2 { - color.Red("Usage: grokkit edit ") - os.Exit(1) - } - filePath := args[0] - instruction := strings.Join(args[1:], " ") + instruction := args[1] - content, err := os.ReadFile(filePath) - if err != nil { - color.Red("Error reading file %s: %v", filePath, err) + if _, err := os.Stat(filePath); os.IsNotExist(err) { + color.Red("File not found: %s", filePath) os.Exit(1) } + original, _ := os.ReadFile(filePath) backupPath := filePath + ".bak" - if backupErr := os.WriteFile(backupPath, content, 0o644); backupErr != nil { - color.Yellow("Warning: backup failed %s: %v", backupPath, backupErr) - } + _ = os.WriteFile(backupPath, original, 0644) - model := viper.GetString("model") client := grok.NewClient() - - systemPrompt := `You are an expert software engineer. - -Your task is to edit code files based on instructions. - -IMPORTANT: Respond ONLY with the COMPLETE new file content. -No explanations, no markdown, no comments outside the code.` - - userPrompt := fmt.Sprintf(`File: %s -Current content: -%s - -Instruction: %s - -Output ONLY the full updated file content:`, filePath, content, instruction) - messages := []map[string]string{ - {"role": "system", "content": systemPrompt}, - {"role": "user", "content": userPrompt}, + {"role": "system", "content": "Return ONLY the complete updated file content. No explanations, no markdown, no diffs."}, + {"role": "user", "content": fmt.Sprintf("File: %s\n\nOriginal content:\n%s\n\nTask: %s", filepath.Base(filePath), original, instruction)}, } - cleanCodeResponse := func(s string) string { - lines := strings.Split(s, "\n") - if len(lines) > 0 { - first := strings.TrimSpace(lines[0]) - if strings.HasPrefix(first, "```") { - lines = lines[1:] - } - } - if len(lines) > 0 { - last := strings.TrimSpace(lines[len(lines)-1]) - if strings.HasSuffix(last, "```") { - lines = lines[:len(lines)-1] - } - } - return strings.TrimSpace(strings.Join(lines, "\n")) - } + color.Yellow("Asking Grok to %s...", instruction) + newContent := client.Stream(messages, "grok-4-1-fast-non-reasoning") - newContent := cleanCodeResponse(client.Stream(messages, model)) + color.Cyan("\nProposed changes (new file content):") + color.White(newContent) - dir := filepath.Dir(filePath) - base := filepath.Base(filePath) - tmpA, err := os.CreateTemp(dir, "grokkit."+base+".old~") - if err != nil { - color.Red("Temp error: %v", err) - return - } - defer os.Remove(tmpA.Name()) - tmpA.Write(content) - tmpA.Close() - - tmpB, err := os.CreateTemp(dir, "grokkit."+base+".new~") - if err != nil { - color.Red("Temp error: %v", err) - return - } - defer os.Remove(tmpB.Name()) - tmpB.Write([]byte(newContent)) - tmpB.Close() - - diffOut := git.Run([]string{"diff", "--no-index", "--no-color", tmpA.Name(), tmpB.Name()}) - - fmt.Println(color.YellowString("📄 Backup: %s", backupPath)) - fmt.Println() - if diffOut != "" { - fmt.Println(color.YellowString("🔄 Proposed changes:")) - fmt.Print(color.CyanString(diffOut)) - } else { - color.Yellow("No changes.") - } - fmt.Println() - - fmt.Print(color.YellowString("Apply? (y/N): ")) + fmt.Print("\nApply these changes? (y/n): ") var confirm string fmt.Scanln(&confirm) - if strings.ToLower(strings.TrimSpace(confirm)) != "y" { - color.Yellow("Aborted.") + if confirm != "y" && confirm != "Y" { + color.Yellow("Changes discarded. Backup saved as %s", backupPath) return } - if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil { - color.Red("Error writing file: %v", err) - os.Exit(1) - } - - color.Green("File %s updated successfully!", filePath) + _ = os.WriteFile(filePath, []byte(newContent), 0644) + color.Green("✅ Applied successfully! Backup: %s", backupPath) }, } diff --git a/cmd/history.go b/cmd/history.go index e4c73e2..9d8018b 100644 --- a/cmd/history.go +++ b/cmd/history.go @@ -1,12 +1,8 @@ package cmd import ( - "fmt" - "os" - "github.com/fatih/color" "github.com/spf13/cobra" - "github.com/spf13/viper" "gmgauthier.com/grokkit/internal/git" "gmgauthier.com/grokkit/internal/grok" ) @@ -15,42 +11,17 @@ var historyCmd = &cobra.Command{ Use: "history", Short: "Summarize recent git history", Run: func(cmd *cobra.Command, args []string) { - model := viper.GetString("model") - + log := git.Run([]string{"log", "--oneline", "-10"}) + if log == "" { + color.Yellow("No commits found.") + return + } client := grok.NewClient() - - if !git.IsRepo() { - color.Red("Error: Not inside a git repository") - os.Exit(1) - } - - logOutput := git.Run([]string{"log", "--oneline", "-10"}) - - if logOutput == "" { - color.Red("Error: No git history found.") - os.Exit(1) - } - - systemPrompt := `You are a git workflow expert. - -Summarize the recent git history: - -- Key changes and themes -- Potential issues or todos -- Suggestions for next steps or refactoring - -Format as bullet points.` - messages := []map[string]string{ - {"role": "system", "content": systemPrompt}, - {"role": "user", "content": logOutput}, + {"role": "system", "content": "Summarize the recent git history in 3-5 bullet points."}, + {"role": "user", "content": log}, } - - summary := client.Stream(messages, model) - - fmt.Println(color.YellowString("📜 Git History Summary:")) - fmt.Println() - fmt.Print(color.CyanString(summary)) - fmt.Println() + color.Yellow("Summarizing recent commits...") + client.Stream(messages, "grok-4") }, } diff --git a/cmd/prdescribe.go b/cmd/prdescribe.go index 5e98228..39619dc 100644 --- a/cmd/prdescribe.go +++ b/cmd/prdescribe.go @@ -5,7 +5,6 @@ import ( "github.com/fatih/color" "github.com/spf13/cobra" - "github.com/spf13/viper" "gmgauthier.com/grokkit/internal/git" "gmgauthier.com/grokkit/internal/grok" ) @@ -14,39 +13,20 @@ var prDescribeCmd = &cobra.Command{ Use: "pr-describe", Short: "Generate full PR description from current branch", Run: func(cmd *cobra.Command, args []string) { - model := viper.GetString("model") - - client := grok.NewClient() - - if !git.IsRepo() { - color.Yellow("Not a git repo.") + diff := git.Run([]string{"diff", "main..HEAD", "--no-color"}) + if diff == "" { + diff = git.Run([]string{"diff", "origin/main..HEAD", "--no-color"}) } - - logMain := git.Run([]string{"log", "--oneline", "main..HEAD"}) - if logMain == "" { - logMain = git.Run([]string{"log", "--oneline", "origin/main..HEAD"}) - } - diffMain := git.Run([]string{"diff", "main..HEAD"}) - if diffMain == "" { - diffMain = git.Run([]string{"diff", "origin/main..HEAD"}) - } - if logMain == "" && diffMain == "" { - color.Yellow("No changes vs main/origin/main. Nothing to describe.") + if diff == "" { + color.Yellow("No changes on this branch compared to main/origin/main.") return } - - systemPrompt := `Write a professional GitHub PR title + detailed body (changes, motivation, testing notes).` - + client := grok.NewClient() messages := []map[string]string{ - {"role": "system", "content": systemPrompt}, - {"role": "user", "content": "Branch changes:\n" + logMain + "\n\nDiff:\n" + diffMain}, + {"role": "system", "content": "Write a professional GitHub PR title + detailed body (changes, motivation, testing notes)."}, + {"role": "user", "content": fmt.Sprintf("Diff:\n%s", diff)}, } - - prDesc := client.Stream(messages, model) - - fmt.Println(color.YellowString("📝 Suggested PR Description:")) - fmt.Println() - fmt.Print(color.CyanString(prDesc)) - fmt.Println() + color.Yellow("Writing PR description...") + client.Stream(messages, "grok-4") }, } diff --git a/cmd/review.go b/cmd/review.go index a34799f..484b834 100644 --- a/cmd/review.go +++ b/cmd/review.go @@ -5,7 +5,6 @@ import ( "github.com/fatih/color" "github.com/spf13/cobra" - "github.com/spf13/viper" "gmgauthier.com/grokkit/internal/git" "gmgauthier.com/grokkit/internal/grok" ) @@ -14,35 +13,15 @@ var reviewCmd = &cobra.Command{ Use: "review [path]", Short: "Review the current repository or directory", Run: func(cmd *cobra.Command, args []string) { - model := viper.GetString("model") - client := grok.NewClient() - - if !git.IsRepo() { - color.Yellow("Not a git repo — basic analysis only.") - } - + diff := git.Run([]string{"diff", "--no-color"}) status := git.Run([]string{"status", "--short"}) - unstagedDiff := git.Run([]string{"diff"}) - files := git.Run([]string{"ls-files"}) - if files == "" { - files = "No tracked files." - } - context := fmt.Sprintf(`Git status:\n\n%s\n\nUnstaged diff:\n%s\n\nTracked files:\n%s`, status, unstagedDiff, files) - - systemPrompt := `You are an expert code reviewer. -Give a concise summary + 3–5 actionable improvements.` messages := []map[string]string{ - {"role": "system", "content": systemPrompt}, - {"role": "user", "content": "Review this project:\\n" + context}, + {"role": "system", "content": "You are an expert code reviewer. Give a concise summary + 3-5 actionable improvements."}, + {"role": "user", "content": fmt.Sprintf("Git status:\n%s\n\nGit diff:\n%s", status, diff)}, } - - review := client.Stream(messages, model) - - fmt.Println(color.YellowString("🤖 AI Code Review:")) - fmt.Println() - fmt.Print(color.CyanString(review)) - fmt.Println() + color.Yellow("Grok is reviewing the repo...") + client.Stream(messages, "grok-4") }, } diff --git a/go.mod b/go.mod index 54e85cd..77bf382 100644 --- a/go.mod +++ b/go.mod @@ -3,45 +3,24 @@ module gmgauthier.com/grokkit go 1.24.2 require ( - github.com/charmbracelet/bubbles v1.0.0 - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/lipgloss v1.1.0 github.com/fatih/color v1.18.0 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.21.0 ) require ( - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.4.1 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // indirect - github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.9.0 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.5.0 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.28.0 // indirect diff --git a/go.sum b/go.sum index 58860a7..eef275c 100644 --- a/go.sum +++ b/go.sum @@ -1,32 +1,6 @@ -github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= -github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= -github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= -github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= -github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= -github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= -github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= -github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= -github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= -github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= -github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -43,29 +17,15 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= -github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -88,13 +48,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= diff --git a/grokkit b/grokkit new file mode 100755 index 0000000..6fa5e57 Binary files /dev/null and b/grokkit differ diff --git a/internal/git/helper.go b/internal/git/helper.go index d243855..4f7f897 100644 --- a/internal/git/helper.go +++ b/internal/git/helper.go @@ -1,13 +1,10 @@ package git -import ( - "os/exec" - "strings" -) +import "os/exec" func Run(args []string) string { - out, _ := exec.Command("git", args...).CombinedOutput() - return strings.TrimSpace(string(out)) + out, _ := exec.Command("git", args...).Output() + return string(out) } func IsRepo() bool { diff --git a/internal/grok/client.go b/internal/grok/client.go index 0ad6d43..89024f2 100644 --- a/internal/grok/client.go +++ b/internal/grok/client.go @@ -5,7 +5,6 @@ import ( "bytes" "encoding/json" "fmt" - "io" "net/http" "os" "strings" @@ -49,31 +48,26 @@ func (c *Client) Stream(messages []map[string]string, model string) string { color.Red("Request failed: %v", err) os.Exit(1) } - if resp.StatusCode != http.StatusOK { - bodyBytes, readErr := io.ReadAll(resp.Body) - if readErr != nil { - color.Red("Failed to read response body: %v", readErr) + defer func() { + if cerr := resp.Body.Close(); cerr != nil { + color.Yellow("Warning: failed to close response body: %v", cerr) } - color.Red("API failed with status %d: %s", resp.StatusCode, string(bodyBytes)) - resp.Body.Close() - os.Exit(1) - } - defer resp.Body.Close() + }() var fullReply strings.Builder scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "data: ") { - data := line[6:] + data := strings.TrimPrefix(line, "data: ") if data == "[DONE]" { break } - var chunk map[string]interface{} + var chunk map[string]any if json.Unmarshal([]byte(data), &chunk) == nil { - if choices, ok := chunk["choices"].([]interface{}); ok && len(choices) > 0 { - if delta, ok := choices[0].(map[string]interface{})["delta"].(map[string]interface{}); ok { - if content, ok := delta["content"].(string); ok { + if choices, ok := chunk["choices"].([]any); ok && len(choices) > 0 { + if delta, ok := choices[0].(map[string]any)["delta"].(map[string]any); ok { + if content, ok := delta["content"].(string); ok && content != "" { fmt.Print(content) fullReply.WriteString(content) } @@ -85,54 +79,3 @@ func (c *Client) Stream(messages []map[string]string, model string) string { fmt.Println() return fullReply.String() } - -func (c *Client) StreamChan(messages []map[string]string, model string) <-chan string { - ch := make(chan string, 100) - go func() { - defer close(ch) - url := c.BaseURL + "/chat/completions" - payload := map[string]interface{}{ - "model": model, - "messages": messages, - "temperature": 0.7, - "stream": true, - } - body, _ := json.Marshal(payload) - req, _ := http.NewRequest("POST", url, bytes.NewReader(body)) - req.Header.Set("Authorization", "Bearer "+c.APIKey) - req.Header.Set("Content-Type", "application/json") - resp, err := http.DefaultClient.Do(req) - if err != nil { - color.Red("Request failed: %v", err) - return - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - bodyBytes, _ := io.ReadAll(resp.Body) - color.Red("API failed with status %d: %s", resp.StatusCode, string(bodyBytes)) - return - } - scanner := bufio.NewScanner(resp.Body) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "data: ") { - data := line[6:] - if data == "[DONE]" { - break - } - var chunk map[string]interface{} - if json.Unmarshal([]byte(data), &chunk) != nil { - continue - } - if choices, ok := chunk["choices"].([]interface{}); ok && len(choices) > 0 { - if delta, ok := choices[0].(map[string]interface{})["delta"].(map[string]interface{}); ok { - if content, ok := delta["content"].(string); ok && content != "" { - ch <- content - } - } - } - } - } - }() - return ch -}