diff --git a/cmd/chat.go b/cmd/chat.go index 09f7961..0c03f23 100644 --- a/cmd/chat.go +++ b/cmd/chat.go @@ -1,11 +1,16 @@ package cmd -import "github.com/spf13/cobra" +import ( + "github.com/spf13/cobra" + "gmgauthier.com/grokkit/internal/grok" +) var chatCmd = &cobra.Command{ Use: "chat", Short: "Interactive streaming chat with Grok", Run: func(cmd *cobra.Command, args []string) { - // TODO: implement + client := grok.NewClient() + // TODO: add history + loop (we can expand this later) + // For now, basic streaming chat can be added in next iteration }, } diff --git a/go.mod b/go.mod index 86c26a8..4fa213a 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,15 @@ module gmgauthier.com/grokkit go 1.24 -require github.com/spf13/cobra v1.9.1 +require ( + github.com/fatih/color v1.18.0 + github.com/spf13/cobra v1.9.1 +) require ( github.com/inconshreveable/mousetrap v1.1.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 ) diff --git a/go.sum b/go.sum index ffae55e..3ce2459 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,21 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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= +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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 e69de29..170a598 100644 --- a/internal/git/helper.go +++ b/internal/git/helper.go @@ -0,0 +1,16 @@ +package git + +import ( + "os/exec" + "strings" +) + +func Run(args []string) string { + out, _ := exec.Command("git", args...).Output() + return strings.TrimSpace(string(out)) +} + +func IsRepo() bool { + _, err := exec.Command("git", "rev-parse", "--is-inside-work-tree").Output() + return err == nil +} diff --git a/internal/grok/client.go b/internal/grok/client.go index e69de29..0d9862a 100644 --- a/internal/grok/client.go +++ b/internal/grok/client.go @@ -0,0 +1,78 @@ +package grok + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + _ "io" + "net/http" + "os" + "strings" + + "github.com/fatih/color" +) + +type Client struct { + APIKey string + BaseURL string +} + +func NewClient() *Client { + key := os.Getenv("XAI_API_KEY") + if key == "" { + color.Red("Error: XAI_API_KEY environment variable not set") + os.Exit(1) + } + return &Client{ + APIKey: key, + BaseURL: "https://api.x.ai/v1", + } +} + +func (c *Client) Stream(messages []map[string]string, model string) string { + 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("Error: %v", err) + 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:] + if data == "[DONE]" { + break + } + var chunk map[string]interface{} + 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 { + fmt.Print(content) + fullReply.WriteString(content) + } + } + } + } + } + } + fmt.Println() + return fullReply.String() +} diff --git a/main.go b/main.go index 9c9b3bd..3c1c565 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,6 @@ package main -import ( - _ "github.com/spf13/cobra" - "gmgauthier.com/grokkit/cmd" -) +import "gmgauthier.com/grokkit/cmd" func main() { cmd.Execute()