// ABOUTME: Tool Search — meta-tool that lets the agent discover and inspect available tools at runtime. // ABOUTME: Provides search, list, and inspect operations against the tool registry. import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { StringEnum } from "@mariozechner/pi-ai"; import { Type } from "@sinclair/typebox"; import { Text } from "@mariozechner/pi-tui"; import { getToolRegistry, type ToolEntry } from "./tool-registry.ts"; import { applyExtensionDefaults } from "./lib/themeMap.ts"; // ── Tool Parameters ──────────────────────────────────────────────────── const ToolSearchParams = Type.Object({ operation: StringEnum(["search", "list", "inspect"] as const), query: Type.Optional(Type.String({ description: "Search query — matches tool names, descriptions, tags, and categories" })), category: Type.Optional(Type.String({ description: "Filter by category (for 'list' operation). Use 'list' without category to see all categories." })), tool_name: Type.Optional(Type.String({ description: "Tool name to inspect (for 'inspect' operation)" })), }); // ── Formatting Helpers ───────────────────────────────────────────────── function formatToolCompact(entry: ToolEntry): string { return `• ${entry.name} [${entry.category}] — ${entry.parameterSummary}`; } function formatToolDetailed(entry: ToolEntry): string { const lines: string[] = [ `## ${entry.name}`, ``, `**Label:** ${entry.label}`, `**Category:** ${entry.category}`, `**Source:** ${entry.source}`, `**Tags:** ${entry.tags.join(", ") || "none"}`, ``, `### Description`, entry.description, ]; return lines.join("\n"); } function formatCategoryList(categories: { name: string; count: number }[]): string { const lines = ["**Available Tool Categories:**", ""]; for (const cat of categories) { lines.push(`• ${cat.name} (${cat.count} tools)`); } return lines.join("\n"); } // ── Extension ────────────────────────────────────────────────────────── export default function (pi: ExtensionAPI) { const registry = getToolRegistry(); pi.registerTool({ name: "tool_search", label: "Tool Search", description: "Search, list, and inspect available tools. Use this to discover what tools are available " + "before calling them. Three operations:\n" + "- 'search': Find tools by query (matches names, descriptions, tags, categories)\n" + "- 'list': List all tools or filter by category. Omit category to see all categories.\n" + "- 'inspect': Get full details and parameter schema for a specific tool by name.\n\n" + "Examples:\n" + '{ "operation": "search", "query": "file management" }\n' + '{ "operation": "list", "category": "commander" }\n' + '{ "operation": "inspect", "tool_name": "commander_task" }', parameters: ToolSearchParams, async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { const { operation, query, category, tool_name } = params; // Ensure registry is populated if (registry.size === 0) { const allTools = pi.getAllTools(); registry.buildIndex(allTools); } if (operation === "search") { if (!query) { return { content: [{ type: "text" as const, text: "Error: 'query' is required for search operation" }], }; } const results = registry.search(query); if (results.length === 0) { return { content: [{ type: "text" as const, text: `No tools found matching "${query}"` }], details: { operation, query, resultCount: 0 }, }; } const formatted = results.map(formatToolCompact).join("\n"); return { content: [{ type: "text" as const, text: `Found ${results.length} tool(s) matching "${query}":\n\n${formatted}`, }], details: { operation, query, resultCount: results.length, results: results.map((r) => r.name) }, }; } if (operation === "list") { if (category) { const tools = registry.getByCategory(category); if (tools.length === 0) { return { content: [{ type: "text" as const, text: `No tools in category "${category}". Use list without category to see available categories.` }], details: { operation, category, resultCount: 0 }, }; } const formatted = tools.map(formatToolCompact).join("\n"); return { content: [{ type: "text" as const, text: `**${category}** tools (${tools.length}):\n\n${formatted}`, }], details: { operation, category, resultCount: tools.length }, }; } // No category — show categories overview const categories = registry.getCategories().map((name) => ({ name, count: registry.getByCategory(name).length, })); const totalTools = registry.size; const formatted = formatCategoryList(categories); return { content: [{ type: "text" as const, text: `${formatted}\n\n**Total:** ${totalTools} tools across ${categories.length} categories`, }], details: { operation, categories: categories.map((c) => c.name), totalTools }, }; } if (operation === "inspect") { if (!tool_name) { return { content: [{ type: "text" as const, text: "Error: 'tool_name' is required for inspect operation" }], }; } const entry = registry.getByName(tool_name); if (!entry) { // Try fuzzy search as fallback const similar = registry.search(tool_name).slice(0, 5); const suggestion = similar.length > 0 ? `\n\nDid you mean: ${similar.map((s) => s.name).join(", ")}?` : ""; return { content: [{ type: "text" as const, text: `Tool "${tool_name}" not found.${suggestion}` }], details: { operation, tool_name, found: false }, }; } return { content: [{ type: "text" as const, text: formatToolDetailed(entry) }], details: { operation, tool_name, found: true, category: entry.category }, }; } return { content: [{ type: "text" as const, text: `Unknown operation: ${operation}. Use 'search', 'list', or 'inspect'.` }], }; }, renderCall(args, theme) { let text = theme.fg("toolTitle", theme.bold("tool_search ")); text += theme.fg("accent", args.operation || ""); if (args.query) text += theme.fg("dim", ` "${args.query}"`); if (args.category) text += theme.fg("dim", ` category:${args.category}`); if (args.tool_name) text += theme.fg("dim", ` ${args.tool_name}`); return new Text(text, 0, 0); }, renderResult(result, { expanded }, theme) { const details = result.details as any; if (!details) { const text = result.content[0]; return new Text(text?.type === "text" ? text.text : "", 0, 0); } if (details.operation === "search" || details.operation === "list") { const count = details.resultCount ?? details.totalTools ?? 0; let summary = theme.fg("success", `${count} result(s)`); if (details.query) summary += theme.fg("dim", ` for "${details.query}"`); if (details.category) summary += theme.fg("dim", ` in ${details.category}`); if (expanded) { const text = result.content[0]; const body = text?.type === "text" ? text.text : ""; return new Text(summary + "\n" + theme.fg("muted", body), 0, 0); } return new Text(summary, 0, 0); } if (details.operation === "inspect") { if (details.found) { const label = theme.fg("success", `✓ ${details.tool_name}`); const cat = theme.fg("dim", ` [${details.category}]`); if (expanded) { const text = result.content[0]; const body = text?.type === "text" ? text.text : ""; return new Text(label + cat + "\n" + theme.fg("muted", body), 0, 0); } return new Text(label + cat, 0, 0); } return new Text(theme.fg("error", `✗ Tool not found: ${details.tool_name}`), 0, 0); } return new Text(theme.fg("dim", "tool_search completed"), 0, 0); }, }); // Register /tool-search command as a shortcut pi.registerCommand("tool-search", { description: "Search for available tools by query", handler: async (args, ctx) => { const query = (args ?? "").trim(); if (!query) { // Show all categories const categories = registry.getCategories().map((name) => ({ name, count: registry.getByCategory(name).length, })); const formatted = formatCategoryList(categories); ctx.ui.notify(`${formatted}\n\nTotal: ${registry.size} tools`, "info"); } else { const results = registry.search(query); if (results.length === 0) { ctx.ui.notify(`No tools found matching "${query}"`, "warning"); } else { const formatted = results.slice(0, 10).map(formatToolCompact).join("\n"); ctx.ui.notify(`Found ${results.length} tool(s):\n${formatted}`, "info"); } } }, }); pi.on("session_start", async (_event, ctx) => { applyExtensionDefaults(import.meta.url, ctx); }); }