255 lines
7.8 KiB
TypeScript
255 lines
7.8 KiB
TypeScript
// ABOUTME: Tool Registry — in-memory index of all available tools with categorization and search.
|
|
// ABOUTME: Provides the foundation for tool_search and call_tool extensions.
|
|
|
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
|
// ── Types ────────────────────────────────────────
|
|
|
|
export interface ToolEntry {
|
|
name: string;
|
|
label: string;
|
|
description: string;
|
|
category: string;
|
|
tags: string[];
|
|
source: "builtin" | "extension" | "skill" | "commander";
|
|
parameterSummary: string;
|
|
}
|
|
|
|
// ── Category Detection ───────────────────────────
|
|
|
|
const CATEGORY_RULES: { category: string; names: string[]; keywords: string[] }[] = [
|
|
{
|
|
category: "filesystem",
|
|
names: ["read", "write", "edit", "ls", "find", "grep"],
|
|
keywords: ["file", "directory", "path", "read", "write", "edit"],
|
|
},
|
|
{
|
|
category: "shell",
|
|
names: ["bash"],
|
|
keywords: ["command", "terminal", "shell", "execute"],
|
|
},
|
|
{
|
|
category: "commander",
|
|
names: [],
|
|
keywords: ["commander"],
|
|
},
|
|
{
|
|
category: "testing",
|
|
names: ["web_remote", "debug_capture"],
|
|
keywords: ["test", "screenshot", "capture", "audit"],
|
|
},
|
|
{
|
|
category: "ui",
|
|
names: ["ask_user", "show_plan", "show_file", "show_report", "show_spec"],
|
|
keywords: ["viewer", "interactive", "user", "display", "plan", "report"],
|
|
},
|
|
{
|
|
category: "agents",
|
|
names: ["dispatch_agent", "subagent_create", "subagent_create_batch", "subagent_continue", "subagent_remove", "subagent_list"],
|
|
keywords: ["agent", "subagent", "dispatch", "spawn"],
|
|
},
|
|
{
|
|
category: "workflow",
|
|
names: ["tasks", "set_mode", "advance_phase", "dispatch_agents", "pipeline_status", "run_chain", "cycle_memory"],
|
|
keywords: ["task", "mode", "pipeline", "phase", "workflow", "chain"],
|
|
},
|
|
];
|
|
|
|
function detectCategory(name: string, description: string): string {
|
|
const lowerName = name.toLowerCase();
|
|
const lowerDesc = description.toLowerCase();
|
|
|
|
// Commander tools — name-based match
|
|
if (lowerName.startsWith("commander_")) return "commander";
|
|
|
|
for (const rule of CATEGORY_RULES) {
|
|
if (rule.names.includes(lowerName)) return rule.category;
|
|
for (const kw of rule.keywords) {
|
|
if (lowerDesc.includes(kw) && !lowerName.startsWith("commander_")) {
|
|
// Only match if not already caught by a name rule above
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keyword-based fallback
|
|
for (const rule of CATEGORY_RULES) {
|
|
for (const kw of rule.keywords) {
|
|
if (lowerDesc.includes(kw)) return rule.category;
|
|
}
|
|
}
|
|
|
|
return "general";
|
|
}
|
|
|
|
// ── Tag Extraction ───────────────────────────────
|
|
|
|
const TAG_KEYWORDS = [
|
|
"file", "read", "write", "edit", "delete", "create", "search", "find",
|
|
"bash", "command", "shell", "terminal", "execute", "run",
|
|
"task", "project", "workflow", "pipeline", "plan", "mode",
|
|
"agent", "subagent", "dispatch", "spawn", "parallel",
|
|
"test", "debug", "screenshot", "capture", "audit", "accessibility",
|
|
"browser", "web", "url", "page", "navigate", "click",
|
|
"image", "generate", "visual",
|
|
"session", "terminal", "cleanup",
|
|
"message", "mailbox", "send", "inbox",
|
|
"dependency", "graph", "block",
|
|
"spec", "requirement", "feature",
|
|
"jira", "issue", "ticket",
|
|
"orchestration", "hierarchy", "registry",
|
|
"git", "commit", "branch",
|
|
"viewer", "interactive", "ui", "display",
|
|
"memory", "compact", "context",
|
|
];
|
|
|
|
function extractTags(name: string, description: string): string[] {
|
|
const text = `${name} ${description}`.toLowerCase();
|
|
const tags: string[] = [];
|
|
|
|
for (const kw of TAG_KEYWORDS) {
|
|
if (text.includes(kw) && !tags.includes(kw)) {
|
|
tags.push(kw);
|
|
}
|
|
}
|
|
|
|
return tags.slice(0, 10); // Cap at 10 tags
|
|
}
|
|
|
|
// ── Source Detection ─────────────────────────────
|
|
|
|
const BUILTIN_TOOLS = ["read", "write", "edit", "bash", "ls", "find", "grep"];
|
|
|
|
function detectSource(name: string): ToolEntry["source"] {
|
|
if (BUILTIN_TOOLS.includes(name)) return "builtin";
|
|
if (name.startsWith("commander_")) return "commander";
|
|
return "extension";
|
|
}
|
|
|
|
// ── Parameter Summary ────────────────────────────
|
|
|
|
function summarizeParameters(description: string): string {
|
|
// Extract parameter info from description — look for common patterns
|
|
const lines = description.split("\n");
|
|
const paramLines: string[] = [];
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
// Match lines like: - "operation": description
|
|
// or: requires field_name
|
|
if (trimmed.match(/^-\s*"?\w+"?\s*[:—-]/)) {
|
|
paramLines.push(trimmed.replace(/^-\s*/, "").trim());
|
|
}
|
|
}
|
|
|
|
if (paramLines.length > 0) {
|
|
return paramLines.slice(0, 5).join("; ");
|
|
}
|
|
|
|
// Fallback: first sentence of description
|
|
const firstSentence = description.split(/[.\n]/)[0]?.trim() || "";
|
|
return firstSentence.length > 100 ? firstSentence.slice(0, 100) + "..." : firstSentence;
|
|
}
|
|
|
|
// ── Registry Class ───────────────────────────────
|
|
|
|
export class ToolRegistry {
|
|
private tools: Map<string, ToolEntry> = new Map();
|
|
|
|
buildIndex(allTools: { name: string; description?: string }[]): void {
|
|
this.tools.clear();
|
|
|
|
for (const tool of allTools) {
|
|
const desc = tool.description || "";
|
|
const entry: ToolEntry = {
|
|
name: tool.name,
|
|
label: tool.name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
description: desc,
|
|
category: detectCategory(tool.name, desc),
|
|
tags: extractTags(tool.name, desc),
|
|
source: detectSource(tool.name),
|
|
parameterSummary: summarizeParameters(desc),
|
|
};
|
|
this.tools.set(tool.name, entry);
|
|
}
|
|
}
|
|
|
|
getAll(): ToolEntry[] {
|
|
return [...this.tools.values()];
|
|
}
|
|
|
|
getByName(name: string): ToolEntry | undefined {
|
|
return this.tools.get(name);
|
|
}
|
|
|
|
getByCategory(category: string): ToolEntry[] {
|
|
return this.getAll().filter((t) => t.category === category);
|
|
}
|
|
|
|
getCategories(): string[] {
|
|
const cats = new Set<string>();
|
|
for (const t of this.tools.values()) cats.add(t.category);
|
|
return [...cats].sort();
|
|
}
|
|
|
|
search(query: string): ToolEntry[] {
|
|
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
if (terms.length === 0) return this.getAll();
|
|
|
|
const scored: { entry: ToolEntry; score: number }[] = [];
|
|
|
|
for (const entry of this.tools.values()) {
|
|
let score = 0;
|
|
const searchText = `${entry.name} ${entry.label} ${entry.description} ${entry.tags.join(" ")} ${entry.category}`.toLowerCase();
|
|
|
|
for (const term of terms) {
|
|
// Exact name match — highest
|
|
if (entry.name.toLowerCase() === term) score += 100;
|
|
// Name contains term
|
|
else if (entry.name.toLowerCase().includes(term)) score += 50;
|
|
// Category match
|
|
else if (entry.category.toLowerCase() === term) score += 40;
|
|
// Tag match
|
|
else if (entry.tags.includes(term)) score += 30;
|
|
// Description contains term
|
|
else if (entry.description.toLowerCase().includes(term)) score += 10;
|
|
// Fuzzy: any field contains
|
|
else if (searchText.includes(term)) score += 5;
|
|
}
|
|
|
|
if (score > 0) {
|
|
scored.push({ entry, score });
|
|
}
|
|
}
|
|
|
|
return scored
|
|
.sort((a, b) => b.score - a.score)
|
|
.map((s) => s.entry);
|
|
}
|
|
|
|
get size(): number {
|
|
return this.tools.size;
|
|
}
|
|
}
|
|
|
|
// ── Singleton & Extension ────────────────────────
|
|
|
|
// Shared registry instance accessible by other extensions via globalThis
|
|
const g = globalThis as any;
|
|
|
|
export function getToolRegistry(): ToolRegistry {
|
|
if (!g.__piToolRegistry) {
|
|
g.__piToolRegistry = new ToolRegistry();
|
|
}
|
|
return g.__piToolRegistry;
|
|
}
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
const registry = getToolRegistry();
|
|
|
|
pi.on("session_start", async (_event, _ctx) => {
|
|
// Build index from all registered tools
|
|
const allTools = pi.getAllTools();
|
|
registry.buildIndex(allTools);
|
|
});
|
|
}
|