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
67 changes: 26 additions & 41 deletions internal/tool/jobs_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,49 +44,34 @@ func (JobsTool) Description() string {
"session id: a session sees its own jobs plus every unowned job."
}

func (JobsTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []interface{}{"list", "run", "read", "wait", "kill"},
"description": "Operation to perform",
},
"command": map[string]interface{}{
"type": "string",
"description": "Shell command to run in the background (required for run)",
},
"label": map[string]interface{}{
"type": "string",
"description": "One-line model-facing label for the job (defaults to the command)",
},
"session": map[string]interface{}{
"type": "string",
"description": "Owner session id. Jobs owned by a session are visible only to it; omitted jobs are unowned and visible to any caller",
},
"id": map[string]interface{}{
"type": "string",
"description": "Job id, e.g. bash-3 (required for read/wait/kill)",
},
"reason": map[string]interface{}{
"type": "string",
"description": "Kill reason forwarded verbatim to the job (default: 'user requested')",
},
"timeout_sec": map[string]interface{}{
"type": "integer",
"description": "Max seconds to wait before returning the current snapshot (default: 60)",
},
"output_limit": map[string]interface{}{
"type": "integer",
"description": "UTF-8 byte cap for stored output (default 100000)",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (JobsTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"action": {Type: "string", Enum: []interface{}{"list", "run", "read", "wait", "kill"}, Description: "Operation to perform"},
"command": {Type: "string", Description: "Shell command to run in the background (required for run)"},
"label": {Type: "string", Description: "One-line model-facing label for the job (defaults to the command)"},
"session": {Type: "string", Description: "Owner session id. Jobs owned by a session are visible only to it; omitted jobs are unowned and visible to any caller"},
"id": {Type: "string", Description: "Job id, e.g. bash-3 (required for read/wait/kill)"},
"reason": {Type: "string", Description: "Kill reason forwarded verbatim to the job (default: 'user requested')"},
"timeout_sec": {Type: "integer", Description: "Max seconds to wait before returning the current snapshot (default: 60)"},
"output_limit": {Type: "integer", Description: "UTF-8 byte cap for stored output (default 100000)"},
},
"required": []string{"action"},
Required: []string{"action"},
}
}

