Última atividade 1769775120

travisshears's Avatar travisshears revisou este gist 1769775120. Ir para a revisão

2 files changed, 518 insertions

COOKING_HELPER.md(arquivo criado)

@@ -0,0 +1,117 @@
1 + # Cooking Helper Script
2 +
3 + A CLI tool for creating, editing, and viewing cooking notes stored in PocketBase. This allows Claude Code to integrate with your cooking notes feature.
4 +
5 + ## Setup
6 +
7 + The script uses your existing `.env` file for PocketBase credentials:
8 + - `POCKET_BASE_HOST` - PocketBase server URL
9 + - `POCKET_BASE_PASSWORD` - PocketBase user password
10 +
11 + ## Commands
12 +
13 + ### List all cooking notes
14 + ```bash
15 + npx ts-node scripts/cooking-helper.ts list
16 + ```
17 + Returns JSON with all cooking notes, sorted by most recently updated.
18 +
19 + ### View a specific note
20 + ```bash
21 + npx ts-node scripts/cooking-helper.ts view <slug>
22 + ```
23 + Example: `npx ts-node scripts/cooking-helper.ts view dashi`
24 +
25 + ### Fetch recipe from URL
26 + ```bash
27 + npx ts-node scripts/cooking-helper.ts fetch-recipe <url>
28 + ```
29 + Fetches and parses a recipe from a URL. Returns extracted title and content to help write a cooking note in your style.
30 +
31 + Example: `npx ts-node scripts/cooking-helper.ts fetch-recipe https://example.com/recipe`
32 +
33 + ### Create a new note
34 + ```bash
35 + npx ts-node scripts/cooking-helper.ts create '<json>'
36 + ```
37 + Create a new cooking note from JSON data.
38 +
39 + **Required fields:** `title`, `slug`, `content`
40 + **Optional fields:** `description`, `tags` (array of strings)
41 +
42 + Example:
43 + ```bash
44 + 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"]}'
45 + ```
46 +
47 + ### Update a note
48 + ```bash
49 + npx ts-node scripts/cooking-helper.ts update <id> '<json>'
50 + ```
51 + Update an existing note by ID. Only fields provided in JSON will be updated.
52 +
53 + Example:
54 + ```bash
55 + npx ts-node scripts/cooking-helper.ts update abc123 '{"content":"Updated content here","tags":["updated","tag"]}'
56 + ```
57 +
58 + ### Delete a note
59 + ```bash
60 + npx ts-node scripts/cooking-helper.ts delete <id>
61 + ```
62 + Permanently delete a cooking note.
63 +
64 + ## Workflow Example
65 +
66 + ### Creating a recipe from a website
67 +
68 + 1. Fetch the recipe URL:
69 + ```bash
70 + npx ts-node scripts/cooking-helper.ts fetch-recipe https://www.example.com/recipe
71 + ```
72 +
73 + 2. Review the extracted content
74 +
75 + 3. Write a cooking note in your style based on the content (using Claude's assistance)
76 +
77 + 4. Create the note:
78 + ```bash
79 + npx ts-node scripts/cooking-helper.ts create '{"title":"...","slug":"...","description":"...","content":"...","tags":[...]}'
80 + ```
81 +
82 + ## Output Format
83 +
84 + All commands return JSON output for easy parsing:
85 +
86 + **Success response:**
87 + ```json
88 + {
89 + "success": true,
90 + "message": "...",
91 + "note": { ... }
92 + }
93 + ```
94 +
95 + **Error response:**
96 + ```json
97 + {
98 + "success": false,
99 + "error": "error message"
100 + }
101 + ```
102 +
103 + ## Tips
104 +
105 + - **Slugs**: Use lowercase, hyphenated slugs (e.g., `pasta-carbonara`)
106 + - **Content**: Use Markdown format with sections like `## Ingredients`, `## Steps`, `## Notes`
107 + - **Tags**: Use simple, lowercase tags for categorization (e.g., `pasta`, `vegetarian`, `quick`)
108 + - **IDs**: Get IDs from `list` or `view` commands to use in `update`/`delete`
109 +
110 + ## With Claude Code
111 +
112 + You can use this script directly in Claude Code to:
113 + 1. Ask Claude to create a cooking note from a recipe URL
114 + 2. Ask Claude to update existing notes
115 + 3. Ask Claude to view notes for reference
116 +
117 + Example prompt: "Create a cooking note for pasta carbonara based on this URL: https://example.com/recipe"

cooking-helper.ts(arquivo criado)

@@ -0,0 +1,401 @@
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 + })();
Próximo Anterior