package app import ( "encoding/json" "fmt" "math" "sort" "strconv" "strings" "github.com/tutti-os/tutti/apps/cli/internal/daemon" ) func parseCommandInput(command daemon.Capability, args []string) (map[string]any, error) { input, err := parseCommandArguments(command, args) if err != nil { return nil, err } return typedCommandInput(command.InputSchema, input), nil } func parseCommandArguments(command daemon.Capability, args []string) (map[string]any, error) { if input, ok, err := parsePositionalCommandInput(command, args); ok || err != nil { return input, err } return parseFlagCommandInput(command, args) } // typedCommandInput restores the JSON types the advertised input schema // declares. Terminal arguments always arrive as text, and the daemon validates // invocation input against that schema before a command binds it, so a numeric // or boolean flag has to leave the CLI as a JSON number or boolean rather than // a quoted string. // // Values that do not parse are forwarded untouched: rejecting them here would // duplicate daemon-owned validation, and the daemon already reports the // authoritative error for the declared type. func typedCommandInput(schema map[string]any, input map[string]any) map[string]any { if len(input) == 0 { return input } properties, ok := schema["properties"].(map[string]any) if !ok { return input } for name, value := range input { text, isText := value.(string) if !isText { continue } property, declared := properties[name] if !declared { continue } if typed, ok := schemaTypedValue(schemaPropertyType(property), text); ok { input[name] = typed } } return input } func schemaTypedValue(propertyType string, value string) (any, bool) { value = strings.TrimSpace(value) switch propertyType { case "integer": parsed, err := strconv.ParseInt(value, 10, 64) if err != nil { return nil, false } return parsed, true case "number": parsed, err := strconv.ParseFloat(value, 64) if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) { return nil, false } return parsed, true case "boolean": // Mirrors the daemon input binder, which has always accepted these // spellings for boolean flags. switch strings.ToLower(value) { case "1", "true", "yes", "on": return true, true case "0", "false", "no", "off": return false, true default: return nil, false } default: return nil, false } } func parseFlagCommandInput(command daemon.Capability, args []string) (map[string]any, error) { booleanFlags := commandBooleanFlags(command.InputSchema) arrayFlags := commandArrayFlags(command.InputSchema) input := map[string]any{} for index := 0; index < len(args); index++ { arg := args[index] if !strings.HasPrefix(arg, "--") { return nil, fmt.Errorf("unexpected argument %q", arg) } nameValue := strings.TrimPrefix(arg, "--") name, value, found := strings.Cut(nameValue, "=") if !found { if index+1 >= len(args) || strings.HasPrefix(args[index+1], "--") { if !booleanFlags[name] { return nil, fmt.Errorf("missing value for --%s", name) } input[name] = true continue } else { index++ value = args[index] } } if strings.TrimSpace(name) == "" { return nil, fmt.Errorf("invalid flag %q", arg) } if existing, ok := input[name]; ok { switch typed := existing.(type) { case []string: input[name] = append(typed, value) case string: input[name] = []string{typed, value} default: input[name] = value } continue } if arrayFlags[name] { input[name] = []string{value} continue } input[name] = value } return input, nil } func commandBooleanFlags(schema map[string]any) map[string]bool { flags := map[string]bool{} properties, ok := schema["properties"].(map[string]any) if !ok { return flags } for name, property := range properties { if schemaPropertyType(property) == "boolean" { flags[name] = true } } return flags } func commandArrayFlags(schema map[string]any) map[string]bool { flags := map[string]bool{} properties, ok := schema["properties"].(map[string]any) if !ok { return flags } for name, property := range properties { if schemaPropertyType(property) == "array" { flags[name] = true } } return flags } func parsePositionalCommandInput(command daemon.Capability, args []string) (map[string]any, bool, error) { switch command.ID { case "agent-context.agent.open": if len(args) != 1 || strings.HasPrefix(args[0], "--") { return nil, false, nil } return map[string]any{"session-id": args[0]}, true, nil case "agent-context.agent.send": if len(args) < 2 || strings.HasPrefix(args[0], "--") { return nil, false, nil } if flagIndex := firstKnownFlagIndex(command.InputSchema, args[1:]); flagIndex >= 0 { input, err := parseFlagCommandInput(command, args[1+flagIndex:]) if err != nil { return nil, true, err } if flagIndex > 0 { input["prompt"] = strings.Join(args[1:1+flagIndex], " ") } input["session-id"] = args[0] return input, true, nil } return map[string]any{ "session-id": args[0], "prompt": strings.Join(args[1:], " "), }, true, nil default: return nil, false, nil } } func firstKnownFlagIndex(schema map[string]any, args []string) int { properties, ok := schema["properties"].(map[string]any) if !ok { return -1 } for index, arg := range args { if !strings.HasPrefix(arg, "--") { continue } nameValue := strings.TrimPrefix(arg, "--") name, _, _ := strings.Cut(nameValue, "=") if _, ok := properties[name]; ok { return index } } return -1 } type commandFlag struct { Name string Type string Description string Required bool Values []string Default string HasDefault bool } func commandFlags(schema map[string]any) []commandFlag { properties, ok := schema["properties"].(map[string]any) if !ok || len(properties) == 0 { return nil } requiredNames := schemaRequiredNames(schema) required := map[string]bool{} for _, name := range requiredNames { required[name] = true } flags := make([]commandFlag, 0, len(properties)) for _, name := range requiredNames { property, ok := properties[name] if !ok { continue } flags = append(flags, commandFlag{ Name: name, Type: schemaPropertyType(property), Description: schemaPropertyDescription(property), Required: true, Values: schemaPropertyEnumValues(property), Default: schemaPropertyDefault(property), HasDefault: schemaPropertyHasDefault(property), }) } optionalNames := make([]string, 0, len(properties)) for name := range properties { if !required[name] { optionalNames = append(optionalNames, name) } } sort.Strings(optionalNames) for _, name := range optionalNames { flags = append(flags, commandFlag{ Name: name, Type: schemaPropertyType(properties[name]), Description: schemaPropertyDescription(properties[name]), Values: schemaPropertyEnumValues(properties[name]), Default: schemaPropertyDefault(properties[name]), HasDefault: schemaPropertyHasDefault(properties[name]), }) } return flags } func schemaRequiredNames(schema map[string]any) []string { value, ok := schema["required"] if !ok { return nil } switch typed := value.(type) { case []string: return typed case []any: names := make([]string, 0, len(typed)) for _, entry := range typed { name, ok := entry.(string) if ok { names = append(names, name) } } return names default: return nil } } func schemaPropertyType(property any) string { propertyMap, ok := property.(map[string]any) if !ok { return "value" } typeName, ok := propertyMap["type"].(string) if !ok || strings.TrimSpace(typeName) == "" { return "value" } return typeName } func schemaPropertyDescription(property any) string { propertyMap, ok := property.(map[string]any) if !ok { return "" } description, ok := propertyMap["description"].(string) if !ok { return "" } return strings.TrimSpace(description) } func schemaPropertyEnumValues(property any) []string { propertyMap, ok := property.(map[string]any) if !ok { return nil } value, ok := propertyMap["enum"].([]any) if ok { values := make([]string, 0, len(value)) for _, item := range value { values = append(values, formatSchemaValue(item)) } return values } stringValues, ok := propertyMap["enum"].([]string) if !ok { return nil } values := make([]string, 0, len(stringValues)) values = append(values, stringValues...) return values } func schemaPropertyDefault(property any) string { propertyMap, ok := property.(map[string]any) if !ok { return "" } value, ok := propertyMap["default"] if !ok { return "" } return formatSchemaValue(value) } func schemaPropertyHasDefault(property any) bool { propertyMap, ok := property.(map[string]any) if !ok { return false } _, ok = propertyMap["default"] return ok } func formatSchemaValue(value any) string { switch typed := value.(type) { case json.Number: return typed.String() case float64: if typed == float64(int64(typed)) { return fmt.Sprintf("%.0f", typed) } return fmt.Sprint(typed) default: return fmt.Sprint(value) } }