Initial: pi-skill — 68 skills, 43 extensions, 11 themes for Pi
This commit is contained in:
246
extensions/tool-search.ts
Normal file
246
extensions/tool-search.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user