import { z } from "zod"; import type { db as prodDb } from "../../../db/index.ts"; import { createCandidate, createThread, deleteCandidate, getAllThreads, getThreadWithCandidates, resolveThread, updateCandidate, } from "../../services/thread.service.ts"; type Db = typeof prodDb; interface ToolResult { content: Array<{ type: "text"; text: string }>; } function textResult(data: unknown): ToolResult { return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } function errorResult(message: string): ToolResult { return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], }; } export const threadToolDefinitions = [ { name: "list_threads", description: "List research threads. Threads are the recommended way to evaluate gear purchases — each thread tracks multiple candidates for a single gear slot, making it easy to compare options before committing.", inputSchema: { includeResolved: z .boolean() .optional() .describe( "Include resolved threads (default: false, only active threads)", ), }, }, { name: "get_thread", description: "Get a thread with all its candidates for detailed comparison.", inputSchema: { id: z.number().describe("Thread ID"), }, }, { name: "create_thread", description: "Start a new research thread for a gear slot. This is the preferred workflow: create a thread, add candidates with pros/cons/prices, compare them, then resolve the thread to add the winner to your collection.", inputSchema: { name: z.string().describe("Thread name (e.g. 'Handlebar bag')"), categoryId: z.number().describe("Category ID"), }, }, { name: "resolve_thread", description: "Resolve a research thread by picking the winning candidate. The winner is automatically added to the gear collection as a new item, and the thread is marked as resolved.", inputSchema: { threadId: z.number().describe("Thread ID"), candidateId: z.number().describe("ID of the winning candidate"), }, }, { name: "add_candidate", description: "Add a candidate option to a research thread for comparison.", inputSchema: { threadId: z.number().describe("Thread ID"), name: z.string().describe("Candidate name"), categoryId: z.number().describe("Category ID"), weightGrams: z.number().optional().describe("Weight in grams"), priceCents: z.number().optional().describe("Price in cents"), notes: z.string().optional().describe("Notes"), productUrl: z.string().optional().describe("Product URL"), imageFilename: z.string().optional().describe("Image filename"), pros: z.string().optional().describe("Pros of this candidate"), cons: z.string().optional().describe("Cons of this candidate"), }, }, { name: "update_candidate", description: "Update a candidate's details (name, price, pros, cons, etc.).", inputSchema: { id: z.number().describe("Candidate ID"), name: z.string().optional().describe("Candidate name"), weightGrams: z.number().optional().describe("Weight in grams"), priceCents: z.number().optional().describe("Price in cents"), categoryId: z.number().optional().describe("Category ID"), notes: z.string().optional().describe("Notes"), productUrl: z.string().optional().describe("Product URL"), imageFilename: z.string().optional().describe("Image filename"), imageSourceUrl: z.string().optional().describe("Image source URL"), status: z .string() .optional() .describe("Status: researching, ordered, or arrived"), pros: z.string().optional().describe("Pros"), cons: z.string().optional().describe("Cons"), }, }, { name: "remove_candidate", description: "Remove a candidate from a research thread.", inputSchema: { id: z.number().describe("Candidate ID to remove"), }, }, ]; export function registerThreadTools(db: Db) { return { list_threads: async (args: { includeResolved?: boolean; }): Promise => { try { const threadList = getAllThreads(db, args.includeResolved ?? false); return textResult(threadList); } catch (err) { return errorResult((err as Error).message); } }, get_thread: async (args: { id: number }): Promise => { try { const thread = getThreadWithCandidates(db, args.id); if (!thread) return errorResult(`Thread ${args.id} not found`); return textResult(thread); } catch (err) { return errorResult((err as Error).message); } }, create_thread: async (args: { name: string; categoryId: number; }): Promise => { try { const thread = createThread(db, args); return textResult(thread); } catch (err) { return errorResult((err as Error).message); } }, resolve_thread: async (args: { threadId: number; candidateId: number; }): Promise => { try { const result = resolveThread(db, args.threadId, args.candidateId); if (!result.success) { return errorResult(result.error ?? "Failed to resolve thread"); } return textResult(result); } catch (err) { return errorResult((err as Error).message); } }, add_candidate: async (args: { threadId: number; name: string; categoryId: number; weightGrams?: number; priceCents?: number; notes?: string; productUrl?: string; imageFilename?: string; pros?: string; cons?: string; }): Promise => { try { const { threadId, ...data } = args; const candidate = createCandidate(db, threadId, data); return textResult(candidate); } catch (err) { return errorResult((err as Error).message); } }, update_candidate: async (args: { id: number; name?: string; weightGrams?: number; priceCents?: number; categoryId?: number; notes?: string; productUrl?: string; imageFilename?: string; imageSourceUrl?: string; status?: "researching" | "ordered" | "arrived"; pros?: string; cons?: string; }): Promise => { try { const { id, ...data } = args; const candidate = updateCandidate(db, id, data); if (!candidate) return errorResult(`Candidate ${id} not found`); return textResult(candidate); } catch (err) { return errorResult((err as Error).message); } }, remove_candidate: async (args: { id: number }): Promise => { try { const candidate = deleteCandidate(db, args.id); if (!candidate) return errorResult(`Candidate ${args.id} not found`); return textResult({ deleted: true, candidate }); } catch (err) { return errorResult((err as Error).message); } }, }; }