diff --git a/.gitignore b/.gitignore index 9f11b75..41f7f34 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea/ +build/ diff --git a/REAMDE.md b/REAMDE.md index e69de29..d822174 100644 --- a/REAMDE.md +++ b/REAMDE.md @@ -0,0 +1,82 @@ +# Grokkit + +Grokkit is a fast Go CLI integrating Grok AI with git workflows and general chat/edit. + +## 🚀 Quick Start + +```bash +export XAI_API_KEY=sk-... + +go install gmgauthier.com/grokkit@grokkit # or build locally + +grokkit --help +``` + +## 📁 Config (optional) + +`~/.config/grokkit/grokkit.yaml`: +```yaml +model: grok-4 +chat: + history_file: ~/.config/grokkit/chat_history.json +``` + +## Commands + +### 💬 `grokkit chat` +Interactive TUI chat with Grok (bubbletea: styled scrolling viewport, live streaming, mouse support). + +``` +grokkit chat +``` + +### 📝 `grokkit commitmsg` +Generate conventional commit from staged changes. + +``` +git add . +grokkit commitmsg +``` + +### ✅ `grokkit commit` +Generate & commit. + +``` +grokkit commit +``` + +### 🔍 `grokkit review` +AI code review of staged changes. + +``` +grokkit review +``` + +### 📋 `grokkit pr-describe` +PR description from branch vs main. + +``` +grokkit pr-describe +``` + +### 📜 `grokkit history` +Summarize recent commits. + +``` +grokkit history +``` + +### ✏️ `grokkit edit FILE "instruction"` +AI edit file. + +``` +grokkit edit main.go "add error handling" +``` + +## Flags + +`--model, -m`: grok-4 (default), etc. + +## License + +[Unlicense](https://unlicense.org/) \ No newline at end of file diff --git a/cmd/chat.go b/cmd/chat.go index 0c03f23..d70b3ef 100644 --- a/cmd/chat.go +++ b/cmd/chat.go @@ -1,16 +1,185 @@ package cmd import ( + "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 streaming chat with Grok", + Short: "Interactive TUI chat with Grok", Run: func(cmd *cobra.Command, args []string) { - client := grok.NewClient() - // TODO: add history + loop (we can expand this later) - // For now, basic streaming chat can be added in next iteration + p := tea.NewProgram(initialModel(), tea.WithAltScreen(), tea.WithMouseCellMotion()) + if _, err := p.Run(); err != nil { + color.Red("Chat failed: %v", err) + os.Exit(1) + } }, } diff --git a/cmd/commit.go b/cmd/commit.go index a22211e..f871bf5 100644 --- a/cmd/commit.go +++ b/cmd/commit.go @@ -1,11 +1,79 @@ package cmd -import "github.com/spf13/cobra" +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" +) var commitCmd = &cobra.Command{ Use: "commit", Short: "Generate message and commit staged changes", Run: func(cmd *cobra.Command, args []string) { - // TODO: implement + 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.") + 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` + + messages := []map[string]string{ + {"role": "system", "content": systemPrompt}, + {"role": "user", "content": diffOutput}, + } + + 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): ")) + var confirm string + fmt.Scanln(&confirm) + if strings.ToLower(strings.TrimSpace(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) + } else { + color.Green("Committed successfully!") + } }, } diff --git a/cmd/commitmsg.go b/cmd/commitmsg.go index 90b7c19..d1a51c2 100644 --- a/cmd/commitmsg.go +++ b/cmd/commitmsg.go @@ -1,11 +1,56 @@ package cmd -import "github.com/spf13/cobra" +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" +) var commitMsgCmd = &cobra.Command{ Use: "commit-msg", Short: "Generate conventional commit message from staged changes", Run: func(cmd *cobra.Command, args []string) { - // TODO: implement + 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.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}, + } + + commitMsg := client.Stream(messages, model) + + fmt.Println(color.YellowString("Suggested commit message:")) + fmt.Println() + fmt.Print(color.CyanString(commitMsg)) + fmt.Println() }, } diff --git a/cmd/edit.go b/cmd/edit.go index ecbfa41..f81bd24 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -1,11 +1,129 @@ package cmd -import "github.com/spf13/cobra" +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/viper" + "gmgauthier.com/grokkit/internal/git" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "gmgauthier.com/grokkit/internal/grok" +) var editCmd = &cobra.Command{ Use: "edit FILE INSTRUCTION", Short: "Edit a file in-place with Grok", Run: func(cmd *cobra.Command, args []string) { - // TODO: implement + if len(args) < 2 { + color.Red("Usage: grokkit edit ") + os.Exit(1) + } + + filePath := args[0] + instruction := strings.Join(args[1:], " ") + + content, err := os.ReadFile(filePath) + if err != nil { + color.Red("Error reading file %s: %v", filePath, err) + os.Exit(1) + } + + backupPath := filePath + ".bak" + if backupErr := os.WriteFile(backupPath, content, 0o644); backupErr != nil { + color.Yellow("Warning: backup failed %s: %v", backupPath, backupErr) + } + + 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}, + } + + 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")) + } + + newContent := cleanCodeResponse(client.Stream(messages, model)) + + 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): ")) + var confirm string + fmt.Scanln(&confirm) + if strings.ToLower(strings.TrimSpace(confirm)) != "y" { + color.Yellow("Aborted.") + 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) }, } diff --git a/cmd/history.go b/cmd/history.go index 207b6d3..e4c73e2 100644 --- a/cmd/history.go +++ b/cmd/history.go @@ -1,11 +1,56 @@ package cmd -import "github.com/spf13/cobra" +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" +) var historyCmd = &cobra.Command{ Use: "history", Short: "Summarize recent git history", Run: func(cmd *cobra.Command, args []string) { - // TODO: implement + model := viper.GetString("model") + + 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}, + } + + summary := client.Stream(messages, model) + + fmt.Println(color.YellowString("📜 Git History Summary:")) + fmt.Println() + fmt.Print(color.CyanString(summary)) + fmt.Println() }, } diff --git a/cmd/prdescribe.go b/cmd/prdescribe.go index 21f4b88..5e98228 100644 --- a/cmd/prdescribe.go +++ b/cmd/prdescribe.go @@ -1,11 +1,52 @@ package cmd -import "github.com/spf13/cobra" +import ( + "fmt" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gmgauthier.com/grokkit/internal/git" + "gmgauthier.com/grokkit/internal/grok" +) var prDescribeCmd = &cobra.Command{ Use: "pr-describe", Short: "Generate full PR description from current branch", Run: func(cmd *cobra.Command, args []string) { - // TODO: implement + model := viper.GetString("model") + + client := grok.NewClient() + + if !git.IsRepo() { + color.Yellow("Not a git repo.") + } + + 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.") + return + } + + systemPrompt := `Write a professional GitHub PR title + detailed body (changes, motivation, testing notes).` + + messages := []map[string]string{ + {"role": "system", "content": systemPrompt}, + {"role": "user", "content": "Branch changes:\n" + logMain + "\n\nDiff:\n" + diffMain}, + } + + prDesc := client.Stream(messages, model) + + fmt.Println(color.YellowString("📝 Suggested PR Description:")) + fmt.Println() + fmt.Print(color.CyanString(prDesc)) + fmt.Println() }, } diff --git a/cmd/review.go b/cmd/review.go index 42fb489..a34799f 100644 --- a/cmd/review.go +++ b/cmd/review.go @@ -1,11 +1,48 @@ package cmd -import "github.com/spf13/cobra" +import ( + "fmt" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gmgauthier.com/grokkit/internal/git" + "gmgauthier.com/grokkit/internal/grok" +) var reviewCmd = &cobra.Command{ Use: "review [path]", Short: "Review the current repository or directory", Run: func(cmd *cobra.Command, args []string) { - // TODO: implement + model := viper.GetString("model") + + client := grok.NewClient() + + if !git.IsRepo() { + color.Yellow("Not a git repo — basic analysis only.") + } + + 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}, + } + + review := client.Stream(messages, model) + + fmt.Println(color.YellowString("🤖 AI Code Review:")) + fmt.Println() + fmt.Print(color.CyanString(review)) + fmt.Println() }, } diff --git a/cmd/root.go b/cmd/root.go index da76c3a..963718f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,8 @@ import ( "os" "github.com/spf13/cobra" + "github.com/spf13/viper" + "gmgauthier.com/grokkit/config" ) var rootCmd = &cobra.Command{ @@ -19,6 +21,10 @@ func Execute() { } func init() { + config.InitConfig() + rootCmd.PersistentFlags().StringP("model", "m", "grok-4", "Grok model (grok-4, grok-3, etc.)") + viper.BindPFlag("model", rootCmd.PersistentFlags().Lookup("model")) + rootCmd.AddCommand(chatCmd) rootCmd.AddCommand(editCmd) rootCmd.AddCommand(reviewCmd) diff --git a/config/config.go b/config/config.go index e69de29..7d47b0e 100644 --- a/config/config.go +++ b/config/config.go @@ -0,0 +1,18 @@ +package config + +import ( + "os" + + "github.com/spf13/viper" +) + +func InitConfig() { + viper.SetConfigName("grokkit") + viper.SetConfigType("yaml") + home, _ := os.UserHomeDir() + viper.AddConfigPath(home + "/.config/grokkit") + viper.SetDefault("model", "grok-4") + viper.SetDefault("chat.history_file", home+"/.config/grokkit/chat_history.json") + viper.AutomaticEnv() + _ = viper.ReadInConfig() +} diff --git a/go.mod b/go.mod index 4fa213a..54e85cd 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,48 @@ module gmgauthier.com/grokkit -go 1.24 +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/spf13/pflag v1.0.6 // indirect - golang.org/x/sys v0.25.0 // 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 3ce2459..58860a7 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,108 @@ +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= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/git/helper.go b/internal/git/helper.go index 170a598..d243855 100644 --- a/internal/git/helper.go +++ b/internal/git/helper.go @@ -6,7 +6,7 @@ import ( ) func Run(args []string) string { - out, _ := exec.Command("git", args...).Output() + out, _ := exec.Command("git", args...).CombinedOutput() return strings.TrimSpace(string(out)) } diff --git a/internal/grok/client.go b/internal/grok/client.go index 0d9862a..0ad6d43 100644 --- a/internal/grok/client.go +++ b/internal/grok/client.go @@ -5,7 +5,7 @@ import ( "bytes" "encoding/json" "fmt" - _ "io" + "io" "net/http" "os" "strings" @@ -46,7 +46,16 @@ func (c *Client) Stream(messages []map[string]string, model string) string { resp, err := http.DefaultClient.Do(req) if err != nil { - color.Red("Error: %v", err) + 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) + } + color.Red("API failed with status %d: %s", resp.StatusCode, string(bodyBytes)) + resp.Body.Close() os.Exit(1) } defer resp.Body.Close() @@ -76,3 +85,54 @@ 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 +}