diff --git a/internal/recipe/runner.go b/internal/recipe/runner.go index f69076f..d480722 100644 --- a/internal/recipe/runner.go +++ b/internal/recipe/runner.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" "gmgauthier.com/grokkit/internal/grok" @@ -49,9 +50,8 @@ func (r *Runner) Run() error { r.handleApplyStep(refactorJSONs) continue - // Explicit trigger for read-only shell commands - case strings.Contains(titleLower, "read-only shell") || - strings.Contains(titleLower, "shell read-only"): + // Explicit trigger for read-only shell + case strings.Contains(titleLower, "read-only shell") || strings.Contains(titleLower, "shell read-only"): r.executeReadOnlyShell(step, previousResults) continue @@ -258,7 +258,7 @@ func createUnifiedPatch(changes []FileChange, patchPath string) error { return nil } -// executeReadOnlyShell — safe, whitelisted, read-only shell execution with user confirmation +// executeReadOnlyShell — now handles numbers in args (like tree -L 3) func (r *Runner) executeReadOnlyShell(step Step, previousResults []string) { prompt := fmt.Sprintf(`You need additional context from the filesystem for this step. @@ -281,7 +281,7 @@ Return ONLY a JSON array of read-only commands. Example: }, { "command": "tree", - "args": ["."] + "args": [".", "-L", 3] } ] @@ -308,11 +308,11 @@ Only use safe read-only commands from the allowed list.`, } jsonStr := response[start:end] - jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"") // fix escaped quotes + jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"") type ShellCommand struct { - Command string `json:"command"` - Args []string `json:"args"` + Command string `json:"command"` + Args []interface{} `json:"args"` // allows strings and numbers } var cmds []ShellCommand @@ -322,9 +322,24 @@ Only use safe read-only commands from the allowed list.`, } for _, cmd := range cmds { + // Build argument list, converting numbers to strings + args := make([]string, len(cmd.Args)) + for i, arg := range cmd.Args { + switch v := arg.(type) { + case string: + args[i] = v + case float64: + args[i] = strconv.FormatFloat(v, 'f', -1, 64) + case int, int64: + args[i] = fmt.Sprintf("%v", v) + default: + args[i] = fmt.Sprintf("%v", v) + } + } + fullCmd := cmd.Command - if len(cmd.Args) > 0 { - fullCmd += " " + strings.Join(cmd.Args, " ") + if len(args) > 0 { + fullCmd += " " + strings.Join(args, " ") } fmt.Printf(" Grok wants to run: %s\n Allow this command? [y/N] ", fullCmd) @@ -353,7 +368,7 @@ Only use safe read-only commands from the allowed list.`, } // Run with strict cwd - execCmd := exec.Command(cmd.Command, cmd.Args...) + execCmd := exec.Command(cmd.Command, args...) execCmd.Dir = r.resolveWorkDir() output, err := execCmd.CombinedOutput()