Zuletzt aktiv 1769775120

Änderung 911972d86c52cd571e7a7c194b4b1ac88f924c5e

COOKING_HELPER.md Orginalformat

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 URL
  • POCKET_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

  1. Fetch the recipe URL:
npx ts-node scripts/cooking-helper.ts fetch-recipe https://www.example.com/recipe
  1. Review the extracted content

  2. Write a cooking note in your style based on the content (using Claude's assistance)

  3. 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 list or view commands to use in update/delete

With Claude Code

You can use this script directly in Claude Code to:

  1. Ask Claude to create a cooking note from a recipe URL
  2. Ask Claude to update existing notes
  3. Ask Claude to view notes for reference

Example prompt: "Create a cooking note for pasta carbonara based on this URL: https://example.com/recipe"

cooking-helper.ts Orginalformat
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
13import PocketBase from "pocketbase";
14import * as fs from "fs";
15import * as path from "path";
16
17// Load env from .env file
18function 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
35const env = loadEnv();
36
37const pb = new PocketBase(env.POCKET_BASE_HOST);
38
39// Authenticate with PocketBase
40async 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
52type 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
64async 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
88async 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
125async 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
180async 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
244async 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
306async 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
338const command = process.argv[2];
339const arg1 = process.argv[3];
340const 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
378Example 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