package skilltools import ( "context" "encoding/json" "fmt" "strings" "github.com/enmanuel/agents/pkg/skills" shellskills "github.com/enmanuel/agents/shell/skills" "github.com/enmanuel/agents/tools" ) // NewSkillSearch creates a skill_search tool that finds relevant skills. func NewSkillSearch(loader *shellskills.Loader, categories []string) tools.Tool { return tools.Tool{ Def: tools.Def{ Name: "skill_search", Description: "Search for skills relevant to a query. Returns a list of skills with their names, descriptions, and relevance scores. Use this when you need to find a skill to help with a task.", Parameters: []tools.Param{ {Name: "query", Type: "string", Description: "Search query describing the task or capability needed", Required: true}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { query := tools.GetString(args, "query") if query == "" { return tools.Result{Err: fmt.Errorf("query is required")} } // Load all skill metadata metas, err := loader.LoadMeta() if err != nil { return tools.Result{Err: fmt.Errorf("load skills metadata: %w", err)} } // Filter by categories if configured metas = skills.FilterByCategory(metas, categories) // Match skills to query matches := skills.Match(query, metas) if len(matches) == 0 { return tools.Result{Output: "No skills found matching the query."} } // Format output var lines []string lines = append(lines, fmt.Sprintf("Found %d relevant skill(s):\n", len(matches))) for i, match := range matches { if i >= 5 { break // limit to top 5 } lines = append(lines, fmt.Sprintf("%d. **%s** (category: %s, confidence: %.2f)", i+1, match.Skill.Name, match.Skill.Category, match.Confidence)) lines = append(lines, fmt.Sprintf(" %s\n", match.Skill.Description)) } return tools.Result{Output: strings.Join(lines, "\n")} }, } } // NewSkillLoad creates a skill_load tool that loads full instructions for a skill. func NewSkillLoad(loader *shellskills.Loader) tools.Tool { return tools.Tool{ Def: tools.Def{ Name: "skill_load", Description: "Load the complete instructions for a skill. This returns the full markdown content of the skill, which you should follow to complete the task. Use this after finding a skill with skill_search.", Parameters: []tools.Param{ {Name: "skill_name", Type: "string", Description: "Name of the skill to load", Required: true}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { skillName := tools.GetString(args, "skill_name") if skillName == "" { return tools.Result{Err: fmt.Errorf("skill_name is required")} } skill, err := loader.LoadSkill(skillName) if err != nil { return tools.Result{Err: fmt.Errorf("load skill: %w", err)} } // Format output with metadata + instructions var output strings.Builder output.WriteString(fmt.Sprintf("# Skill: %s\n\n", skill.Meta.Name)) output.WriteString(fmt.Sprintf("**Category**: %s\n\n", skill.Meta.Category)) output.WriteString(fmt.Sprintf("**Description**: %s\n\n", skill.Meta.Description)) if len(skill.Scripts) > 0 { output.WriteString(fmt.Sprintf("**Scripts available**: %s\n", strings.Join(skill.Scripts, ", "))) } if len(skill.References) > 0 { output.WriteString(fmt.Sprintf("**References available**: %s\n", strings.Join(skill.References, ", "))) } if len(skill.Templates) > 0 { output.WriteString(fmt.Sprintf("**Templates available**: %s\n", strings.Join(skill.Templates, ", "))) } output.WriteString("\n---\n\n") output.WriteString(skill.Instructions) return tools.Result{Output: output.String()} }, } } // NewSkillReadResource creates a skill_read_resource tool that reads a specific resource. func NewSkillReadResource(loader *shellskills.Loader) tools.Tool { return tools.Tool{ Def: tools.Def{ Name: "skill_read_resource", Description: "Read a specific resource file from a skill (script, reference doc, template, or asset). Use this to load additional documentation or code referenced in the skill instructions.", Parameters: []tools.Param{ {Name: "skill_name", Type: "string", Description: "Name of the skill", Required: true}, {Name: "resource_path", Type: "string", Description: "Path to the resource relative to the skill directory (e.g., 'scripts/deploy.sh', 'references/api.md')", Required: true}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { skillName := tools.GetString(args, "skill_name") resourcePath := tools.GetString(args, "resource_path") if skillName == "" || resourcePath == "" { return tools.Result{Err: fmt.Errorf("skill_name and resource_path are required")} } content, err := loader.ReadResource(skillName, resourcePath) if err != nil { return tools.Result{Err: fmt.Errorf("read resource: %w", err)} } return tools.Result{Output: content} }, } } // NewSkillRunScript creates a skill_run_script tool that executes a skill script. func NewSkillRunScript(loader *shellskills.Loader, executor *shellskills.Executor) tools.Tool { return tools.Tool{ Def: tools.Def{ Name: "skill_run_script", Description: "Execute a script from a skill with the given arguments. The script must be in the skill's scripts/ directory and use an allowed interpreter. Returns the script output.", Parameters: []tools.Param{ {Name: "skill_name", Type: "string", Description: "Name of the skill", Required: true}, {Name: "script_name", Type: "string", Description: "Name of the script file (e.g., 'deploy.sh')", Required: true}, {Name: "args", Type: "array", Description: "Array of arguments to pass to the script", Required: false}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { skillName := tools.GetString(args, "skill_name") scriptName := tools.GetString(args, "script_name") if skillName == "" || scriptName == "" { return tools.Result{Err: fmt.Errorf("skill_name and script_name are required")} } // Parse args array var scriptArgs []string if argsRaw, ok := args["args"]; ok { argsJSON, _ := json.Marshal(argsRaw) _ = json.Unmarshal(argsJSON, &scriptArgs) } // Load skill to get base path skill, err := loader.LoadSkill(skillName) if err != nil { return tools.Result{Err: fmt.Errorf("load skill: %w", err)} } // Verify script exists scriptFound := false for _, s := range skill.Scripts { if s == scriptName { scriptFound = true break } } if !scriptFound { return tools.Result{Err: fmt.Errorf("script not found in skill: %s", scriptName)} } // Execute script scriptPath := fmt.Sprintf("%s/scripts/%s", skill.BasePath, scriptName) output, err := executor.Run(ctx, scriptPath, scriptArgs) if err != nil { return tools.Result{ Output: output, Err: fmt.Errorf("script execution failed: %w", err), } } return tools.Result{Output: output} }, } }