Cooking Helper Script
A CLI tool for creating, editing, and viewing cooking notes stored in PocketBase. This allows Claude Code to integrate with your cooking notes feature.
Setup
The script uses your existing .env file for PocketBase credentials:
POCKET_BASE_HOST- PocketBase server URLPOCKET_BASE_PASSWORD- PocketBase user password
Commands
List all cooking notes
npx ts-node scripts/cooking-helper.ts list
Returns JSON with all cooking notes, sorted by most recently updated.
View a specific note
npx ts-node scripts/cooking-helper.ts view <slug>
Example: npx ts-node scripts/cooking-helper.ts view dashi
Fetch recipe from URL
npx ts-node scripts/cooking-helper.ts fetch-recipe <url>
Fetches and parses a recipe from a URL. Returns extracted title and content to help write a cooking note in your style.
Example: npx ts-node scripts/cooking-helper.ts fetch-recipe https://example.com/recipe
Create a new note
npx ts-node scripts/cooking-helper.ts create '<json>'
Create a new cooking note from JSON data.
Required fields: title, slug, content
Optional fields: description, tags (array of strings)
Example:
npx ts-node scripts/cooking-helper.ts create '{"title":"Pasta Carbonara","slug":"pasta-carbonara","description":"Classic Italian pasta","content":"## Ingredients\n- Pasta\n- Eggs\n- Guanciale\n\n## Steps\n1. Cook pasta\n2. Make sauce","tags":["pasta","italian","quick"]}'
Update a note
npx ts-node scripts/cooking-helper.ts update <id> '<json>'
Update an existing note by ID. Only fields provided in JSON will be updated.
Example:
npx ts-node scripts/cooking-helper.ts update abc123 '{"content":"Updated content here","tags":["updated","tag"]}'
Delete a note
npx ts-node scripts/cooking-helper.ts delete <id>
Permanently delete a cooking note.
Workflow Example
Creating a recipe from a website
- Fetch the recipe URL:
npx ts-node scripts/cooking-helper.ts fetch-recipe https://www.example.com/recipe
-
Review the extracted content
-
Write a cooking note in your style based on the content (using Claude's assistance)
-
Create the note:
npx ts-node scripts/cooking-helper.ts create '{"title":"...","slug":"...","description":"...","content":"...","tags":[...]}'
Output Format
All commands return JSON output for easy parsing:
Success response:
{
"success": true,
"message": "...",
"note": { ... }
}
Error response:
{
"success": false,
"error": "error message"
}
Tips
- Slugs: Use lowercase, hyphenated slugs (e.g.,
pasta-carbonara) - Content: Use Markdown format with sections like
## Ingredients,## Steps,## Notes - Tags: Use simple, lowercase tags for categorization (e.g.,
pasta,vegetarian,quick) - IDs: Get IDs from
listorviewcommands to use inupdate/delete
With Claude Code
You can use this script directly in Claude Code to:
- Ask Claude to create a cooking note from a recipe URL
- Ask Claude to update existing notes
- Ask Claude to view notes for reference
Example prompt: "Create a cooking note for pasta carbonara based on this URL: https://example.com/recipe"
| 1 | /** |
| 2 | * Cooking Notes Helper Script |
| 3 | * Allows Claude to create, edit, and view cooking notes from PocketBase |
| 4 | * |
| 5 | * Usage: |
| 6 | * npx ts-node scripts/cooking-helper.ts list |
| 7 | * npx ts-node scripts/cooking-helper.ts view <slug> |
| 8 | * npx ts-node scripts/cooking-helper.ts fetch-recipe <url> |
| 9 | * npx ts-node scripts/cooking-helper.ts create <json> |
| 10 | * npx ts-node scripts/cooking-helper.ts update <id> <json> |
| 11 | */ |
| 12 | |
| 13 | import PocketBase from "pocketbase"; |
| 14 | import * as fs from "fs"; |
| 15 | import * as path from "path"; |
| 16 | |
| 17 | // Load env from .env file |
| 18 | function loadEnv() { |
| 19 | const envPath = path.join(process.cwd(), ".env"); |
| 20 | const envContent = fs.readFileSync(envPath, "utf-8"); |
| 21 | const env: Record<string, string> = {}; |
| 22 | |
| 23 | envContent.split("\n").forEach((line) => { |
| 24 | const trimmed = line.trim(); |
| 25 | if (trimmed && !trimmed.startsWith("#")) { |
| 26 | const [key, ...valueParts] = trimmed.split("="); |
| 27 | const value = valueParts.join("=").replace(/^"(.*)"$/, "$1"); |
| 28 | env[key] = value; |
| 29 | } |
| 30 | }); |
| 31 | |
| 32 | return env; |
| 33 | } |
| 34 | |
| 35 | const env = loadEnv(); |
| 36 | |
| 37 | const pb = new PocketBase(env.POCKET_BASE_HOST); |
| 38 | |
| 39 | // Authenticate with PocketBase |
| 40 | async function authenticate() { |
| 41 | try { |
| 42 | const userName = "personal_site_astro"; |
| 43 | const pw = env.POCKET_BASE_PASSWORD; |
| 44 | |
| 45 | await pb.collection("users").authWithPassword(userName, pw); |
| 46 | } catch (e: any) { |
| 47 | console.error("Auth failed:", e.message); |
| 48 | throw new Error("Failed to authenticate with PocketBase"); |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | type CookingNote = { |
| 53 | id: string; |
| 54 | content: string; |
| 55 | slug: string; |
| 56 | title: string; |
| 57 | description: string; |
| 58 | updated: string; |
| 59 | expand?: { |
| 60 | tags: Array<{ id: string; tag: string }>; |
| 61 | }; |
| 62 | }; |
| 63 | |
| 64 | async function listNotes() { |
| 65 | await authenticate(); |
| 66 | const notes = await pb |
| 67 | .collection< |
| 68 | Pick<CookingNote, "slug" | "title" | "updated"> |
| 69 | >("cooking_notes") |
| 70 | .getList(1, 100, { |
| 71 | sort: "-updated", |
| 72 | fields: "slug,title,updated", |
| 73 | }); |
| 74 | |
| 75 | console.log( |
| 76 | JSON.stringify( |
| 77 | { |
| 78 | success: true, |
| 79 | count: notes.items.length, |
| 80 | notes: notes.items, |
| 81 | }, |
| 82 | null, |
| 83 | 2, |
| 84 | ), |
| 85 | ); |
| 86 | } |
| 87 | |
| 88 | async function viewNote(slug: string) { |
| 89 | await authenticate(); |
| 90 | try { |
| 91 | const note = await pb |
| 92 | .collection<CookingNote>("cooking_notes") |
| 93 | .getFirstListItem(`slug = '${slug}'`, { |
| 94 | expand: "tags", |
| 95 | }); |
| 96 | |
| 97 | console.log( |
| 98 | JSON.stringify( |
| 99 | { |
| 100 | success: true, |
| 101 | note, |
| 102 | }, |
| 103 | null, |
| 104 | 2, |
| 105 | ), |
| 106 | ); |
| 107 | } catch (e: any) { |
| 108 | if (e.status === 404) { |
| 109 | console.log( |
| 110 | JSON.stringify( |
| 111 | { |
| 112 | success: false, |
| 113 | error: "Note not found", |
| 114 | }, |
| 115 | null, |
| 116 | 2, |
| 117 | ), |
| 118 | ); |
| 119 | } else { |
| 120 | throw e; |
| 121 | } |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | async function fetchRecipe(url: string) { |
| 126 | try { |
| 127 | const response = await fetch(url); |
| 128 | const html = await response.text(); |
| 129 | |
| 130 | // Extract recipe content using marked and some basic parsing |
| 131 | const titleMatch = html.match(/<title>([^<]+)<\/title>/); |
| 132 | const title = titleMatch ? titleMatch[1].trim() : "Recipe from " + url; |
| 133 | |
| 134 | // Extract main content (very basic - you might need to customize per recipe site) |
| 135 | const contentMatch = html.match( |
| 136 | /<(article|main|div[^>]*class="[^"]*recipe[^"]*"[^>]*)>([\s\S]*?)<\/(article|main|div)>/i, |
| 137 | ); |
| 138 | let content = contentMatch ? contentMatch[2] : html; |
| 139 | |
| 140 | // Basic HTML to text conversion |
| 141 | content = content |
| 142 | .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "") |
| 143 | .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "") |
| 144 | .replace(/<[^>]+>/g, "\n") |
| 145 | .replace(/\n\n+/g, "\n") |
| 146 | .trim(); |
| 147 | |
| 148 | console.log( |
| 149 | JSON.stringify( |
| 150 | { |
| 151 | success: true, |
| 152 | title, |
| 153 | content: content.substring(0, 2000), // First 2000 chars |
| 154 | sourceUrl: url, |
| 155 | instructions: [ |
| 156 | "Use the above content as a reference to write a cooking note", |
| 157 | "Match the style of your other cooking notes", |
| 158 | "Format: include ingredients, steps, timing, difficulty", |
| 159 | "Use tags for categories (e.g., 'vegetarian', 'quick', 'pasta')", |
| 160 | ], |
| 161 | }, |
| 162 | null, |
| 163 | 2, |
| 164 | ), |
| 165 | ); |
| 166 | } catch (e: any) { |
| 167 | console.log( |
| 168 | JSON.stringify( |
| 169 | { |
| 170 | success: false, |
| 171 | error: "Failed to fetch recipe: " + e.message, |
| 172 | }, |
| 173 | null, |
| 174 | 2, |
| 175 | ), |
| 176 | ); |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | async function createNote(dataStr: string) { |
| 181 | await authenticate(); |
| 182 | |
| 183 | try { |
| 184 | const data = JSON.parse(dataStr); |
| 185 | |
| 186 | // Validate required fields |
| 187 | if (!data.title || !data.slug || !data.content) { |
| 188 | throw new Error("Missing required fields: title, slug, content"); |
| 189 | } |
| 190 | |
| 191 | // Get or create tags |
| 192 | const tagIds: string[] = []; |
| 193 | if (data.tags && Array.isArray(data.tags)) { |
| 194 | for (const tag of data.tags) { |
| 195 | try { |
| 196 | const existingTag = await pb |
| 197 | .collection("cooking_tags") |
| 198 | .getFirstListItem(`tag = '${tag}'`); |
| 199 | tagIds.push(existingTag.id); |
| 200 | } catch (e: any) { |
| 201 | if (e.status === 404) { |
| 202 | const newTag = await pb.collection("cooking_tags").create({ tag }); |
| 203 | tagIds.push(newTag.id); |
| 204 | } else { |
| 205 | throw e; |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | const note = await pb.collection<CookingNote>("cooking_notes").create({ |
| 212 | title: data.title, |
| 213 | slug: data.slug, |
| 214 | content: data.content, |
| 215 | description: data.description || "", |
| 216 | tags: tagIds, |
| 217 | }); |
| 218 | |
| 219 | console.log( |
| 220 | JSON.stringify( |
| 221 | { |
| 222 | success: true, |
| 223 | message: "Note created successfully", |
| 224 | note, |
| 225 | }, |
| 226 | null, |
| 227 | 2, |
| 228 | ), |
| 229 | ); |
| 230 | } catch (e: any) { |
| 231 | console.log( |
| 232 | JSON.stringify( |
| 233 | { |
| 234 | success: false, |
| 235 | error: e.message, |
| 236 | }, |
| 237 | null, |
| 238 | 2, |
| 239 | ), |
| 240 | ); |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | async function updateNote(id: string, dataStr: string) { |
| 245 | await authenticate(); |
| 246 | |
| 247 | try { |
| 248 | const data = JSON.parse(dataStr); |
| 249 | |
| 250 | // Get or create tags |
| 251 | let tagIds: string[] = []; |
| 252 | if (data.tags && Array.isArray(data.tags)) { |
| 253 | for (const tag of data.tags) { |
| 254 | try { |
| 255 | const existingTag = await pb |
| 256 | .collection("cooking_tags") |
| 257 | .getFirstListItem(`tag = '${tag}'`); |
| 258 | tagIds.push(existingTag.id); |
| 259 | } catch (e: any) { |
| 260 | if (e.status === 404) { |
| 261 | const newTag = await pb.collection("cooking_tags").create({ tag }); |
| 262 | tagIds.push(newTag.id); |
| 263 | } else { |
| 264 | throw e; |
| 265 | } |
| 266 | } |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | const updateData: Record<string, any> = {}; |
| 271 | if (data.title) updateData.title = data.title; |
| 272 | if (data.slug) updateData.slug = data.slug; |
| 273 | if (data.content) updateData.content = data.content; |
| 274 | if (data.description) updateData.description = data.description; |
| 275 | if (tagIds.length > 0) updateData.tags = tagIds; |
| 276 | |
| 277 | const note = await pb |
| 278 | .collection<CookingNote>("cooking_notes") |
| 279 | .update(id, updateData); |
| 280 | |
| 281 | console.log( |
| 282 | JSON.stringify( |
| 283 | { |
| 284 | success: true, |
| 285 | message: "Note updated successfully", |
| 286 | note, |
| 287 | }, |
| 288 | null, |
| 289 | 2, |
| 290 | ), |
| 291 | ); |
| 292 | } catch (e: any) { |
| 293 | console.log( |
| 294 | JSON.stringify( |
| 295 | { |
| 296 | success: false, |
| 297 | error: e.message, |
| 298 | }, |
| 299 | null, |
| 300 | 2, |
| 301 | ), |
| 302 | ); |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | async function deleteNote(id: string) { |
| 307 | await authenticate(); |
| 308 | |
| 309 | try { |
| 310 | await pb.collection("cooking_notes").delete(id); |
| 311 | |
| 312 | console.log( |
| 313 | JSON.stringify( |
| 314 | { |
| 315 | success: true, |
| 316 | message: "Note deleted successfully", |
| 317 | id, |
| 318 | }, |
| 319 | null, |
| 320 | 2, |
| 321 | ), |
| 322 | ); |
| 323 | } catch (e: any) { |
| 324 | console.log( |
| 325 | JSON.stringify( |
| 326 | { |
| 327 | success: false, |
| 328 | error: e.message, |
| 329 | }, |
| 330 | null, |
| 331 | 2, |
| 332 | ), |
| 333 | ); |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | // Main |
| 338 | const command = process.argv[2]; |
| 339 | const arg1 = process.argv[3]; |
| 340 | const arg2 = process.argv[4]; |
| 341 | |
| 342 | (async () => { |
| 343 | try { |
| 344 | switch (command) { |
| 345 | case "list": |
| 346 | await listNotes(); |
| 347 | break; |
| 348 | case "view": |
| 349 | if (!arg1) throw new Error("Missing slug argument"); |
| 350 | await viewNote(arg1); |
| 351 | break; |
| 352 | case "fetch-recipe": |
| 353 | if (!arg1) throw new Error("Missing URL argument"); |
| 354 | await fetchRecipe(arg1); |
| 355 | break; |
| 356 | case "create": |
| 357 | if (!arg1) throw new Error("Missing JSON argument"); |
| 358 | await createNote(arg1); |
| 359 | break; |
| 360 | case "update": |
| 361 | if (!arg1 || !arg2) throw new Error("Missing id or JSON argument"); |
| 362 | await updateNote(arg1, arg2); |
| 363 | break; |
| 364 | case "delete": |
| 365 | if (!arg1) throw new Error("Missing id argument"); |
| 366 | await deleteNote(arg1); |
| 367 | break; |
| 368 | default: |
| 369 | console.error(`Unknown command: ${command}`); |
| 370 | console.error(`Available commands: |
| 371 | list - List all cooking notes |
| 372 | view <slug> - View a specific note |
| 373 | fetch-recipe <url> - Fetch and parse recipe from URL |
| 374 | create <json> - Create new note from JSON |
| 375 | update <id> <json> - Update existing note |
| 376 | delete <id> - Delete a note |
| 377 | |
| 378 | Example create JSON: |
| 379 | { |
| 380 | "title": "Pasta Carbonara", |
| 381 | "slug": "pasta-carbonara", |
| 382 | "description": "Classic Italian pasta", |
| 383 | "content": "...", |
| 384 | "tags": ["pasta", "italian", "quick"] |
| 385 | }`); |
| 386 | process.exit(1); |
| 387 | } |
| 388 | } catch (e: any) { |
| 389 | console.log( |
| 390 | JSON.stringify( |
| 391 | { |
| 392 | success: false, |
| 393 | error: e.message, |
| 394 | }, |
| 395 | null, |
| 396 | 2, |
| 397 | ), |
| 398 | ); |
| 399 | process.exit(1); |
| 400 | } |
| 401 | })(); |
| 402 |