package tools import ( "fmt" "os" "path/filepath" "strings" "ollie/paths" ) // ToolsPath returns the directory to search for named tool scripts. // Resolved in order: first entry of OLLIE_TOOLS_PATH (colon-separated), // then ~/.config/ollie/tools. func ToolsPath() string { if p := os.Getenv("OLLIE_TOOLS_PATH"); p != "" { if i := strings.Index(p, ":"); i >= 0 { p = p[:i] } return p } return paths.CfgDir() + "/tools" } // ExtractPrompt parses the ollie:prompt ... ollie:end block from a tool // script's header comments. Returns the prompt text with comment prefixes // stripped, or "" if no prompt block is found. func ExtractPrompt(script string) string { var lines []string var inPrompt bool for _, line := range strings.Split(script, "\n") { trimmed := strings.TrimSpace(line) // Strip comment prefix (# or //) bare := trimmed if strings.HasPrefix(bare, "# ") { bare = bare[2:] } else if strings.HasPrefix(bare, "#") { bare = bare[1:] } else if strings.HasPrefix(bare, "// ") { bare = bare[3:] } else if strings.HasPrefix(bare, "//") { bare = bare[2:] } else if inPrompt { // Non-comment line while in prompt block — end of header break } else { // Non-comment line before prompt block found if trimmed != "" && !strings.HasPrefix(trimmed, "#!") { break } continue } if strings.Contains(bare, "ollie:prompt") { inPrompt = true continue } if inPrompt && strings.Contains(bare, "ollie:end") { break } if inPrompt { lines = append(lines, bare) } } if len(lines) == 0 { return "" } return strings.Join(lines, "\n") } // DiscoverTools scans the tools directory and returns metadata for all scripts, // including their ollie:prompt blocks and short descriptions. func DiscoverTools() []ToolInfo { dir := ToolsPath() entries, err := os.ReadDir(dir) if err != nil { return nil } var infos []ToolInfo for _, e := range entries { if e.IsDir() || e.Name() == "idx" || strings.HasPrefix(e.Name(), ".") { continue } data, err := os.ReadFile(filepath.Join(dir, e.Name())) if err != nil { continue } script := string(data) prompt := ExtractPrompt(script) desc := extractShortDescription(prompt) infos = append(infos, ToolInfo{ Name: e.Name(), Description: desc, Prompt: prompt, }) } return infos } // extractShortDescription gets the first non-heading, non-empty line from a prompt. func extractShortDescription(prompt string) string { for _, line := range strings.Split(prompt, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } return line } return "" } // ExtractShortDescription is the exported version of extractShortDescription. func ExtractShortDescription(prompt string) string { return extractShortDescription(prompt) } // ToolPrompt returns the full ollie:prompt content for a named tool. // Returns "" if the tool has no prompt or doesn't exist. func ToolPrompt(name string) string { if strings.Contains(name, "/") || strings.Contains(name, "..") { return "" } path := filepath.Join(ToolsPath(), name) data, err := os.ReadFile(path) if err != nil { return "" } return ExtractPrompt(string(data)) } // ReadTool reads a named tool script from the tools directory. func ReadTool(name string) (string, error) { if strings.Contains(name, "/") || strings.Contains(name, "..") { return "", fmt.Errorf("invalid tool name") } path := filepath.Join(ToolsPath(), name) data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return "", fmt.Errorf("tool not found: %s", name) } return "", fmt.Errorf("read tool %s: %w", name, err) } return string(data), nil }