Compare commits

...

15 Commits

Author SHA1 Message Date
Gregory Gauthier
4ff65e8137 docs(chat): add documentation for interactive agent mode
Some checks failed
Auto-complete TODO / move-todo (pull_request) Failing after 2s
CI / Test (pull_request) Successful in 34s
CI / Lint (pull_request) Successful in 27s
CI / Build (pull_request) Successful in 21s
- Introduce `--agent` flag for `grokkit chat` as the new primary way to work with Grok on code
- Describe features: tool calling, safety previews, confirmation, persistent sessions
- Note deprecation of old `grokkit agent` command in v0.3.0
- Mention use of fast non-reasoning model for reliability
2026-03-04 14:31:05 +00:00
Gregory Gauthier
365f0c01ec refactor(cmd): clean up tool handling and Scanln usage
- Remove unnecessary comments and reset lines in chat.go for edit and scaffold tools.
- Ignore return values from fmt.Scanln in edit.go and scaffold.go to handle potential errors.
- Delete unused edit_helper_test.go file.
2026-03-04 14:18:23 +00:00
Gregory Gauthier
426288b356 refactor(cmd): replace global vars with flags for agent mode params
Use command-line flags for edit instructions and scaffold descriptions
instead of global variables to improve modularity and avoid globals.
2026-03-04 14:09:04 +00:00
Gregory Gauthier
2809b55912 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.
2026-03-04 14:04:29 +00:00
Gregory Gauthier
bec85a69e0 refactor(chat): remove temporary overrides for edit and scaffold tools
Simplify tool call handling by eliminating global overrides for instructions and descriptions, directly executing commands with provided arguments.
2026-03-04 12:24:23 +00:00
Gregory Gauthier
79c28da120 feat(chat): add overrides for direct instruction passing in tool calls
Introduce temporary global variables `editInstructionOverride` and `scaffoldDescriptionOverride` to pass instructions from the AI agent directly to the edit and scaffold commands. This avoids interactive prompts and enables seamless tool integration without major refactoring. Resets overrides after execution.
2026-03-04 12:20:20 +00:00
Gregory Gauthier
3e2b8ee7bf refactor(chat): simplify tool call handling by directly setting command args
Update the handleToolCall function to use cmd.SetArgs and Execute for edit, scaffold, testgen, and lint tools, removing the need for custom RunE overrides. This streamlines the execution flow while maintaining functionality.
2026-03-04 12:08:51 +00:00
Gregory Gauthier
d42b5278c1 refactor(chat): integrate tool calls with Cobra command execution
Refactor handleToolCall to set RunE on existing Cobra commands (edit, scaffold, testgen, lint) and execute them for better integration and reuse of CLI logic. Update commit tool to ignore output. Remove unused model parameter and adjust comments for consistency.
2026-03-04 12:05:01 +00:00
Gregory Gauthier
a5fda5bbfd refactor(chat): move tool handling to cmd/chat.go to resolve import cycle
Relocate ToolCall struct and HandleToolCall function from internal/agent/tools.go
to cmd/chat.go, renaming to handleToolCall for package-internal use. This eliminates
the import cycle between cmd and agent packages while preserving agent mode functionality.
2026-03-04 11:59:23 +00:00
Gregory Gauthier
69c5d776e2 feat(agent): implement tool calling in agent mode
Add support for Grok to call tools (edit, scaffold, testgen, lint, commit) via JSON in ```tool blocks.
Introduce HandleToolCall to parse and execute tool requests, integrating with existing commands.
Update system prompt and chat loop to handle tool calls and feed results back.
2026-03-04 11:50:21 +00:00
Gregory Gauthier
875e34669c fix(chat): remove unnecessary nil check in history loading
Simplify the condition for initializing chat history, assuming loadChatHistory always returns a non-nil slice.
2026-03-04 11:19:50 +00:00
Gregory Gauthier
b5e1733952 feat(chat): add chat history persistence
Implement loading and saving of chat history to a JSON file. The history file path is configurable via Viper or defaults to ~/.config/grokkit/chat_history.json. Add functions to load, save, and get the history file path. Remove outdated comment in model selection.
2026-03-04 11:15:54 +00:00
Gregory Gauthier
af7270967c fix(config): correct chat model config key
Update the Viper default key for the chat command from "commands.chat" to "commands.chat.model" to match expected structure.

Remove redundant comment in GetTimeout function for cleaner code.
2026-03-04 11:09:34 +00:00
Gregory Gauthier
6eeb919013 refactor(chat): improve chat command with better UI, prompts, and model handling
- Simplify model selection and defaults in config
- Enhance system prompt and history management
- Update CLI output with colors and exit commands
- Remove placeholder tool handling for cleaner agent mode
2026-03-04 11:04:08 +00:00
Gregory Gauthier
87851513f1 refactor(chat): deprecate agent command and integrate into chat with --agent flag
- Add deprecation warning to agent command, redirecting to `grokkit chat --agent`.
- Refactor chat command to support --agent mode with tool calling, custom models, and history management.
- Introduce basic tool call handling placeholder in agent mode.
- Update config defaults for chat and chat-agent models.
2026-03-04 10:49:53 +00:00
7 changed files with 326 additions and 366 deletions

View File

@ -72,6 +72,23 @@ grokkit chat --debug # Enable debug logging
- History is saved automatically between sessions - History is saved automatically between sessions
- Use `--debug` to see API request timing - Use `--debug` to see API request timing
### 🧠 `grokkit chat --agent`
**Interactive Agent Mode** — the new primary way to work with Grok on code.
Grok can now call tools directly (`edit`, `scaffold`, `testgen`, `lint`, `commit`) while maintaining full conversation history and safety previews.
```bash
grokkit chat --agent
```
- Uses the fast non-reasoning model for tool reliability
- Every tool action shows a preview and requires explicit confirmation (y/n)
- Persistent session — you can iterate, reject changes, or give follow-up instructions
- Normal grokkit chat (without --agent) remains unchanged (full reasoning model for thoughtful conversation)
**Note**: The old `grokkit agent` command is deprecated and will be removed in v0.3.0. Please migrate to chat --agent.
### ✏️ `grokkit edit FILE "instruction"` ### ✏️ `grokkit edit FILE "instruction"`
AI-powered file editing with preview. AI-powered file editing with preview.

View File

@ -18,6 +18,10 @@ var agentCmd = &cobra.Command{
Short: "Multi-file agent — Grok intelligently edits multiple files with preview", Short: "Multi-file agent — Grok intelligently edits multiple files with preview",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
color.Red("⚠️ grokkit agent is deprecated!")
color.Red("Use `grokkit chat --agent` instead.")
color.Red("It provides the same (and better) multi-file editing with full conversation, safety previews, and tool control.")
color.Red("This command will be removed in v0.3.0.")
instruction := args[0] instruction := args[0]
modelFlag, _ := cmd.Flags().GetString("model") modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("agent", modelFlag) model := config.GetModel("agent", modelFlag)

View File

@ -12,6 +12,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"gmgauthier.com/grokkit/config" "gmgauthier.com/grokkit/config"
"gmgauthier.com/grokkit/internal/git"
"gmgauthier.com/grokkit/internal/grok" "gmgauthier.com/grokkit/internal/grok"
) )
@ -19,6 +20,15 @@ type ChatHistory struct {
Messages []map[string]string `json:"messages"` Messages []map[string]string `json:"messages"`
} }
type ToolCall struct {
Tool string `json:"tool"`
File string `json:"file,omitempty"`
Path string `json:"path,omitempty"`
Instruction string `json:"instruction,omitempty"`
Description string `json:"description,omitempty"`
Message string `json:"message,omitempty"`
}
func loadChatHistory() []map[string]string { func loadChatHistory() []map[string]string {
histFile := getChatHistoryFile() histFile := getChatHistoryFile()
data, err := os.ReadFile(histFile) data, err := os.ReadFile(histFile)
@ -36,7 +46,7 @@ func loadChatHistory() []map[string]string {
func saveChatHistory(messages []map[string]string) error { func saveChatHistory(messages []map[string]string) error {
histFile := getChatHistoryFile() histFile := getChatHistoryFile()
hist := ChatHistory{Messages: messages} hist := ChatHistory{Messages: messages}
data, err := json.MarshalIndent(hist, "", " ") data, err := json.MarshalIndent(hist, "", " ")
if err != nil { if err != nil {
return err return err
} }
@ -48,77 +58,165 @@ func getChatHistoryFile() string {
if configFile != "" { if configFile != "" {
return configFile return configFile
} }
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
if home == "" { if home == "" {
home = "." home = "."
} }
histDir := filepath.Join(home, ".config", "grokkit") histDir := filepath.Join(home, ".config", "grokkit")
_ = os.MkdirAll(histDir, 0755) // Ignore error, WriteFile will catch it _ = os.MkdirAll(histDir, 0755)
return filepath.Join(histDir, "chat_history.json") return filepath.Join(histDir, "chat_history.json")
} }
var chatCmd = &cobra.Command{ var chatCmd = &cobra.Command{
Use: "chat", Use: "chat",
Short: "Simple interactive CLI chat with Grok (full history + streaming)", Short: "Interactive chat with Grok (use --agent for tool-enabled mode)",
Run: func(cmd *cobra.Command, args []string) { Long: `Start a persistent conversation with Grok.
modelFlag, _ := cmd.Flags().GetString("model")
model := config.GetModel("chat", modelFlag)
client := grok.NewClient() Normal mode: coherent, reasoning-focused chat (uses full model).
Agent mode (--agent): Grok can call tools with safety previews (uses fast model).`,
// Strong system prompt to lock in correct model identity Run: runChat,
systemPrompt := map[string]string{
"role": "system",
"content": fmt.Sprintf("You are Grok 4, the latest and most powerful model from xAI (2026). You are currently running as `%s`. Be helpful, truthful, and a little irreverent. Never claim to be an older model.", model),
}
// Load history or start fresh
history := loadChatHistory()
if history == nil {
history = []map[string]string{systemPrompt}
} else {
// Update system prompt in loaded history
if len(history) > 0 && history[0]["role"] == "system" {
history[0] = systemPrompt
} else {
history = append([]map[string]string{systemPrompt}, history...)
}
}
color.Cyan("┌──────────────────────────────────────────────────────────────┐")
color.Cyan("│ Grokkit Chat — Model: %s │", model)
color.Cyan("│ Type /quit or Ctrl+C to exit │")
color.Cyan("└──────────────────────────────────────────────────────────────┘\n")
scanner := bufio.NewScanner(os.Stdin)
for {
color.Yellow("You > ")
if !scanner.Scan() {
break
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
continue
}
if input == "/quit" || input == "/q" || input == "exit" {
color.Cyan("\nGoodbye 👋\n")
break
}
history = append(history, map[string]string{"role": "user", "content": input})
color.Green("Grok > ")
reply := client.Stream(history, model)
history = append(history, map[string]string{"role": "assistant", "content": reply})
// Save history after each exchange
_ = saveChatHistory(history)
fmt.Println()
}
},
} }
func init() {
chatCmd.Flags().Bool("agent", false, "Enable agent mode with tool calling (uses fast non-reasoning model)")
chatCmd.Flags().String("model", "", "Override model")
rootCmd.AddCommand(chatCmd)
}
func runChat(cmd *cobra.Command, args []string) {
agentMode, _ := cmd.Flags().GetBool("agent")
modelFlag, _ := cmd.Flags().GetString("model")
var model string
if modelFlag != "" {
model = modelFlag
} else if agentMode {
model = config.GetModel("chat-agent", "")
} else {
model = config.GetModel("chat", "")
}
client := grok.NewClient()
systemPrompt := map[string]string{
"role": "system",
"content": fmt.Sprintf("You are Grok 4, the latest and most powerful model from xAI (2026). You are currently running as `%s`. Be helpful, truthful, and a little irreverent.", model),
}
if agentMode {
systemPrompt["content"] = "You are Grok in Agent Mode.\n" +
"You can call tools using this exact JSON format inside ```tool blocks:\n\n" +
"```tool\n" +
"{\"tool\": \"edit\", \"file\": \"main.go\", \"instruction\": \"Add error handling\"}\n" +
"```\n\n" +
"Available tools: edit, scaffold, testgen, lint, commit.\n\n" +
"Always use tools when the user asks you to change code, generate tests, lint, or commit.\n" +
"After every tool call, wait for the result before continuing.\n" +
"Be concise and action-oriented."
}
history := loadChatHistory()
if len(history) == 0 {
history = []map[string]string{systemPrompt}
} else if history[0]["role"] != "system" {
history = append([]map[string]string{systemPrompt}, history...)
} else {
history[0] = systemPrompt
}
color.Cyan("┌──────────────────────────────────────────────────────────────┐")
color.Cyan("│ Grokkit Chat %s — Model: %s │", map[bool]string{true: "(Agent Mode)", false: ""}[agentMode], model)
color.Cyan("│ Type /quit to exit │")
color.Cyan("└──────────────────────────────────────────────────────────────┘\n")
scanner := bufio.NewScanner(os.Stdin)
for {
color.Yellow("You > ")
if !scanner.Scan() {
break
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
continue
}
if input == "/quit" || input == "/q" || input == "exit" {
color.Cyan("\nGoodbye 👋\n")
break
}
history = append(history, map[string]string{"role": "user", "content": input})
color.Green("Grok > ")
reply := client.Stream(history, model)
if agentMode && strings.Contains(reply, "```tool") {
handleToolCall(reply, &history)
continue
}
history = append(history, map[string]string{"role": "assistant", "content": reply})
_ = saveChatHistory(history)
fmt.Println()
}
}
func handleToolCall(reply string, history *[]map[string]string) {
start := strings.Index(reply, "```tool")
if start == -1 {
return
}
end := strings.Index(reply[start+7:], "```")
if end == -1 {
return
}
block := strings.TrimSpace(reply[start+7 : start+7+end])
var tc ToolCall
if err := json.Unmarshal([]byte(block), &tc); err != nil {
color.Red("Failed to parse tool call: %v", err)
return
}
color.Yellow("\n[Agent] Grok wants to call tool: %s", tc.Tool)
switch tc.Tool {
case "edit":
if tc.File != "" && tc.Instruction != "" {
editInstruction = tc.Instruction
editCmd.SetArgs([]string{tc.File})
_ = editCmd.Execute()
}
case "scaffold":
if tc.Path != "" && tc.Description != "" {
scaffoldDescription = tc.Description
scaffoldCmd.SetArgs([]string{tc.Path})
_ = scaffoldCmd.Execute()
}
case "testgen":
if tc.File != "" {
testgenCmd.SetArgs([]string{tc.File})
_ = testgenCmd.Execute()
}
case "lint":
if tc.File != "" {
lintCmd.SetArgs([]string{tc.File})
_ = lintCmd.Execute()
}
case "commit":
if tc.Message != "" {
_, _ = git.Run([]string{"commit", "-m", tc.Message})
}
default:
color.Red("Unknown tool: %s", tc.Tool)
}
*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,91 @@
package cmd package cmd
import ( import (
"bufio"
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gmgauthier.com/grokkit/config" "gmgauthier.com/grokkit/config"
"gmgauthier.com/grokkit/internal/grok" "gmgauthier.com/grokkit/internal/grok"
"gmgauthier.com/grokkit/internal/logger"
) )
var editCmd = &cobra.Command{ var editCmd = &cobra.Command{
Use: "edit FILE INSTRUCTION", Use: "edit [file]",
Short: "Edit a file in-place with Grok (safe preview)", Short: "Edit a file in-place with Grok (safe preview)",
Args: cobra.ExactArgs(2), Long: `Ask Grok to edit a file. Shows a preview diff and requires explicit confirmation before writing.`,
Run: func(cmd *cobra.Command, args []string) { Run: runEdit,
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!")
},
} }
func removeLastModifiedComments(content string) string { func init() {
lines := strings.Split(content, "\n") editCmd.Flags().String("instruction", "", "Edit instruction (used internally by agent mode)")
var cleanedLines []string rootCmd.AddCommand(editCmd)
}
for _, line := range lines { func runEdit(cmd *cobra.Command, args []string) {
if strings.Contains(line, "Last modified") { if len(args) == 0 {
continue 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,53 +0,0 @@
package cmd
import (
"testing"
)
func TestRemoveLastModifiedComments(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "removes last modified comment",
input: "// Last modified: 2024-01-01\npackage main\n\nfunc main() {}",
expected: "package main\n\nfunc main() {}",
},
{
name: "removes multiple last modified comments",
input: "// Last modified: 2024-01-01\npackage main\n// Last modified by: user\nfunc main() {}",
expected: "package main\nfunc main() {}",
},
{
name: "preserves code without last modified",
input: "package main\n\nfunc main() {}",
expected: "package main\n\nfunc main() {}",
},
{
name: "handles empty string",
input: "",
expected: "",
},
{
name: "preserves other comments",
input: "// This is a regular comment\npackage main\n// Last modified: 2024\n// Another comment\nfunc main() {}",
expected: "// This is a regular comment\npackage main\n// Another comment\nfunc main() {}",
},
{
name: "handles line with only last modified",
input: "Last modified: 2024-01-01",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := removeLastModifiedComments(tt.input)
if result != tt.expected {
t.Errorf("removeLastModifiedComments() = %q, want %q", result, tt.expected)
}
})
}
}

View File

@ -1,186 +1,90 @@
package cmd package cmd
import ( import (
"bufio"
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gmgauthier.com/grokkit/config" "gmgauthier.com/grokkit/config"
"gmgauthier.com/grokkit/internal/grok" "gmgauthier.com/grokkit/internal/grok"
"gmgauthier.com/grokkit/internal/logger"
) )
var scaffoldCmd = &cobra.Command{ var scaffoldCmd = &cobra.Command{
Use: "scaffold FILE DESCRIPTION", Use: "scaffold [path]",
Short: "Scaffold a new file with Grok (safe preview + confirmation)", Short: "Scaffold a new file with Grok (safe preview + confirmation)",
Args: cobra.ExactArgs(2), Long: `Generate a new file from a description. Shows preview and requires explicit confirmation before writing.`,
Run: func(cmd *cobra.Command, args []string) { Run: runScaffold,
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)
},
}
// 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()
} }
func init() { func init() {
scaffoldCmd.Flags().Bool("with-tests", false, "Also scaffold a basic _test.go file") scaffoldCmd.Flags().String("description", "", "Scaffold description (used internally by agent mode)")
scaffoldCmd.Flags().Bool("dry-run", false, "Preview only, do not write") rootCmd.AddCommand(scaffoldCmd)
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.)") 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),
},
}
} }

