type ApiCallResult = { status: number; body: unknown; }; export type ToolExecutor = (name: string, argsJson: string) => Promise; export function createApiToolExecutor(request: Request): ToolExecutor { const origin = new URL(request.url).origin; return async (name: string, argsJson: string) => { const args = parseArgs(argsJson); const result = await dispatchTool(name, args, request, origin); return JSON.stringify(result); }; } function parseArgs(argsJson: string): Record { if (!argsJson.trim()) return {}; const parsed: unknown = JSON.parse(argsJson); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("Tool arguments must be a JSON object"); } return parsed as Record; } async function dispatchTool( name: string, args: Record, request: Request, origin: string, ): Promise { switch (name) { case "list_lists": { const type = typeof args.type === "string" ? args.type : undefined; const qs = type ? `?type=${encodeURIComponent(type)}` : ""; return callApi(request, origin, "GET", `/api/v1/lists${qs}`); } case "list_list_items": { const listId = requireString(args, "listId"); return callApi(request, origin, "GET", `/api/v1/lists/${listId}/items`); } case "add_list_item": { const text = requireString(args, "text"); let listId = typeof args.listId === "string" ? args.listId : undefined; if (!listId) { const listType = typeof args.listType === "string" ? args.listType : undefined; const listsResult = await callApi( request, origin, "GET", listType ? `/api/v1/lists?type=${encodeURIComponent(listType)}` : "/api/v1/lists", ); if (listsResult.status !== 200 || !Array.isArray(listsResult.body)) { return listsResult; } const first = listsResult.body[0] as { id?: string } | undefined; listId = first?.id; if (!listId) { return { status: 404, body: { error: "No matching list found" } }; } } const body: Record = { text }; if (typeof args.qty === "string") body.qty = args.qty; return callApi(request, origin, "POST", `/api/v1/lists/${listId}/items`, body); } case "update_list_item": { const listId = requireString(args, "listId"); const itemId = requireString(args, "itemId"); const body: Record = {}; if (typeof args.done === "boolean") body.done = args.done; if (typeof args.text === "string") body.text = args.text; if (typeof args.qty === "string") body.qty = args.qty; return callApi(request, origin, "PATCH", `/api/v1/lists/${listId}/items/${itemId}`, body); } case "delete_list_item": { const listId = requireString(args, "listId"); const itemId = requireString(args, "itemId"); return callApi(request, origin, "DELETE", `/api/v1/lists/${listId}/items/${itemId}`); } case "create_list": { const body = { type: requireString(args, "type"), name: requireString(args, "name"), }; return callApi(request, origin, "POST", "/api/v1/lists", body); } case "update_list": { const listId = requireString(args, "listId"); const body: Record = {}; if (typeof args.name === "string") body.name = args.name; if (typeof args.archived === "boolean") body.archived = args.archived; return callApi(request, origin, "PATCH", `/api/v1/lists/${listId}`, body); } case "delete_list": { const listId = requireString(args, "listId"); return callApi(request, origin, "DELETE", `/api/v1/lists/${listId}`); } case "list_calendars": return callApi(request, origin, "GET", "/api/v1/calendars"); case "list_events": { const from = requireString(args, "from"); const to = requireString(args, "to"); const calendarIds = typeof args.calendarIds === "string" && args.calendarIds.length > 0 ? args.calendarIds : "all"; const qs = new URLSearchParams({ from, to, calendarIds }); return callApi(request, origin, "GET", `/api/v1/events?${qs.toString()}`); } case "create_event": { const resolved = await resolveCalendarId(args, request, origin); if ("error" in resolved) return resolved.error; const body: Record = { calendarId: resolved.calendarId, title: requireString(args, "title"), startAt: requireString(args, "startAt"), endAt: requireString(args, "endAt"), allDay: typeof args.allDay === "boolean" ? args.allDay : false, }; if (typeof args.location === "string") body.location = args.location; if (typeof args.notes === "string") body.notes = args.notes; if (typeof args.remindMinutesBefore === "number") { body.remindMinutesBefore = args.remindMinutesBefore; } return callApi(request, origin, "POST", "/api/v1/events", body); } case "update_event": { const eventId = requireString(args, "eventId"); const body: Record = {}; if (typeof args.title === "string") body.title = args.title; if (typeof args.startAt === "string") body.startAt = args.startAt; if (typeof args.endAt === "string") body.endAt = args.endAt; if (typeof args.allDay === "boolean") body.allDay = args.allDay; if (typeof args.location === "string") body.location = args.location; if (typeof args.notes === "string") body.notes = args.notes; if (typeof args.remindMinutesBefore === "number") { body.remindMinutesBefore = args.remindMinutesBefore; } return callApi(request, origin, "PATCH", `/api/v1/events/${eventId}`, body); } case "delete_event": { const eventId = requireString(args, "eventId"); return callApi(request, origin, "DELETE", `/api/v1/events/${eventId}`); } case "create_calendar": { const body: Record = { name: requireString(args, "name"), }; if (typeof args.color === "string") body.color = args.color; if (args.visibility === "private" || args.visibility === "household") { body.visibility = args.visibility; } return callApi(request, origin, "POST", "/api/v1/calendars", body); } case "list_notes": return callApi(request, origin, "GET", "/api/v1/notes"); case "create_note": { const body: Record = { title: requireString(args, "title"), body: typeof args.body === "string" ? args.body : "", pinned: typeof args.pinned === "boolean" ? args.pinned : false, }; if (typeof args.remindAt === "string") body.remindAt = args.remindAt; return callApi(request, origin, "POST", "/api/v1/notes", body); } case "update_note": { const noteId = requireString(args, "noteId"); const body: Record = {}; if (typeof args.title === "string") body.title = args.title; if (typeof args.body === "string") body.body = args.body; if (typeof args.pinned === "boolean") body.pinned = args.pinned; if (typeof args.remindAt === "string") body.remindAt = args.remindAt; if (args.remindAt === null) body.remindAt = null; return callApi(request, origin, "PATCH", `/api/v1/notes/${noteId}`, body); } case "delete_note": { const noteId = requireString(args, "noteId"); return callApi(request, origin, "DELETE", `/api/v1/notes/${noteId}`); } case "list_journal_entries": { const limit = typeof args.limit === "number" ? String(args.limit) : undefined; const qs = limit ? `?limit=${encodeURIComponent(limit)}` : ""; return callApi(request, origin, "GET", `/api/v1/journal/entries${qs}`); } case "create_journal_entry": { const body: Record = { recordedAt: requireString(args, "recordedAt"), }; if (typeof args.title === "string") body.title = args.title; if (typeof args.body === "string") body.body = args.body; if (Array.isArray(args.moods)) body.moods = args.moods; if (typeof args.stress === "number") body.stress = args.stress; if (typeof args.pillsTaken === "boolean") body.pillsTaken = args.pillsTaken; return callApi(request, origin, "POST", "/api/v1/journal/entries", body); } case "update_journal_entry": { const entryId = requireString(args, "entryId"); const body: Record = {}; if (typeof args.recordedAt === "string") body.recordedAt = args.recordedAt; if (typeof args.title === "string") body.title = args.title; if (typeof args.body === "string") body.body = args.body; if (Array.isArray(args.moods)) body.moods = args.moods; if (typeof args.stress === "number") body.stress = args.stress; if (typeof args.pillsTaken === "boolean") body.pillsTaken = args.pillsTaken; return callApi(request, origin, "PATCH", `/api/v1/journal/entries/${entryId}`, body); } case "delete_journal_entry": { const entryId = requireString(args, "entryId"); return callApi(request, origin, "DELETE", `/api/v1/journal/entries/${entryId}`); } case "list_bangs": { const limit = typeof args.limit === "number" ? String(args.limit) : undefined; const qs = limit ? `?limit=${encodeURIComponent(limit)}` : ""; return callApi(request, origin, "GET", `/api/v1/bangs${qs}`); } case "add_bang": { const body: Record = {}; if (typeof args.occurredOn === "string") body.occurredOn = args.occurredOn; return callApi(request, origin, "POST", "/api/v1/bangs", body); } case "update_bang": { const bangId = requireString(args, "bangId"); const body = { occurredOn: requireString(args, "occurredOn") }; return callApi(request, origin, "PATCH", `/api/v1/bangs/${bangId}`, body); } case "delete_bang": { const bangId = requireString(args, "bangId"); return callApi(request, origin, "DELETE", `/api/v1/bangs/${bangId}`); } case "list_garden_containers": return callApi(request, origin, "GET", "/api/v1/garden/containers"); case "list_garden_plants": { const containerId = typeof args.containerId === "string" ? args.containerId : undefined; const qs = containerId ? `?containerId=${encodeURIComponent(containerId)}` : ""; return callApi(request, origin, "GET", `/api/v1/garden/plants${qs}`); } case "get_garden_plant": { const plantId = requireString(args, "plantId"); return callApi(request, origin, "GET", `/api/v1/garden/plants/${plantId}`); } case "create_garden_container": { const body: Record = { name: requireString(args, "name") }; if (typeof args.type === "string") body.type = args.type; if (typeof args.locationNotes === "string") body.locationNotes = args.locationNotes; return callApi(request, origin, "POST", "/api/v1/garden/containers", body); } case "update_garden_container": { const containerId = requireString(args, "containerId"); const body: Record = {}; if (typeof args.name === "string") body.name = args.name; if (typeof args.type === "string") body.type = args.type; if (typeof args.locationNotes === "string") body.locationNotes = args.locationNotes; return callApi(request, origin, "PATCH", `/api/v1/garden/containers/${containerId}`, body); } case "delete_garden_container": { const containerId = requireString(args, "containerId"); return callApi(request, origin, "DELETE", `/api/v1/garden/containers/${containerId}`); } case "create_garden_plant": { const body: Record = { name: requireString(args, "name") }; if (typeof args.category === "string") body.category = args.category; if (typeof args.containerId === "string") body.containerId = args.containerId; if (typeof args.healthStatus === "string") body.healthStatus = args.healthStatus; if (typeof args.notes === "string") body.notes = args.notes; return callApi(request, origin, "POST", "/api/v1/garden/plants", body); } case "update_garden_plant": { const plantId = requireString(args, "plantId"); const body: Record = {}; if (typeof args.name === "string") body.name = args.name; if (typeof args.category === "string") body.category = args.category; if (typeof args.containerId === "string") body.containerId = args.containerId; if (typeof args.healthStatus === "string") body.healthStatus = args.healthStatus; if (typeof args.notes === "string") body.notes = args.notes; return callApi(request, origin, "PATCH", `/api/v1/garden/plants/${plantId}`, body); } case "delete_garden_plant": { const plantId = requireString(args, "plantId"); return callApi(request, origin, "DELETE", `/api/v1/garden/plants/${plantId}`); } case "log_garden_care": { const plantId = requireString(args, "plantId"); const body: Record = { careType: requireString(args, "careType") }; if (typeof args.notes === "string") body.notes = args.notes; if (typeof args.performedAt === "string") body.performedAt = args.performedAt; return callApi(request, origin, "POST", `/api/v1/garden/plants/${plantId}/care-logs`, body); } case "list_garden_care_logs": { const plantId = requireString(args, "plantId"); const limit = typeof args.limit === "number" ? `?limit=${args.limit}` : ""; return callApi(request, origin, "GET", `/api/v1/garden/plants/${plantId}/care-logs${limit}`); } case "list_garden_care_schedules": { const plantId = requireString(args, "plantId"); return callApi(request, origin, "GET", `/api/v1/garden/plants/${plantId}/care-schedules`); } case "upsert_garden_care_schedule": { const plantId = requireString(args, "plantId"); const body: Record = { careType: requireString(args, "careType"), intervalDays: requireNumber(args, "intervalDays"), }; if (typeof args.enabled === "boolean") body.enabled = args.enabled; return callApi( request, origin, "POST", `/api/v1/garden/plants/${plantId}/care-schedules`, body, ); } case "delete_garden_care_schedule": { const scheduleId = requireString(args, "scheduleId"); return callApi(request, origin, "DELETE", `/api/v1/garden/care-schedules/${scheduleId}`); } case "toggle_garden_care_schedule": { const scheduleId = requireString(args, "scheduleId"); const body = { enabled: requireBoolean(args, "enabled") }; return callApi(request, origin, "PATCH", `/api/v1/garden/care-schedules/${scheduleId}`, body); } case "push_overdue_garden_care": return callApi(request, origin, "POST", "/api/v1/garden/overdue-care/push"); case "schedule_garden_care_on_calendar": { const scheduleId = requireString(args, "scheduleId"); const body: Record = { calendarId: requireString(args, "calendarId"), }; if (typeof args.reminderMinutesBefore === "number") { body.reminderMinutesBefore = args.reminderMinutesBefore; } return callApi( request, origin, "POST", `/api/v1/garden/care-schedules/${scheduleId}/calendar`, body, ); } case "list_shareable_entity_types": return callApi(request, origin, "GET", "/api/v1/share-links?entityTypes=true"); case "list_share_links": { const qs = new URLSearchParams(); if (typeof args.entityType === "string") qs.set("entityType", args.entityType); if (typeof args.entityId === "string") qs.set("entityId", args.entityId); const query = qs.toString(); return callApi( request, origin, "GET", query ? `/api/v1/share-links?${query}` : "/api/v1/share-links", ); } case "create_share_link": { const body: Record = { entityType: requireString(args, "entityType"), entityId: requireString(args, "entityId"), }; if (typeof args.expiresAt === "string") body.expiresAt = args.expiresAt; if (typeof args.write === "boolean") { body.capabilities = { write: args.write }; } return callApi(request, origin, "POST", "/api/v1/share-links", body); } case "revoke_share_link": { const linkId = requireString(args, "linkId"); return callApi(request, origin, "DELETE", `/api/v1/share-links/${linkId}`); } case "get_api_docs": return getApiDocs(args); case "call_api": return callApiFallback(args, request, origin); default: return { status: 400, body: { error: `Unknown tool: ${name}` } }; } } async function resolveCalendarId( args: Record, request: Request, origin: string, ): Promise<{ calendarId: string } | { error: ApiCallResult }> { const calendarId = typeof args.calendarId === "string" ? args.calendarId.trim() : ""; if (calendarId) return { calendarId }; const calendarsResult = await callApi(request, origin, "GET", "/api/v1/calendars"); if (calendarsResult.status !== 200 || !Array.isArray(calendarsResult.body)) { return { error: calendarsResult }; } const calendars = calendarsResult.body as Array<{ id?: string; name?: string }>; const calendarName = typeof args.calendarName === "string" ? args.calendarName.trim().toLowerCase() : ""; if (calendarName) { const match = calendars.find( (calendar) => typeof calendar.name === "string" && calendar.name.toLowerCase() === calendarName, ); if (!match?.id) { return { error: { status: 404, body: { error: `No calendar named "${args.calendarName}"`, calendars: calendars.map((calendar) => ({ id: calendar.id, name: calendar.name })), }, }, }; } return { calendarId: match.id }; } const first = calendars[0]; if (!first?.id) { return { error: { status: 404, body: { error: "No calendars found" } } }; } return { calendarId: first.id }; } function requireString(args: Record, key: string): string { const value = args[key]; if (typeof value !== "string" || value.trim().length === 0) { throw new Error(`Missing required argument: ${key}`); } return value; } function requireNumber(args: Record, key: string): number { const value = args[key]; if (typeof value !== "number" || !Number.isFinite(value)) { throw new Error(`Missing required argument: ${key}`); } return value; } function requireBoolean(args: Record, key: string): boolean { const value = args[key]; if (typeof value !== "boolean") { throw new Error(`Missing required argument: ${key}`); } return value; } async function getApiDocs(args: Record): Promise { const { readFile } = await import("fs/promises"); const path = await import("path"); const specPath = path.join(process.cwd(), "docs", "api", "openapi.yaml"); const spec = await readFile(specPath, "utf8"); const search = typeof args.search === "string" ? args.search.trim().toLowerCase() : ""; const pathLines = spec .split("\n") .map((line) => line.trim()) .filter((line) => line.startsWith("/api/v1/")); if (!search) { return { status: 200, body: { paths: pathLines.slice(0, 80), pathCount: pathLines.length, hint: "Pass search (e.g. calendar, events, lists) to get matching lines. Do not request the full OpenAPI dump.", }, }; } const lines = spec.split("\n"); const matches = lines.filter((line) => line.toLowerCase().includes(search)); return { status: 200, body: { search, matchCount: matches.length, matches: matches.slice(0, 40), hint: "Use a dedicated tool when one exists; otherwise call_api with method and path from the matches above.", }, }; } async function callApiFallback( args: Record, request: Request, origin: string, ): Promise { const method = requireString(args, "method").toUpperCase(); if (!["GET", "POST", "PATCH", "DELETE"].includes(method)) { return { status: 400, body: { error: "method must be GET, POST, PATCH, or DELETE" } }; } let apiPath = requireString(args, "path"); if (!apiPath.startsWith("/api/v1/")) { return { status: 400, body: { error: "path must start with /api/v1/" } }; } if (apiPath.includes("..")) { return { status: 400, body: { error: "invalid path" } }; } if (typeof args.query === "object" && args.query !== null && !Array.isArray(args.query)) { const qs = new URLSearchParams(); for (const [key, value] of Object.entries(args.query as Record)) { if (value !== undefined && value !== null) qs.set(key, String(value)); } const query = qs.toString(); if (query) apiPath += `?${query}`; } const body = typeof args.body === "object" && args.body !== null && !Array.isArray(args.body) ? (args.body as Record) : undefined; return callApi(request, origin, method, apiPath, body); } async function callApi( request: Request, origin: string, method: string, path: string, body?: Record, ): Promise { const response = await fetch(`${origin}${path}`, { method, headers: { "Content-Type": "application/json", cookie: request.headers.get("cookie") ?? "", }, body: body !== undefined ? JSON.stringify(body) : undefined, }); const text = await response.text(); let parsed: unknown = null; if (text) { try { parsed = JSON.parse(text); } catch { parsed = text; } } return { status: response.status, body: parsed }; }