284 lines
6.6 KiB
Go
284 lines
6.6 KiB
Go
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
|
|
}
|