View File

@ -36,7 +36,8 @@ func Load() {
viper.SetDefault("commands.prdescribe.model", "grok-4") viper.SetDefault("commands.prdescribe.model", "grok-4")
viper.SetDefault("commands.review.model", "grok-4") viper.SetDefault("commands.review.model", "grok-4")
viper.SetDefault("commands.docs.model", "grok-4") viper.SetDefault("commands.docs.model", "grok-4")
viper.SetDefault("commands.chat.model", "grok-4-1")
viper.SetDefault("commands.chat-agent.model", "grok-4-1-fast-non-reasoning")
// Config file is optional, so we ignore read errors // Config file is optional, so we ignore read errors
_ = viper.ReadInConfig() _ = viper.ReadInConfig()
} }
@ -48,8 +49,7 @@ func GetModel(commandName string, flagModel string) string {
} }
return flagModel return flagModel
} }
cmdModel := viper.GetString("commands." + commandName + ".model") if cmdModel := viper.GetString("commands." + commandName + ".model"); cmdModel != "" {
if cmdModel != "" {
return cmdModel return cmdModel
} }
return viper.GetString("default_model") return viper.GetString("default_model")
@ -62,7 +62,7 @@ func GetTemperature() float64 {
func GetTimeout() int { func GetTimeout() int {
timeout := viper.GetInt("timeout") timeout := viper.GetInt("timeout")
if timeout <= 0 { if timeout <= 0 {
return 60 // Default 60 seconds return 60
} }
return timeout return timeout
} }