feat(recipe): support numeric arguments in read-only shell commands

Update the executeReadOnlyShell function to handle numbers in command arguments,
such as 'tree -L 3', by changing Args to []interface{} and converting them to strings
before execution. Add strconv import for formatting.
This commit is contained in:
Greg Gauthier 2026-03-07 19:27:15 +00:00
parent a36f3585f4
commit 685b0f40d7

View File

@ -6,6 +6,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"gmgauthier.com/grokkit/internal/grok" "gmgauthier.com/grokkit/internal/grok"
@ -49,9 +50,8 @@ func (r *Runner) Run() error {
r.handleApplyStep(refactorJSONs) r.handleApplyStep(refactorJSONs)
continue continue
// Explicit trigger for read-only shell commands // Explicit trigger for read-only shell
case strings.Contains(titleLower, "read-only shell") || case strings.Contains(titleLower, "read-only shell") || strings.Contains(titleLower, "shell read-only"):
strings.Contains(titleLower, "shell read-only"):
r.executeReadOnlyShell(step, previousResults) r.executeReadOnlyShell(step, previousResults)
continue continue
@ -258,7 +258,7 @@ func createUnifiedPatch(changes []FileChange, patchPath string) error {
return nil 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) { func (r *Runner) executeReadOnlyShell(step Step, previousResults []string) {
prompt := fmt.Sprintf(`You need additional context from the filesystem for this step. 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", "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 := response[start:end]
jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"") // fix escaped quotes jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"")
type ShellCommand struct { type ShellCommand struct {
Command string `json:"command"` Command string `json:"command"`
Args []string `json:"args"` Args []interface{} `json:"args"` // allows strings and numbers
} }
var cmds []ShellCommand var cmds []ShellCommand
@ -322,9 +322,24 @@ Only use safe read-only commands from the allowed list.`,
} }
for _, cmd := range cmds { 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 fullCmd := cmd.Command
if len(cmd.Args) > 0 { if len(args) > 0 {
fullCmd += " " + strings.Join(cmd.Args, " ") fullCmd += " " + strings.Join(args, " ")
} }
fmt.Printf(" Grok wants to run: %s\n Allow this command? [y/N] ", fullCmd) 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 // Run with strict cwd
execCmd := exec.Command(cmd.Command, cmd.Args...) execCmd := exec.Command(cmd.Command, args...)
execCmd.Dir = r.resolveWorkDir() execCmd.Dir = r.resolveWorkDir()
output, err := execCmd.CombinedOutput() output, err := execCmd.CombinedOutput()