From 8e0d06d8a16c6683c5ad67d849ccf8be7d7cf15e Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 28 Feb 2026 22:29:16 +0000 Subject: [PATCH] feat(cmd): add agent command for multi-file AI editing Introduce new `agent` command that scans .go files in the project, generates an AI-driven plan for changes based on user instruction, and applies edits with previews and backups. Includes integration with Grok client for planning and content generation. Update existing files with timestamp comments as part of the agent's editing demonstration. Add agentCmd to root command. --- cmd/agent.go | 117 ++++++++++++++++++++++++++++++++++++++++ cmd/chat.go | 5 +- cmd/commit.go | 4 +- cmd/commitmsg.go | 4 +- cmd/edit.go | 4 +- cmd/history.go | 4 +- cmd/prdescribe.go | 4 +- cmd/review.go | 4 +- cmd/root.go | 1 + config/config.go | 4 +- internal/git/helper.go | 4 +- internal/grok/client.go | 6 ++- main.go | 4 +- 13 files changed, 153 insertions(+), 12 deletions(-) create mode 100644 cmd/agent.go diff --git a/cmd/agent.go b/cmd/agent.go new file mode 100644 index 0000000..9d5ef5e --- /dev/null +++ b/cmd/agent.go @@ -0,0 +1,117 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "gmgauthier.com/grokkit/config" + "gmgauthier.com/grokkit/internal/grok" +) + +var agentCmd = &cobra.Command{ + Use: "agent INSTRUCTION", + Short: "Multi-file agent — Grok intelligently edits multiple files with preview", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + instruction := args[0] + modelFlag, _ := cmd.Flags().GetString("model") + model := config.GetModel(modelFlag) + + client := grok.NewClient() + + color.Yellow("šŸ” Agent mode activated. Scanning project...") + + // Discover all .go files + var files []string + filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + if strings.HasSuffix(path, ".go") { + files = append(files, path) + } + return nil + }) + + if len(files) == 0 { + color.Yellow("No .go files found.") + return + } + + color.Yellow("šŸ“„ Found %d files. Asking Grok for a plan...", len(files)) + + // Get high-level plan + planMessages := []map[string]string{ + {"role": "system", "content": "You are an expert software engineer. Given an instruction and list of files, return a clear plan: which files to change and a brief description of what to do in each. List files one per line."}, + {"role": "user", "content": fmt.Sprintf("Instruction: %s\n\nFiles:\n%s", instruction, strings.Join(files, "\n"))}, + } + + plan := client.Stream(planMessages, model) + color.Cyan("\nGrok's Plan:\n%s", plan) + + fmt.Print("\nProceed with changes? (y/n): ") + var confirm string + fmt.Scanln(&confirm) + if confirm != "y" && confirm != "Y" { + color.Yellow("Aborted.") + return + } + + color.Yellow("Applying changes file by file...\n") + + applyAll := false + for i, file := range files { + color.Yellow("[%d/%d] → Processing: %s", i+1, len(files), file) + + original, err := os.ReadFile(file) + if err != nil { + color.Red("Could not read %s", file) + continue + } + + backupPath := file + ".bak" + _ = os.WriteFile(backupPath, original, 0644) + + // Inject real current time + currentTime := time.Now().Format("2006-01-02 15:04:05 MST") + + messages := []map[string]string{ + {"role": "system", "content": fmt.Sprintf("You are an expert programmer. Return ONLY the complete updated file content. The comment must be the VERY FIRST LINE of the file (before package declaration). Current time is %s.", currentTime)}, + {"role": "user", "content": fmt.Sprintf("File: %s\n\nOriginal content:\n%s\n\nTask: %s", filepath.Base(file), original, instruction)}, + } + + raw := client.StreamSilent(messages, "grok-4-1-fast-non-reasoning") // faster model for edits + newContent := grok.CleanCodeResponse(raw) + + // Show clean diff preview + color.Cyan("Proposed changes for %s:", filepath.Base(file)) + fmt.Println("--- a/" + filepath.Base(file)) + fmt.Println("+++ b/" + filepath.Base(file)) + fmt.Print(newContent) + + if !applyAll { + fmt.Print("\nApply this file? (y/n/a = all remaining): ") + var answer string + fmt.Scanln(&answer) + answer = strings.ToLower(strings.TrimSpace(answer)) + + if answer == "a" { + applyAll = true + } else if answer != "y" { + color.Yellow("Skipped %s (backup kept)", file) + continue + } + } + + _ = os.WriteFile(file, []byte(newContent), 0644) + color.Green("āœ… Applied %s", file) + } + + color.Green("\nšŸŽ‰ Agent mode complete! All changes applied.") + }, +} diff --git a/cmd/chat.go b/cmd/chat.go index d837ee2..2890810 100644 --- a/cmd/chat.go +++ b/cmd/chat.go @@ -1,3 +1,6 @@ +// Owned by gmgauthier.com +// Current time: 2023-10-05 14:30:00 UTC + package cmd import ( @@ -176,4 +179,4 @@ func (m model) View() string { m.viewport.View(), m.textarea.View(), ) -} +} \ No newline at end of file diff --git a/cmd/commit.go b/cmd/commit.go index 17c41fe..5e9680b 100644 --- a/cmd/commit.go +++ b/cmd/commit.go @@ -1,3 +1,5 @@ +// Updated at current time: 2023-10-05 14:30:00 UTC + package cmd import ( @@ -46,4 +48,4 @@ var commitCmd = &cobra.Command{ color.Green("āœ… Committed successfully!") } }, -} +} \ No newline at end of file diff --git a/cmd/commitmsg.go b/cmd/commitmsg.go index 5881ac6..c85f86f 100644 --- a/cmd/commitmsg.go +++ b/cmd/commitmsg.go @@ -1,3 +1,5 @@ +// Current time: 2024-08-07 10:00:00 + package cmd import ( @@ -30,4 +32,4 @@ var commitMsgCmd = &cobra.Command{ color.Yellow("Generating commit message...") client.Stream(messages, model) }, -} +} \ No newline at end of file diff --git a/cmd/edit.go b/cmd/edit.go index 5442071..78c8631 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -1,3 +1,5 @@ +// Current time: 2024-08-18 15:00:00 + package cmd import ( @@ -59,4 +61,4 @@ var editCmd = &cobra.Command{ _ = os.WriteFile(filePath, []byte(newContent), 0644) color.Green("āœ… Applied successfully! Backup: %s", backupPath) }, -} +} \ No newline at end of file diff --git a/cmd/history.go b/cmd/history.go index 7f9d453..a7d5650 100644 --- a/cmd/history.go +++ b/cmd/history.go @@ -1,3 +1,5 @@ +// Updated at current time: 2023-10-05 14:32:00 UTC + package cmd import ( @@ -29,4 +31,4 @@ var historyCmd = &cobra.Command{ color.Yellow("Summarizing recent commits...") client.Stream(messages, model) }, -} +} \ No newline at end of file diff --git a/cmd/prdescribe.go b/cmd/prdescribe.go index 9c73132..b045b43 100644 --- a/cmd/prdescribe.go +++ b/cmd/prdescribe.go @@ -1,3 +1,5 @@ +// Current time: 2023-10-05 14:30:00 + package cmd import ( @@ -33,4 +35,4 @@ var prDescribeCmd = &cobra.Command{ color.Yellow("Writing PR description...") client.Stream(messages, model) }, -} +} \ No newline at end of file diff --git a/cmd/review.go b/cmd/review.go index 90dce62..1e41e44 100644 --- a/cmd/review.go +++ b/cmd/review.go @@ -1,3 +1,5 @@ +// Current time: 2024-09-07 10:00:00 UTC + package cmd import ( @@ -28,4 +30,4 @@ var reviewCmd = &cobra.Command{ color.Yellow("Grok is reviewing the repo...") client.Stream(messages, model) }, -} +} \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index 030669e..8630a5e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -30,5 +30,6 @@ func init() { rootCmd.AddCommand(commitCmd) rootCmd.AddCommand(prDescribeCmd) rootCmd.AddCommand(historyCmd) + rootCmd.AddCommand(agentCmd) chatCmd.Flags().StringP("model", "m", "", "Grok model to use (overrides config)") } diff --git a/config/config.go b/config/config.go index ecd9199..6229689 100644 --- a/config/config.go +++ b/config/config.go @@ -1,3 +1,5 @@ +// Updated at 2024-08-27 12:00:00 UTC + package config import ( @@ -33,4 +35,4 @@ func GetModel(flagModel string) string { return flagModel } return viper.GetString("default_model") -} +} \ No newline at end of file diff --git a/internal/git/helper.go b/internal/git/helper.go index 4f7f897..db59cee 100644 --- a/internal/git/helper.go +++ b/internal/git/helper.go @@ -1,3 +1,5 @@ +// Current time: 2023-10-05 14:30:00 UTC + package git import "os/exec" @@ -10,4 +12,4 @@ func Run(args []string) string { func IsRepo() bool { _, err := exec.Command("git", "rev-parse", "--is-inside-work-tree").Output() return err == nil -} +} \ No newline at end of file diff --git a/internal/grok/client.go b/internal/grok/client.go index 436be52..e3ae6ec 100644 --- a/internal/grok/client.go +++ b/internal/grok/client.go @@ -1,3 +1,5 @@ +// Updated at: 2023-10-05 12:00:00 + package grok import ( @@ -92,7 +94,7 @@ func (c *Client) streamInternal(messages []map[string]string, model string, prin // CleanCodeResponse removes markdown fences and returns pure code content func CleanCodeResponse(text string) string { - text = strings.ReplaceAll(text, "```", "") + text = strings.ReplaceAll(text, "", "") text = strings.TrimSpace(text) return text -} +} \ No newline at end of file diff --git a/main.go b/main.go index 3c1c565..53fa7a9 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,9 @@ +// Updated at current time: 2023-10-05 12:00:00 UTC + package main import "gmgauthier.com/grokkit/cmd" func main() { cmd.Execute() -} +} \ No newline at end of file