Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

376 lines
9.1 KiB
Go

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)
}
}