type jobsInput struct {
func (JobsTool) Parameters() map[string]interface{} {
return jobsSchema.ToJSONSchema()
}

// jobsSchema is the single source of truth for Jobs' input schema.
var jobsSchema = JobsTool{}.Schema()

// JobsInput is the typed input for JobsTool.
type JobsInput struct {
Action string `json:"action"`
Command string `json:"command"`
Label string `json:"label"`
Expand All @@ -112,8 +97,8 @@ type jobsSnapshotJSON struct {
}

func (JobsTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p jobsInput
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[JobsInput]("Jobs", input)
if err != nil {
return "", err
}
if p.Action == "" {
Expand Down
52 changes: 32 additions & 20 deletions internal/tool/lsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,49 @@ type LSPTool struct {
Manager *lsp.LSPManager
}

// LSPInput is the typed input for LSPTool.
type LSPInput struct {
Action string `json:"action"`
Path string `json:"path"`
Line int `json:"line"`
Column int `json:"column"`
Symbol string `json:"symbol"`
Root string `json:"root"`
}

func (LSPTool) Name() string { return "LSP" }
func (LSPTool) Aliases() []string { return []string{"lsp"} }
func (LSPTool) Description() string {
return "Get code intelligence through configured language servers, with codegraph and local-tool fallbacks."
}

func (LSPTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{"type": "string", "enum": []string{"diagnostics", "definition", "references", "implementations"}, "description": "LSP action"},
"path": map[string]interface{}{"type": "string", "description": "File path"},
"line": map[string]interface{}{"type": "integer", "description": "Line number (1-based)"},
"column": map[string]interface{}{"type": "integer", "description": "Column number (1-based)"},
"symbol": map[string]interface{}{"type": "string", "description": "Symbol name to look up (alternative to line/column)"},
"root": map[string]interface{}{"type": "string", "description": "Project root directory (default: current dir)"},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (LSPTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"action": {Type: "string", Enum: []interface{}{"diagnostics", "definition", "references", "implementations"}, Description: "LSP action"},
"path": {Type: "string", Description: "File path"},
"line": {Type: "integer", Description: "Line number (1-based)"},
"column": {Type: "integer", Description: "Column number (1-based)"},
"symbol": {Type: "string", Description: "Symbol name to look up (alternative to line/column)"},
"root": {Type: "string", Description: "Project root directory (default: current dir)"},
},
"required": []string{"action", "path"},
Required: []string{"action", "path"},
}
}

func (LSPTool) Parameters() map[string]interface{} {
return lspSchema.ToJSONSchema()
}

// lspSchema is the single source of truth for LSP's input schema.
var lspSchema = LSPTool{}.Schema()

func (t LSPTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Action string `json:"action"`
Path string `json:"path"`
Line int `json:"line"`
Column int `json:"column"`
Symbol string `json:"symbol"`
Root string `json:"root"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[LSPInput]("LSP", input)
if err != nil {
return "", err
}

Expand Down
44 changes: 25 additions & 19 deletions internal/tool/powershell.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,42 @@ import (
// PowerShellTool executes PowerShell commands (Windows/cross-platform pwsh).
type PowerShellTool struct{}

// PowerShellInput is the typed input for PowerShellTool.
type PowerShellInput struct {
Command string `json:"command"`
Timeout int64 `json:"timeout"`
}

func (PowerShellTool) Name() string { return "PowerShell" }
func (PowerShellTool) RiskLevel() string { return "high" }
func (PowerShellTool) Aliases() []string { return []string{"powershell"} }
func (PowerShellTool) Description() string {
return "Execute a PowerShell command. Use this instead of Bash when running on Windows or when PowerShell-specific cmdlets are needed."
}

func (PowerShellTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"command": map[string]interface{}{
"type": "string",
"description": "The PowerShell command to execute",
},
"timeout": map[string]interface{}{
"type": "number",
"description": "Timeout in milliseconds (max 600000, default 120000)",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (PowerShellTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"command": {Type: "string", Description: "The PowerShell command to execute"},
"timeout": {Type: "number", Description: "Timeout in milliseconds (max 600000, default 120000)"},
},
"required": []string{"command"},
Required: []string{"command"},
}
}

func (PowerShellTool) Parameters() map[string]interface{} {
return powershellSchema.ToJSONSchema()
}

// powershellSchema is the single source of truth for PowerShell's input schema.
var powershellSchema = PowerShellTool{}.Schema()

func (PowerShellTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Command string `json:"command"`
Timeout int64 `json:"timeout"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[PowerShellInput]("PowerShell", input)
if err != nil {
return "", err
}
if p.Command == "" {
Expand Down Expand Up @@ -78,7 +84,7 @@ func (PowerShellTool) Execute(ctx context.Context, input json.RawMessage) (strin
cmd.Stdout = &stdout
cmd.Stderr = &stderr

err := cmd.Run()
err = cmd.Run()
result := stdout.String()
if stderr.Len() > 0 {
if result != "" {
Expand Down
89 changes: 89 additions & 0 deletions internal/tool/schema_batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,3 +576,92 @@ func TestImportOrganizerSchemaProvider(t *testing.T) {
t.Fatalf("required = %v, want [path]", ImportOrganizerTool{}.Parameters()["required"])
}
}

func TestJobsSchemaProvider(t *testing.T) {
var _ SchemaProvider = JobsTool{}
props := schemaProps(t, JobsTool{}.Parameters())
enum, ok := props["action"].(map[string]interface{})["enum"].([]interface{})
if !ok || len(enum) != 5 || enum[0] != "list" || enum[4] != "kill" {
t.Fatalf("action enum = %v, want 5 options", props["action"])
}
if props["command"].(map[string]interface{})["type"] != "string" {
t.Fatal("command type wrong")
}
req, _ := JobsTool{}.Parameters()["required"].([]string)
if len(req) != 1 || req[0] != "action" {
t.Fatalf("required = %v, want [action]", JobsTool{}.Parameters()["required"])
}
}

func TestLSPToolSchemaProvider(t *testing.T) {
var _ SchemaProvider = LSPTool{}
props := schemaProps(t, LSPTool{}.Parameters())
enum, ok := props["action"].(map[string]interface{})["enum"].([]interface{})
if !ok || len(enum) != 4 {
t.Fatalf("action enum = %v, want 4 options", props["action"])
}
req, _ := LSPTool{}.Parameters()["required"].([]string)
if len(req) != 2 || req[0] != "action" || req[1] != "path" {
t.Fatalf("required = %v, want [action path]", LSPTool{}.Parameters()["required"])
}
}

func TestToolSearchSchemaProvider(t *testing.T) {
var _ SchemaProvider = ToolSearchTool{}
props := schemaProps(t, ToolSearchTool{}.Parameters())
if props["query"].(map[string]interface{})["type"] != "string" {
t.Fatal("query type wrong")
}
if props["max_results"].(map[string]interface{})["type"] != "integer" {
t.Fatal("max_results type wrong")
}
req, _ := ToolSearchTool{}.Parameters()["required"].([]string)
if len(req) != 1 || req[0] != "query" {
t.Fatalf("required = %v, want [query]", ToolSearchTool{}.Parameters()["required"])
}
}

func TestToolsetSchemaProvider(t *testing.T) {
var _ SchemaProvider = ToolsetTool{}
props := schemaProps(t, ToolsetTool{}.Parameters())
enum, ok := props["action"].(map[string]interface{})["enum"].([]interface{})
if !ok || len(enum) != 2 || enum[0] != "list" || enum[1] != "resolve" {
t.Fatalf("action enum = %v, want [list resolve]", props["action"])
}
req, _ := ToolsetTool{}.Parameters()["required"].([]string)
if len(req) != 1 || req[0] != "action" {
t.Fatalf("required = %v, want [action]", ToolsetTool{}.Parameters()["required"])
}
}

func TestWorktreeSchemasProvider(t *testing.T) {
var _ SchemaProvider = EnterWorktreeTool{}
var _ SchemaProvider = ExitWorktreeTool{}
props := schemaProps(t, EnterWorktreeTool{}.Parameters())
if props["path"].(map[string]interface{})["type"] != "string" {
t.Fatal("enter path type wrong")
}
req, _ := EnterWorktreeTool{}.Parameters()["required"].([]string)
if len(req) != 1 || req[0] != "path" {
t.Fatalf("enter required = %v, want [path]", EnterWorktreeTool{}.Parameters()["required"])
}
eprops := schemaProps(t, ExitWorktreeTool{}.Parameters())
if eprops["cleanup"].(map[string]interface{})["type"] != "boolean" {
t.Fatal("exit cleanup type wrong")
}
}

func TestPowerShellSchemaProvider(t *testing.T) {
var _ SchemaProvider = PowerShellTool{}
props := schemaProps(t, PowerShellTool{}.Parameters())
if props["command"].(map[string]interface{})["type"] != "string" {
t.Fatal("command type wrong")
}
if props["timeout"].(map[string]interface{})["type"] != "number" {
t.Fatal("timeout type wrong")
}
req, _ := PowerShellTool{}.Parameters()["required"].([]string)
if len(req) != 1 || req[0] != "command" {
t.Fatalf("required = %v, want [command]", PowerShellTool{}.Parameters()["required"])
}
}
36 changes: 24 additions & 12 deletions internal/tool/tool_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,42 @@ import (

type ToolSearchTool struct{}

// ToolSearchInput is the typed input for ToolSearchTool.
type ToolSearchInput struct {
Query string `json:"query"`
MaxResults int `json:"max_results"`
}

func (ToolSearchTool) Name() string { return "ToolSearch" }
func (ToolSearchTool) RiskLevel() string { return "low" }
func (ToolSearchTool) Aliases() []string { return []string{"tool_search"} }
func (ToolSearchTool) Description() string {
return `Search available tools by name or description. Use query "select:<tool_name>" for direct selection.`
}

func (ToolSearchTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"query": map[string]interface{}{"type": "string", "description": `Search terms, or "select:<tool_name>"`},
"max_results": map[string]interface{}{"type": "integer", "description": "Maximum results to return (default 5)"},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (ToolSearchTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"query": {Type: "string", Description: `Search terms, or "select:<tool_name>"`},
"max_results": {Type: "integer", Description: "Maximum results to return (default 5)"},
},
"required": []string{"query"},
Required: []string{"query"},
}
}

func (ToolSearchTool) Parameters() map[string]interface{} {
return toolSearchSchema.ToJSONSchema()
}

// toolSearchSchema is the single source of truth for ToolSearch's input schema.
var toolSearchSchema = ToolSearchTool{}.Schema()

func (ToolSearchTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Query string `json:"query"`
MaxResults int `json:"max_results"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[ToolSearchInput]("ToolSearch", input)
if err != nil {
return "", err
}
p.Query = strings.TrimSpace(p.Query)
Expand Down
Loading
Loading