grokkit/cmd/agent.go
Greg Gauthier 8e0d06d8a1 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.
2026-02-28 22:29:16 +00:00

118 lines
3.4 KiB
Go

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.")
},
}