refactor(cmd): integrate agent mode with edit and scaffold commands

Refactor edit and scaffold commands to support invocation from chat --agent mode by using temporary global variables for passing instructions/descriptions. Add interactive prompts for normal usage, preview diffs, and confirmation before writing files. Update chat handler to set these variables and reset after execution.
This commit is contained in:
Gregory Gauthier 2026-03-04 14:04:29 +00:00
parent bec85a69e0
commit 2809b55912
3 changed files with 156 additions and 247 deletions

View File

@ -183,14 +183,19 @@ func handleToolCall(reply string, history *[]map[string]string) {
switch tc.Tool {
case "edit":
if tc.File != "" {
if tc.File != "" && tc.Instruction != "" {
// Pass instruction directly to edit command
editInstruction = tc.Instruction
editCmd.SetArgs([]string{tc.File})
_ = editCmd.Execute()
editInstruction = "" // reset
}
case "scaffold":
if tc.Path != "" {
if tc.Path != "" && tc.Description != "" {
scaffoldDescription = tc.Description
scaffoldCmd.SetArgs([]string{tc.Path})
_ = scaffoldCmd.Execute()
scaffoldDescription = ""
}
case "testgen":
if tc.File != "" {
@ -212,3 +217,9 @@ func handleToolCall(reply string, history *[]map[string]string) {
*history = append(*history, map[string]string{"role": "assistant", "content": reply})
}
// Temporary variables used by agent mode to pass instructions to existing commands
var (
editInstruction string
scaffoldDescription string
)

View File

@ -1,101 +1,93 @@
package cmd
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
"gmgauthier.com/grokkit/config"
"gmgauthier.com/grokkit/internal/grok"
"gmgauthier.com/grokkit/internal/logger"
)
var editCmd = &cobra.Command{
Use: "edit FILE INSTRUCTION",
Use: "edit [file]",
Short: "Edit a file in-place with Grok (safe preview)",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
filePath := args[0]
instruction := args[1]
modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("edit", modelFlag)
logger.Info("edit command started",
"file", filePath,
"instruction", instruction,
"model", model)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
logger.Error("file not found", "file", filePath, "error", err)
color.Red("File not found: %s", filePath)
os.Exit(1)
}
original, err := os.ReadFile(filePath)
if err != nil {
logger.Error("failed to read file", "file", filePath, "error", err)
color.Red("Failed to read file: %v", err)
os.Exit(1)
}
logger.Debug("file read successfully", "file", filePath, "size_bytes", len(original))
cleanedOriginal := removeLastModifiedComments(string(original))
client := grok.NewClient()
messages := []map[string]string{
{"role": "system", "content": "You are an expert programmer. Remove all unnecessary comments including last modified timestamps and ownership comments. Return only the cleaned code with no explanations, no markdown, no extra text."},
{"role": "user", "content": fmt.Sprintf("File: %s\n\nOriginal content:\n%s\n\nTask: %s", filepath.Base(filePath), cleanedOriginal, instruction)},
}
color.Yellow("Asking Grok to %s...\n", instruction)
raw := client.StreamSilent(messages, model)
newContent := grok.CleanCodeResponse(raw)
color.Green("✓ Response received")
color.Cyan("\nProposed changes:")
fmt.Println("--- a/" + filepath.Base(filePath))
fmt.Println("+++ b/" + filepath.Base(filePath))
fmt.Print(newContent)
fmt.Print("\n\nApply these changes? (y/n): ")
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil {
color.Red("Failed to read input: %v", err)
color.Yellow("Changes discarded.")
return
}
if confirm != "y" && confirm != "Y" {
color.Yellow("Changes discarded.")
return
}
logger.Debug("applying changes", "file", filePath, "new_size_bytes", len(newContent))
if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil {
logger.Error("failed to write file", "file", filePath, "error", err)
color.Red("Failed to write file: %v", err)
os.Exit(1)
}
logger.Info("changes applied successfully",
"file", filePath,
"original_size", len(original),
"new_size", len(newContent))
color.Green("✅ Applied successfully!")
},
Long: `Ask Grok to edit a file. Shows a preview diff and requires explicit confirmation before writing.`,
Run: runEdit,
}
func removeLastModifiedComments(content string) string {
lines := strings.Split(content, "\n")
var cleanedLines []string
var editInstruction string // set by agent mode when calling from chat --agent
for _, line := range lines {
if strings.Contains(line, "Last modified") {
continue
func init() {
editCmd.Flags().String("instruction", "", "Edit instruction (used internally by agent mode)")
rootCmd.AddCommand(editCmd)
}
func runEdit(cmd *cobra.Command, args []string) {
if len(args) == 0 {
color.Red("Usage: grokkit edit <file>")
return
}
file := args[0]
// If agent mode passed an instruction, use it directly (no prompt)
var instruction string
if editInstruction != "" {
instruction = editInstruction
color.Cyan("Agent instruction: %s", instruction)
} else {
// Normal interactive mode
color.Yellow("Enter edit instruction for %s: ", file)
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
instruction = strings.TrimSpace(scanner.Text())
}
if instruction == "" {
color.Yellow("No instruction provided.")
return
}
cleanedLines = append(cleanedLines, line)
}
return strings.Join(cleanedLines, "\n")
client := grok.NewClient()
messages := buildEditMessages(file, instruction)
color.Yellow("Asking Grok to edit %s...", file)
edited := client.Stream(messages, config.GetModel("edit", ""))
// Show preview
color.Cyan("\n--- Proposed changes to %s ---\n%s\n--------------------------------", file, edited)
var confirm string
color.Yellow("Apply these changes to %s? (y/n): ", file)
fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {
color.Yellow("Aborted.")
return
}
if err := os.WriteFile(file, []byte(edited), 0644); err != nil {
color.Red("Failed to write file: %v", err)
return
}
color.Green("✅ Successfully edited %s", file)
}
// buildEditMessages is kept unchanged from original
func buildEditMessages(file, instruction string) []map[string]string {
content, _ := os.ReadFile(file)
return []map[string]string{
{
"role": "system",
"content": "You are an expert programmer. Edit the following file according to the user's instruction. Return ONLY the full new file content. Do not include any explanations.",
},
{
"role": "user",
"content": fmt.Sprintf("File: %s\n\nCurrent content:\n%s\n\nInstruction: %s", file, string(content), instruction),
},
}
}

View File

@ -1,186 +1,92 @@
package cmd
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
"gmgauthier.com/grokkit/config"
"gmgauthier.com/grokkit/internal/grok"
"gmgauthier.com/grokkit/internal/logger"
)
var scaffoldCmd = &cobra.Command{
Use: "scaffold FILE DESCRIPTION",
Use: "scaffold [path]",
Short: "Scaffold a new file with Grok (safe preview + confirmation)",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
filePath := args[0]
description := args[1]
withTests, _ := cmd.Flags().GetBool("with-tests")
dryRun, _ := cmd.Flags().GetBool("dry-run")
force, _ := cmd.Flags().GetBool("force")
yes, _ := cmd.Flags().GetBool("yes")
langOverride, _ := cmd.Flags().GetString("lang")
modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("scaffold", modelFlag)
logger.Info("scaffold command started",
"file", filePath,
"description", description,
"with_tests", withTests,
"model", model)
// Safety: don't overwrite existing file unless --force
if _, err := os.Stat(filePath); err == nil && !force {
color.Red("File already exists: %s (use --force to overwrite)", filePath)
os.Exit(1)
}
dir := filepath.Dir(filePath)
if dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0755); err != nil {
logger.Error("failed to create directory", "dir", dir, "error", err)
color.Red("Failed to create directory: %v", err)
os.Exit(1)
}
}
// Detect language and harvest context
lang := detectLanguage(filePath, langOverride)
context := harvestContext(filePath, lang)
// Build system prompt with style enforcement
systemPrompt := fmt.Sprintf(`You are an expert %s programmer.
Match the exact style, naming conventions, error handling, logging, and package structure of this project.
Return ONLY the complete code file. No explanations, no markdown, no backticks.`, lang)
if withTests {
systemPrompt += "\nAlso generate a basic _test.go file with at least one test when --with-tests is used."
}
messages := []map[string]string{
{"role": "system", "content": systemPrompt},
{"role": "user", "content": fmt.Sprintf(
"File path: %s\n\nProject context:\n%s\n\nCreate this new file:\n%s",
filePath, context, description)},
}
color.Yellow("Asking Grok to scaffold %s...\n", filepath.Base(filePath))
client := grok.NewClient()
raw := client.StreamSilent(messages, model)
newContent := grok.CleanCodeResponse(raw)
color.Green("Response received")
// Preview
color.Cyan("\nProposed new file:")
fmt.Printf("--- /dev/null\n")
fmt.Printf("+++ b/%s\n", filepath.Base(filePath))
fmt.Println(newContent)
if dryRun {
color.Yellow("\n--dry-run: file not written")
return
}
if !yes {
fmt.Print("\n\nCreate this file? (y/n): ")
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil || (confirm != "y" && confirm != "Y") {
color.Yellow("Scaffold cancelled.")
return
}
}
// Write main file
if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil {
logger.Error("failed to write file", "file", filePath, "error", err)
color.Red("Failed to write file: %v", err)
os.Exit(1)
}
color.Green("✓ Created: %s", filePath)
// Optional test file
if withTests {
testPath := strings.TrimSuffix(filePath, filepath.Ext(filePath)) + "_test.go"
testMessages := []map[string]string{
{"role": "system", "content": systemPrompt},
{"role": "user", "content": fmt.Sprintf("Generate a basic test file for %s using the same style.", filepath.Base(filePath))},
}
testRaw := client.StreamSilent(testMessages, model)
testContent := grok.CleanCodeResponse(testRaw)
if err := os.WriteFile(testPath, []byte(testContent), 0644); err == nil {
color.Green("✓ Created test: %s", filepath.Base(testPath))
}
}
logger.Info("scaffold completed successfully", "file", filePath, "with_tests", withTests)
},
Long: `Generate a new file from a description. Shows preview and requires explicit confirmation before writing.`,
Run: runScaffold,
}
// Simple language detector (can be moved to internal/linter later)
func detectLanguage(path, override string) string {
if override != "" {
return override
}
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".go":
return "Go"
case ".py":
return "Python"
case ".js", ".ts":
return "TypeScript"
case ".c":
return "C"
case ".cpp":
return "C++"
case ".java":
return "Java"
// add more as needed
default:
return "code"
}
}
// Basic context harvester (~4000 token cap)
func harvestContext(filePath, lang string) string {
var sb strings.Builder
dir := filepath.Dir(filePath)
// Siblings
files, _ := os.ReadDir(dir)
for _, f := range files {
if f.IsDir() || strings.HasPrefix(f.Name(), ".") {
continue
}
if filepath.Ext(f.Name()) == filepath.Ext(filePath) && f.Name() != filepath.Base(filePath) {
content, _ := os.ReadFile(filepath.Join(dir, f.Name()))
if len(content) > 2000 {
content = content[:2000]
}
// Fixed: use Fprintf instead of WriteString + Sprintf
fmt.Fprintf(&sb, "=== %s ===\n%s\n\n", f.Name(), string(content))
}
}
// Rough token cap
if sb.Len() > 4000 {
return sb.String()[:4000] + "\n... (truncated)"
}
return sb.String()
}
var scaffoldDescription string // set by agent mode when calling from chat --agent
func init() {
scaffoldCmd.Flags().Bool("with-tests", false, "Also scaffold a basic _test.go file")
scaffoldCmd.Flags().Bool("dry-run", false, "Preview only, do not write")
scaffoldCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
scaffoldCmd.Flags().Bool("force", false, "Overwrite existing file")
scaffoldCmd.Flags().String("lang", "", "Force language for prompt (Go, Python, etc.)")
scaffoldCmd.Flags().String("description", "", "Scaffold description (used internally by agent mode)")
rootCmd.AddCommand(scaffoldCmd)
}
func runScaffold(cmd *cobra.Command, args []string) {
if len(args) == 0 {
color.Red("Usage: grokkit scaffold <path>")
return
}
path := args[0]
// If agent mode passed a description, use it directly
var description string
if scaffoldDescription != "" {
description = scaffoldDescription
color.Cyan("Agent description: %s", description)
} else {
// Normal interactive mode
color.Yellow("Enter description for %s: ", path)
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
description = strings.TrimSpace(scanner.Text())
}
if description == "" {
color.Yellow("No description provided.")
return
}
}
client := grok.NewClient()
messages := buildScaffoldMessages(path, description)
color.Yellow("Asking Grok to scaffold %s...", path)
scaffolded := client.Stream(messages, config.GetModel("scaffold", ""))
// Show preview
color.Cyan("\n--- Proposed content for %s ---\n%s\n--------------------------------", path, scaffolded)
var confirm string
color.Yellow("Create %s with this content? (y/n): ", path)
fmt.Scanln(&confirm)
if confirm != "y" && confirm != "Y" {
color.Yellow("Aborted.")
return
}
if err := os.WriteFile(path, []byte(scaffolded), 0644); err != nil {
color.Red("Failed to write file: %v", err)
return
}
color.Green("✅ Successfully scaffolded %s", path)
}
// buildScaffoldMessages is kept unchanged from original
func buildScaffoldMessages(path, description string) []map[string]string {
return []map[string]string{
{
"role": "system",
"content": "You are an expert programmer. Generate a complete, well-structured file based on the user's description. Return ONLY the full file content. Do not include any explanations or markdown fences.",
},
{
"role": "user",
"content": fmt.Sprintf("Path: %s\n\nDescription: %s", path, description),
},
}
}