package openai import ( "strings" ) type commandArgSchema struct { allowsString bool allowsObject bool objectProps map[string]any } func normalizeCommandArgument(command any, schema commandArgSchema) (any, bool) { if text, ok := command.(string); ok { text = strings.TrimSpace(text) if text == "" { return command, false } if schema.allowsObject && !schema.allowsString && !commandStringNeedsShell(text) { if structured, ok := structuredCommandFromShellWords(text); ok { return structured, true } } return text, text != command } m, ok := command.(map[string]any) if !ok { return command, false } commandText, ok := m["command"].(string) if !ok { return command, false } if args, ok := stringSliceArgument(m["args"]); ok { commandText = strings.TrimSpace(commandText) if schema.allowsString { return renderShellCommand(commandText, args), true } if commandExecutableNeedsShell(commandText) || commandArgsNeedShell(args) { if schema.allowsString || !schema.allowsObject { return renderShellCommand(commandText, args), true } return cleanedStructuredCommand(commandText, args, schema.objectProps), true } if schema.allowsObject || !schema.allowsString { cleaned := cleanedStructuredCommand(commandText, args, schema.objectProps) return cleaned, !commandStructuredArgsAlreadyClean(m, args) || commandObjectHasStrippedKeys(m, schema.objectProps) } return renderShellCommand(commandText, args), true } commandText = strings.TrimSpace(commandText) if commandText != "" { if schema.allowsString { return commandText, true } if commandStringNeedsShell(commandText) { return commandText, true } if (schema.allowsObject || !schema.allowsString) && !commandStringNeedsShell(commandText) { structured, ok := structuredCommandFromShellWords(commandText) if ok { if len(schema.objectProps) > 0 { structured = filterMapToSchemaProperties(structured, schema.objectProps) } return structured, true } } if structured, ok := structuredCommandFromShellWords(commandText); ok && schema.allowsObject && !schema.allowsString { return structured, true } return commandText, true } return command, false } func shouldStripToolArgumentKey(key string, props map[string]any) bool { if len(props) > 0 { return !schemaHasProperty(props, key) } return key == "description" || key == "runInTerminal" } func schemaHasProperty(props map[string]any, key string) bool { _, ok := props[key] return ok } func schemaSingleCommandField(props map[string]any) string { for _, key := range []string{"command", "cmd"} { if schemaAllowsType(props[key], "string") { return key } } return "" } func firstCommandAsShellString(commands []any) (string, bool) { if len(commands) == 0 { return "", false } return commandAsShellString(commands[0]) } func commandAsShellString(command any) (string, bool) { switch v := command.(type) { case string: text := strings.TrimSpace(v) return text, text != "" case map[string]any: commandText, ok := v["command"].(string) if !ok { return "", false } args, _ := stringSliceArgument(v["args"]) text := renderShellCommand(commandText, args) return text, strings.TrimSpace(text) != "" default: return "", false } } func cleanedStructuredCommand(commandText string, args []string, props map[string]any) map[string]any { cleaned := map[string]any{ "command": commandText, } if len(args) > 0 { cleaned["args"] = args } if len(props) > 0 { return filterMapToSchemaProperties(cleaned, props) } return cleaned } func filterMapToSchemaProperties(m map[string]any, props map[string]any) map[string]any { out := make(map[string]any, len(m)) for key, value := range m { if schemaHasProperty(props, key) { out[key] = value } } return out } func commandObjectHasStrippedKeys(m map[string]any, props map[string]any) bool { for key := range m { switch key { case "command", "args": continue default: if len(props) == 0 || !schemaHasProperty(props, key) { return true } } } return false } func stringSliceArgument(value any) ([]string, bool) { if value == nil { return nil, false } raw, ok := value.([]any) if !ok { return nil, false } out := make([]string, 0, len(raw)) for _, item := range raw { s, ok := item.(string) if !ok { return nil, false } out = append(out, s) } return out, true } func commandStructuredArgsAlreadyClean(m map[string]any, args []string) bool { if len(m) != 2 { return false } rawArgs, ok := m["args"].([]any) if !ok || len(rawArgs) != len(args) { return false } for i, arg := range args { if rawArgs[i] != arg { return false } } return true } func shellJoinArgs(args []string) string { if len(args) == 0 { return "" } quoted := make([]string, 0, len(args)) for _, arg := range args { quoted = append(quoted, shellQuoteArg(arg)) } return " " + strings.Join(quoted, " ") } func renderShellCommand(command string, args []string) string { return strings.TrimSpace(command) + shellJoinArgs(args) } func shellQuoteArg(arg string) string { if arg == "" { return "''" } if isShellOperatorToken(arg) { return arg } if !strings.ContainsAny(arg, " \t\n\r|&;<>(){}[]*$`'\"\\") { return arg } return "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'" } func structuredCommandFromShellWords(text string) (map[string]any, bool) { words := strings.Fields(text) if len(words) == 0 { return nil, false } out := map[string]any{ "command": words[0], } if len(words) > 1 { out["args"] = words[1:] } return out, true } func commandExecutableNeedsShell(text string) bool { text = strings.TrimSpace(text) return text == "" || strings.ContainsAny(text, " \t\n\r|&;<>(){}[]*$`'\"\\") || commandStringNeedsShell(text) } func commandArgsNeedShell(args []string) bool { for _, arg := range args { if isShellOperatorToken(arg) { return true } } return false } func isShellOperatorToken(arg string) bool { switch arg { case "&&", "||", "|", "|&", ";", "&", ">", ">>", "<", "<<", "<<<", "2>", "2>>", "&>", "2>&1": return true default: return false } } func commandStringNeedsShell(text string) bool { text = strings.TrimSpace(text) if text == "" { return false } if strings.ContainsAny(text, "\n\r|&;<>(){}[]*$`'\"\\") { return true } first := firstShellWord(text) switch first { case "alias", "bg", "cd", "command", "export", "fg", "hash", "jobs", "read", "set", "source", "test", "type", "ulimit", "umask", "unalias", "unset", "[", "[[", ":": return true default: return false } } func firstShellWord(s string) string { for i, r := range s { switch r { case ' ', '\t', '\n', '\r': return s[:i] } } return s }