diff --git a/cmd/chat.go b/cmd/chat.go index 171f33a..0226766 100644 --- a/cmd/chat.go +++ b/cmd/chat.go @@ -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 +) diff --git a/cmd/edit.go b/cmd/edit.go index b8ae8d8..6cc294c 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -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 ") + 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), + }, + } } diff --git a/cmd/scaffold.go b/cmd/scaffold.go index d5e1a2a..d472a2c 100644 --- a/cmd/scaffold.go +++ b/cmd/scaffold.go @@ -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 ") + 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), + }, + } }