import { StringEnum } from "@earendil-works/pi-ai"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import type { DatabaseManager } from "../store/db.js"; import { getMemoryStats, searchMemories } from "../store/sqlite-memory-store.js"; import type { MemoryCategory } from "../types.js"; import { createSharedToolResultRenderer } from "./shared-output-view.js"; import { searchResultView } from "./tool-result-views.js"; interface SearchResult { success: boolean; count?: number; message?: string; output?: string; } function mutationTarget(entry: { target: "memory" | "user" | "failure"; project: string | null }): | "memory" | "user" | "failure" | "project" { // A project name scopes ordinary memory entries, but project-attributed // failures still live in (and must be mutated through) the failure store. return entry.target === "memory" && entry.project ? "project" : entry.target; } function scopeLabel(project: string | null): string { return project ? `project:${encodeURIComponent(project)}` : "global"; } export function registerMemorySearchTool(pi: ExtensionAPI, dbManager: DatabaseManager): void { pi.registerTool({ name: "memory_search", label: "Memory Search", description: `Search extended memory store for relevant entries. Use this when you need context beyond what's in the system prompt — the extended store has unlimited capacity and is searchable. Use cases: - Find memories about a specific topic: "What do I know about auth setup?" - Search project-specific memories: "What conventions does project X follow?" - Find user preferences: "What are the user's testing preferences?" - Search for past failures: "memory_search('auth', category='failure')" Returns matching memory entries with their mutation target, scope, and dates. The displayed target is the value required by memory_replace and memory_remove.`, promptSnippet: "Search extended memory store (unlimited capacity)", promptGuidelines: [ "Use memory_search when you need context beyond what is in the system prompt.", "Use memory_search to find project-specific memories or user preferences.", "Use memory_search with category filter to find specific types of memories (failure, correction, insight, etc.).", ], renderResult: createSharedToolResultRenderer(searchResultView), parameters: Type.Object({ query: Type.String({ description: "Search query. Use natural language or specific terms." }), project: Type.Optional( Type.String({ description: "Filter by project name. Pass null for global memories only." }), ), target: Type.Optional( StringEnum(["memory", "user", "failure"] as const, { description: "Filter by target type (memory, user, or failure).", }), ), category: Type.Optional( StringEnum(["failure", "correction", "insight", "preference", "convention", "tool-quirk"] as const, { description: "Filter by memory category.", }), ), limit: Type.Optional(Type.Number({ description: "Maximum results to return (default: 10, max: 20)." })), }), execute: async ( _id: string, args: { query: string; project?: string; target?: string; category?: string; limit?: number }, ) => { const query = args.query; const project = args.project; const target = args.target; const category = args.category as MemoryCategory | undefined; const limit = Math.min(args.limit || 10, 20); if (!query || query.trim().length === 0) { const result: SearchResult = { success: false, message: "query is required" }; return { content: [{ type: "text" as const, text: result.message! }], details: result }; } const stats = getMemoryStats(dbManager); if (stats.total === 0) { const result: SearchResult = { success: false, message: "No memories in extended store yet. Use memory_add to store memories.", }; return { content: [{ type: "text" as const, text: result.message! }], details: result }; } const results = searchMemories(dbManager, query, { project, target, category, limit }); if (results.length === 0) { const result: SearchResult = { success: true, count: 0, message: `No memories found matching "${query}". Try a different search term or broader query.`, }; return { content: [{ type: "text" as const, text: result.message! }], details: result }; } let output = `Found ${results.length} memories matching "${query}":\n\n`; for (const entry of results) { const target = mutationTarget(entry); const projectLabel = `scope=${scopeLabel(entry.project)}`; const mutationTargetLabel = `[target=${target}]`; const targetLabel = entry.target === "user" ? "👤" : entry.target === "failure" ? "⚠️" : "🧠"; const categoryLabel = entry.category ? ` [${entry.category}]` : ""; output += `${targetLabel} ${projectLabel} ${mutationTargetLabel}${categoryLabel} ${entry.content}\n`; output += ` Created: ${entry.created} | Last used: ${entry.lastReferenced}\n\n`; } const finalResult: SearchResult = { success: true, count: results.length, output: output.trim() }; return { content: [{ type: "text" as const, text: output.trim() }], details: finalResult }; }, }); }