Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 35 additions & 23 deletions internal/tool/fuzzy_find_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,49 @@ func (FuzzyFindTool) Description() string {
return "Fuzzy-search a project for files by name. Handles partial names, camel-case abbreviations (CP→CachePlanner), and multi-word queries (cache gate→cache_gate.go). Returns ranked paths."
}

func (FuzzyFindTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{
"type": "string",
"description": "Fuzzy query: full or partial file name, camel-case abbreviation, or space-separated terms.",
// FuzzyFindInput is the typed input for FuzzyFindTool.
type FuzzyFindInput struct {
Query string `json:"query"`
Path string `json:"path"`
Limit int `json:"limit"`
}

// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge. The limit bounds are preserved verbatim.
func (FuzzyFindTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"query": {
Type: "string",
Description: "Fuzzy query: full or partial file name, camel-case abbreviation, or space-separated terms.",
},
"path": map[string]interface{}{
"type": "string",
"description": "Project directory (default: session working directory).",
"path": {
Type: "string",
Description: "Project directory (default: session working directory).",
},
"limit": map[string]interface{}{
"type": "integer",
"minimum": 1,
"maximum": 100,
"description": "Maximum results (default 20).",
"limit": {
Type: "integer",
Minimum: 1,
Maximum: 100,
Description: "Maximum results (default 20).",
},
},
"required": []string{"query"},
Required: []string{"query"},
}
}

func (FuzzyFindTool) Parameters() map[string]interface{} {
return fuzzyFindSchema.ToJSONSchema()
}

// fuzzyFindSchema is the single source of truth for FuzzyFind's input schema.
var fuzzyFindSchema = FuzzyFindTool{}.Schema()

func (FuzzyFindTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var params struct {
Query string `json:"query"`
Path string `json:"path"`
Limit int `json:"limit"`
}
if err := json.Unmarshal(input, &params); err != nil {
return "", fmt.Errorf("invalid input: %w", err)
params, err := DecodeInput[FuzzyFindInput]("FuzzyFind", input)
if err != nil {
return "", err
}
if strings.TrimSpace(params.Query) == "" {
return "", fmt.Errorf("query is required")
Expand Down
35 changes: 35 additions & 0 deletions internal/tool/schema_batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,38 @@ func TestScreenshotSchemaProvider(t *testing.T) {
t.Fatalf("required = %v, want [url]", ScreenshotTool{}.Parameters()["required"])
}
}

func TestFuzzyFindSchemaProvider(t *testing.T) {
var _ SchemaProvider = FuzzyFindTool{}
props := schemaProps(t, FuzzyFindTool{}.Parameters())
limit, ok := props["limit"].(map[string]interface{})
if !ok || limit["type"] != "integer" {
t.Fatalf("limit prop = %v, want integer type", props["limit"])
}
// Bounds must survive the migration verbatim.
if limit["minimum"] != 1 || limit["maximum"] != 100 {
t.Fatalf("limit bounds = %v/%v, want 1/100", limit["minimum"], limit["maximum"])
}
req, _ := FuzzyFindTool{}.Parameters()["required"].([]string)
if len(req) != 1 || req[0] != "query" {
t.Fatalf("required = %v, want [query]", FuzzyFindTool{}.Parameters()["required"])
}
}

func TestMatchesRange(t *testing.T) {
if !matchesRange("x", 1, 100) {
t.Fatal("non-numeric value should pass (only numbers are ranged)")
}
if !matchesRange(float64(50), 1, 100) {
t.Fatal("50 should be in range")
}
if matchesRange(float64(0), 1, 100) {
t.Fatal("0 should be out of range")
}
if matchesRange(float64(101), 1, 100) {
t.Fatal("101 should be out of range")
}
if !matchesRange(float64(5), nil, nil) {
t.Fatal("no bounds should pass")
}
}
11 changes: 11 additions & 0 deletions internal/tool/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ type SchemaProperty struct {
Enum []interface{} `json:"enum,omitempty"`
// Items describes the element type for array fields.
Items *SchemaProperty `json:"items,omitempty"`
// Minimum/Maximum constrain numeric fields. Kept as interface{} (like
// Default) so authors write plain literals and the wire output matches
// hand-written schemas exactly.
Minimum interface{} `json:"minimum,omitempty"`
Maximum interface{} `json:"maximum,omitempty"`
}

// SchemaProvider is an optional interface tools can implement to expose a typed
Expand Down Expand Up @@ -97,6 +102,12 @@ func (p SchemaProperty) toMap() map[string]interface{} {
if len(p.Enum) > 0 {
m["enum"] = p.Enum
}
if p.Minimum != nil {
m["minimum"] = p.Minimum
}
if p.Maximum != nil {
m["maximum"] = p.Maximum
}
if p.Items != nil {
m["items"] = p.Items.toMap()
}
Expand Down
37 changes: 37 additions & 0 deletions internal/tool/validate_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,43 @@ func validateSchema(input map[string]interface{}, schema ToolSchema, name string
if len(prop.Enum) > 0 && !matchesEnum(value, prop.Enum) {
continue
}
if (prop.Minimum != nil || prop.Maximum != nil) && !matchesRange(value, prop.Minimum, prop.Maximum) {
// Same softening as above: advisory only, caught by unit tests.
continue
}
}
}

func matchesRange(value, minimum, maximum interface{}) bool {
n, ok := value.(float64) // JSON numbers decode as float64
if !ok {
return true
}
if minimum != nil {
if min, ok := toFloat(minimum); ok && n < min {
return false
}
}
if maximum != nil {
if max, ok := toFloat(maximum); ok && n > max {
return false
}
}
return true
}

func toFloat(v interface{}) (float64, bool) {
switch n := v.(type) {
case float64:
return n, true
case float32:
return float64(n), true
case int:
return float64(n), true
case int64:
return float64(n), true
default:
return 0, false
}
}

Expand Down
Loading