grokkit/cmd/agent.go
Greg Gauthier 81fd65b14d
All checks were successful
CI / Test (push) Successful in 35s
CI / Lint (push) Successful in 25s
CI / Build (push) Successful in 19s
refactor(cmd): remove automatic .bak backup creation
Eliminate .bak file backups from edit, docs, lint, testgen, and agent commands to simplify safety features, relying on previews and confirmations instead. Update README, architecture docs, troubleshooting, and TODOs to reflect changes. Adjust tests to remove backup assertions.
2026-03-03 20:44:39 +00:00

136 lines
4.0 KiB
Go

package cmd
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"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("agent", modelFlag)
client := grok.NewClient()
color.Yellow("🔍 Agent mode activated. Scanning project...")
var files []string
err := 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 err != nil {
color.Red("Failed to scan project: %v", err)
os.Exit(1)
}
if len(files) == 0 {
color.Yellow("No .go files found.")
return
}
color.Yellow("📄 Found %d files. Asking Grok for a plan...", len(files))
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
if _, err := fmt.Scanln(&confirm); err != nil {
color.Red("Failed to read input: %v", err)
return
}
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] → %s", i+1, len(files), file)
original, err := os.ReadFile(file)
if err != nil {
color.Red("Could not read %s", file)
continue
}
messages := []map[string]string{
{"role": "system", "content": "You are an expert programmer. Remove all unnecessary comments including last modified timestamps and ownership comments. Return clean, complete file content with no explanations, markdown, diffs, or extra text."},
{"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")
newContent := grok.CleanCodeResponse(raw)
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
if _, err := fmt.Scanln(&answer); err != nil {
color.Yellow("Skipped %s (failed to read input)", file)
continue
}
answer = strings.ToLower(strings.TrimSpace(answer))
if answer == "a" {
applyAll = true
} else if answer != "y" {
color.Yellow("Skipped %s", file)
continue
}
}
_ = os.WriteFile(file, []byte(newContent), 0644)
color.Green("✅ Applied %s", file)
}
color.Green("\n🎉 Agent mode complete! All changes applied.")
},
}
// CleanCodeResponse removes markdown fences and returns pure code content
func CleanCodeResponse(text string) string {
fence := "```"
// Remove any line that starts with the fence (opening fence, possibly with language tag)
text = regexp.MustCompile(`(?m)^`+regexp.QuoteMeta(fence)+`.*$`).ReplaceAllString(text, "")
// Remove any line that is just the fence (closing fence)
text = regexp.MustCompile(`(?m)^`+regexp.QuoteMeta(fence)+`\s*$`).ReplaceAllString(text, "")
// Trim only leading and trailing whitespace.
// Do NOT collapse internal blank lines — they are intentional in code.
text = strings.TrimSpace(text)
return text
}