package desktop import ( "fmt" "math" "os" "path/filepath" "strings" ) // Args reads a tool's arguments as JSON decoded them: strings, float64 numbers, booleans. type Args map[string]any // Text is a required string, trimmed. func (a Args) Text(name string) (string, error) { v, ok := a[name].(string) if !ok || strings.TrimSpace(v) == "" { return "", fmt.Errorf("%s is required, as text", name) } return strings.TrimSpace(v), nil } // Opt is an optional string, trimmed, or def. func (a Args) Opt(name, def string) string { if v, ok := a[name].(string); ok && strings.TrimSpace(v) != "" { return strings.TrimSpace(v) } return def } // Has is whether the caller gave the argument at all. func (a Args) Has(name string) bool { v, ok := a[name] return ok && v != nil } // Bool is an optional boolean: its value, and whether it was given. func (a Args) Bool(name string) (bool, bool, error) { v, ok := a[name] if !ok || v == nil { return false, false, nil } b, isBool := v.(bool) if !isBool { return false, false, fmt.Errorf("%s is true or false", name) } return b, true, nil } // Number is an optional number: its value, and whether it was given. func (a Args) Number(name string) (float64, bool, error) { v, ok := a[name] if !ok || v == nil { return 0, false, nil } f, isNum := v.(float64) if !isNum || math.IsNaN(f) || math.IsInf(f, 0) { return 0, false, fmt.Errorf("%s is a number", name) } return f, true, nil } // Whole is an optional whole number within [lo, hi], or def. func (a Args) Whole(name string, def, lo, hi int) (int, error) { f, given, err := a.Number(name) if err != nil { return 0, err } if !given { return def, nil } if f != math.Trunc(f) || f < float64(lo) || f > float64(hi) { return 0, fmt.Errorf("%s is a whole number from %d to %d", name, lo, hi) } return int(f), nil } // OneOf is an optional string that must be one of choices, or def. func (a Args) OneOf(name, def string, choices ...string) (string, error) { v := a.Opt(name, def) for _, c := range choices { if v == c { return v, nil } } return "", fmt.Errorf("%s is one of %s", name, strings.Join(choices, ", ")) } // Strings is an optional list of strings. func (a Args) Strings(name string) ([]string, error) { v, ok := a[name] if !ok || v == nil { return nil, nil } list, isList := v.([]any) if !isList { return nil, fmt.Errorf("%s is a list of text", name) } out := make([]string, 0, len(list)) for _, x := range list { s, isText := x.(string) if !isText { return nil, fmt.Errorf("%s is a list of text", name) } out = append(out, s) } return out, nil } // Home is the operator account's home: the runtime's word for it, else this process's. func Home() string { if h := os.Getenv("MESH_OPERATOR_HOME"); h != "" { return h } if h, err := os.UserHomeDir(); err == nil { return h } return "/" } // InHome resolves a path the caller gave: `~/x` and a relative path are under the home. A path // that leaves the home through `..` is refused, so a tool that writes never writes outside it. func InHome(path string) (string, error) { home := Home() switch { case path == "~": path = home case strings.HasPrefix(path, "~/"): path = filepath.Join(home, path[2:]) case !filepath.IsAbs(path): path = filepath.Join(home, path) } path = filepath.Clean(path) if path != home && !strings.HasPrefix(path, home+string(filepath.Separator)) { return "", fmt.Errorf("%s is outside the account's home", path) } return path, nil } // Schema builds a tool's input schema from property descriptions; required names those that must // be given. A property is a string unless its description object says otherwise. func Schema(props map[string]any, required ...string) map[string]any { s := map[string]any{"type": "object", "properties": props} if len(required) > 0 { s["required"] = required } return s } // Str, Num, Flag, List and Enum describe one property. func Str(desc string) map[string]any { return map[string]any{"type": "string", "description": desc} } func Num(desc string) map[string]any { return map[string]any{"type": "number", "description": desc} } func Int(desc string) map[string]any { return map[string]any{"type": "integer", "description": desc} } func Flag(desc string) map[string]any { return map[string]any{"type": "boolean", "description": desc} } func List(desc string) map[string]any { return map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": desc} } func Enum(desc string, values ...string) map[string]any { return map[string]any{"type": "string", "enum": values, "description": desc} }