/** * Cooking Notes Helper Script * Allows Claude to create, edit, and view cooking notes from PocketBase * * Usage: * npx ts-node scripts/cooking-helper.ts list * npx ts-node scripts/cooking-helper.ts view * npx ts-node scripts/cooking-helper.ts fetch-recipe * npx ts-node scripts/cooking-helper.ts create * npx ts-node scripts/cooking-helper.ts update */ import PocketBase from "pocketbase"; import * as fs from "fs"; import * as path from "path"; // Load env from .env file function loadEnv() { const envPath = path.join(process.cwd(), ".env"); const envContent = fs.readFileSync(envPath, "utf-8"); const env: Record = {}; envContent.split("\n").forEach((line) => { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith("#")) { const [key, ...valueParts] = trimmed.split("="); const value = valueParts.join("=").replace(/^"(.*)"$/, "$1"); env[key] = value; } }); return env; } const env = loadEnv(); const pb = new PocketBase(env.POCKET_BASE_HOST); // Authenticate with PocketBase async function authenticate() { try { const userName = "personal_site_astro"; const pw = env.POCKET_BASE_PASSWORD; await pb.collection("users").authWithPassword(userName, pw); } catch (e: any) { console.error("Auth failed:", e.message); throw new Error("Failed to authenticate with PocketBase"); } } type CookingNote = { id: string; content: string; slug: string; title: string; description: string; updated: string; expand?: { tags: Array<{ id: string; tag: string }>; }; }; async function listNotes() { await authenticate(); const notes = await pb .collection< Pick >("cooking_notes") .getList(1, 100, { sort: "-updated", fields: "slug,title,updated", }); console.log( JSON.stringify( { success: true, count: notes.items.length, notes: notes.items, }, null, 2, ), ); } async function viewNote(slug: string) { await authenticate(); try { const note = await pb .collection("cooking_notes") .getFirstListItem(`slug = '${slug}'`, { expand: "tags", }); console.log( JSON.stringify( { success: true, note, }, null, 2, ), ); } catch (e: any) { if (e.status === 404) { console.log( JSON.stringify( { success: false, error: "Note not found", }, null, 2, ), ); } else { throw e; } } } async function fetchRecipe(url: string) { try { const response = await fetch(url); const html = await response.text(); // Extract recipe content using marked and some basic parsing const titleMatch = html.match(/([^<]+)<\/title>/); const title = titleMatch ? titleMatch[1].trim() : "Recipe from " + url; // Extract main content (very basic - you might need to customize per recipe site) const contentMatch = html.match( /<(article|main|div[^>]*class="[^"]*recipe[^"]*"[^>]*)>([\s\S]*?)<\/(article|main|div)>/i, ); let content = contentMatch ? contentMatch[2] : html; // Basic HTML to text conversion content = content .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "") .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "") .replace(/<[^>]+>/g, "\n") .replace(/\n\n+/g, "\n") .trim(); console.log( JSON.stringify( { success: true, title, content: content.substring(0, 2000), // First 2000 chars sourceUrl: url, instructions: [ "Use the above content as a reference to write a cooking note", "Match the style of your other cooking notes", "Format: include ingredients, steps, timing, difficulty", "Use tags for categories (e.g., 'vegetarian', 'quick', 'pasta')", ], }, null, 2, ), ); } catch (e: any) { console.log( JSON.stringify( { success: false, error: "Failed to fetch recipe: " + e.message, }, null, 2, ), ); } } async function createNote(dataStr: string) { await authenticate(); try { const data = JSON.parse(dataStr); // Validate required fields if (!data.title || !data.slug || !data.content) { throw new Error("Missing required fields: title, slug, content"); } // Get or create tags const tagIds: string[] = []; if (data.tags && Array.isArray(data.tags)) { for (const tag of data.tags) { try { const existingTag = await pb .collection("cooking_tags") .getFirstListItem(`tag = '${tag}'`); tagIds.push(existingTag.id); } catch (e: any) { if (e.status === 404) { const newTag = await pb.collection("cooking_tags").create({ tag }); tagIds.push(newTag.id); } else { throw e; } } } } const note = await pb.collection<CookingNote>("cooking_notes").create({ title: data.title, slug: data.slug, content: data.content, description: data.description || "", tags: tagIds, }); console.log( JSON.stringify( { success: true, message: "Note created successfully", note, }, null, 2, ), ); } catch (e: any) { console.log( JSON.stringify( { success: false, error: e.message, }, null, 2, ), ); } } async function updateNote(id: string, dataStr: string) { await authenticate(); try { const data = JSON.parse(dataStr); // Get or create tags let tagIds: string[] = []; if (data.tags && Array.isArray(data.tags)) { for (const tag of data.tags) { try { const existingTag = await pb .collection("cooking_tags") .getFirstListItem(`tag = '${tag}'`); tagIds.push(existingTag.id); } catch (e: any) { if (e.status === 404) { const newTag = await pb.collection("cooking_tags").create({ tag }); tagIds.push(newTag.id); } else { throw e; } } } } const updateData: Record<string, any> = {}; if (data.title) updateData.title = data.title; if (data.slug) updateData.slug = data.slug; if (data.content) updateData.content = data.content; if (data.description) updateData.description = data.description; if (tagIds.length > 0) updateData.tags = tagIds; const note = await pb .collection<CookingNote>("cooking_notes") .update(id, updateData); console.log( JSON.stringify( { success: true, message: "Note updated successfully", note, }, null, 2, ), ); } catch (e: any) { console.log( JSON.stringify( { success: false, error: e.message, }, null, 2, ), ); } } async function deleteNote(id: string) { await authenticate(); try { await pb.collection("cooking_notes").delete(id); console.log( JSON.stringify( { success: true, message: "Note deleted successfully", id, }, null, 2, ), ); } catch (e: any) { console.log( JSON.stringify( { success: false, error: e.message, }, null, 2, ), ); } } // Main const command = process.argv[2]; const arg1 = process.argv[3]; const arg2 = process.argv[4]; (async () => { try { switch (command) { case "list": await listNotes(); break; case "view": if (!arg1) throw new Error("Missing slug argument"); await viewNote(arg1); break; case "fetch-recipe": if (!arg1) throw new Error("Missing URL argument"); await fetchRecipe(arg1); break; case "create": if (!arg1) throw new Error("Missing JSON argument"); await createNote(arg1); break; case "update": if (!arg1 || !arg2) throw new Error("Missing id or JSON argument"); await updateNote(arg1, arg2); break; case "delete": if (!arg1) throw new Error("Missing id argument"); await deleteNote(arg1); break; default: console.error(`Unknown command: ${command}`); console.error(`Available commands: list - List all cooking notes view <slug> - View a specific note fetch-recipe <url> - Fetch and parse recipe from URL create <json> - Create new note from JSON update <id> <json> - Update existing note delete <id> - Delete a note Example create JSON: { "title": "Pasta Carbonara", "slug": "pasta-carbonara", "description": "Classic Italian pasta", "content": "...", "tags": ["pasta", "italian", "quick"] }`); process.exit(1); } } catch (e: any) { console.log( JSON.stringify( { success: false, error: e.message, }, null, 2, ), ); process.exit(1); } })();