pantrybase 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +8 -2
- package/README.md +4 -0
- package/dist/{chunk-2EMC6TJB.js → chunk-IUQQ3I2E.js} +605 -8
- package/dist/cli.js +1 -1
- package/dist/server.js +239 -61
- package/dist/web/assets/{graph-Mpnwdw0-.js → graph-70CI6xc-.js} +1 -1
- package/dist/web/assets/{index-PLKhwbDz.css → index--7z6Fb_H.css} +1 -1
- package/dist/web/assets/index-PBzjTL7i.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/providers/generic/profile.yaml +1 -0
- package/providers/ucook/guide.md +1 -1
- package/providers/ucook/profile.yaml +2 -0
- package/skills/cook/SKILL.md +9 -1
- package/skills/ingest/SKILL.md +6 -5
- package/dist/web/assets/index-BQVf10Ok.js +0 -1
package/dist/cli.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -5,9 +5,11 @@ import {
|
|
|
5
5
|
MODEL_MAX_PX,
|
|
6
6
|
SOURCE_KINDS,
|
|
7
7
|
addToPantry,
|
|
8
|
+
autofillPlan,
|
|
8
9
|
availableIngredients,
|
|
9
10
|
closeAllContexts,
|
|
10
11
|
finalizeAttach,
|
|
12
|
+
getPlan,
|
|
11
13
|
imageForModel,
|
|
12
14
|
importPaths,
|
|
13
15
|
listPantry,
|
|
@@ -26,14 +28,19 @@ import {
|
|
|
26
28
|
recipeInputSchema,
|
|
27
29
|
reconcile,
|
|
28
30
|
removeFromPantry,
|
|
31
|
+
renderShoppingList,
|
|
29
32
|
resolveItem,
|
|
30
33
|
resolveItems,
|
|
34
|
+
rotateItem,
|
|
35
|
+
savePlan,
|
|
31
36
|
scoreRecipes,
|
|
32
37
|
searchRecipes,
|
|
38
|
+
shoppingList,
|
|
33
39
|
startUiServer,
|
|
34
|
-
|
|
40
|
+
suggestRecipes,
|
|
41
|
+
viewItems,
|
|
35
42
|
writeRecipeFile
|
|
36
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-IUQQ3I2E.js";
|
|
37
44
|
|
|
38
45
|
// src/server.ts
|
|
39
46
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -136,7 +143,7 @@ import path from "path";
|
|
|
136
143
|
import { z as z3 } from "zod";
|
|
137
144
|
var pb_inbox_image_default = defineTool({
|
|
138
145
|
name: "pb_inbox_image",
|
|
139
|
-
description: "Show one queued (or archived) photo as an image, resized for reading (long edge \u2264 1600px by default; ask for up to 2400 if small print is unreadable). HEIC is converted automatically.",
|
|
146
|
+
description: "Show one queued (or archived) photo as an image, resized for reading (long edge \u2264 1600px by default; ask for up to 2400 if small print is unreadable). HEIC is converted automatically. If the text runs sideways or upside down, fix it with pb_inbox_rotate and look again.",
|
|
140
147
|
schema: {
|
|
141
148
|
id: z3.number().int(),
|
|
142
149
|
max_px: z3.number().int().min(400).max(2400).optional()
|
|
@@ -175,7 +182,10 @@ var pb_inbox_list_default = defineTool({
|
|
|
175
182
|
},
|
|
176
183
|
handler: ({ status, limit, offset }, ctx) => {
|
|
177
184
|
const items = ctx.store.listIngest({ status, limit: limit ?? 20, offset });
|
|
178
|
-
return text({
|
|
185
|
+
return text({
|
|
186
|
+
counts: ctx.store.ingestCounts(),
|
|
187
|
+
items: viewItems(items, ctx.store.allIngest())
|
|
188
|
+
});
|
|
179
189
|
}
|
|
180
190
|
});
|
|
181
191
|
|
|
@@ -183,7 +193,7 @@ var pb_inbox_list_default = defineTool({
|
|
|
183
193
|
import { z as z5 } from "zod";
|
|
184
194
|
var pb_inbox_next_default = defineTool({
|
|
185
195
|
name: "pb_inbox_next",
|
|
186
|
-
description: "The next pending inbox photos in capture order, plus queue progress. Cards are usually shot front then back (a back has no title), so look at the photos in order with pb_inbox_image, group the ones that belong to one card, and save it with pb_save_recipe {ingest:[{id, side}]}. `retake_of` flags a near-identical photo: attach it as an extra image or skip it with pb_inbox_resolve.",
|
|
196
|
+
description: "The next pending inbox photos in capture order, plus queue progress. Cards are usually shot front then back (a back has no title), so look at the photos in order with pb_inbox_image, group the ones that belong to one card, and save it with pb_save_recipe {ingest:[{id, side}]}. `retake_of` flags a near-identical photo: attach it as an extra image or skip it with pb_inbox_resolve. `rotation_hint` flags a photo whose shape differs from the rest \u2014 rotate it with pb_inbox_rotate if it reads sideways.",
|
|
187
197
|
schema: {
|
|
188
198
|
window: z5.number().int().min(1).max(12).optional().describe("How many photos to return (default 4)")
|
|
189
199
|
},
|
|
@@ -210,14 +220,26 @@ var pb_inbox_resolve_default = defineTool({
|
|
|
210
220
|
}
|
|
211
221
|
});
|
|
212
222
|
|
|
213
|
-
// src/mcp/tools/
|
|
223
|
+
// src/mcp/tools/pb_inbox_rotate.ts
|
|
214
224
|
import { z as z7 } from "zod";
|
|
225
|
+
var pb_inbox_rotate_default = defineTool({
|
|
226
|
+
name: "pb_inbox_rotate",
|
|
227
|
+
description: "Turn a photo upright when its text runs sideways or upside down (camera orientation flags are already applied, so this is for photos whose pixels are rotated). Degrees are clockwise: text reading bottom-to-top needs 90, upside down needs 180, top-to-bottom needs 270. The file is fixed on disk (HEIC becomes JPEG), so the archive, thumbnails and retake detection all see the upright photo. Call before pb_inbox_image when pb_inbox_next gives a rotation_hint, or after a photo shows up sideways; view it again to confirm.",
|
|
228
|
+
schema: {
|
|
229
|
+
id: z7.number().int(),
|
|
230
|
+
degrees: z7.union([z7.literal(90), z7.literal(180), z7.literal(270)])
|
|
231
|
+
},
|
|
232
|
+
handler: async ({ id, degrees }, ctx) => text({ rotated: degrees, item: await rotateItem(ctx, id, degrees) })
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// src/mcp/tools/pb_ingest_source.ts
|
|
236
|
+
import { z as z8 } from "zod";
|
|
215
237
|
var pb_ingest_source_default = defineTool({
|
|
216
238
|
name: "pb_ingest_source",
|
|
217
239
|
description: "Copy recipe photos (JPEG, PNG, WebP, HEIC, TIFF) from files or folders into pantrybase/inbox/ for extraction. Originals are never moved or changed; photos already known (same bytes) are skipped. Folders are searched recursively by default. Returns counts and the queue state; then work through it with pb_inbox_next.",
|
|
218
240
|
schema: {
|
|
219
|
-
paths:
|
|
220
|
-
recursive:
|
|
241
|
+
paths: z8.array(z8.string()).min(1).describe("Files or folders; relative to the project, absolute, or ~/\u2026"),
|
|
242
|
+
recursive: z8.boolean().optional()
|
|
221
243
|
},
|
|
222
244
|
handler: async ({ paths, recursive }, ctx) => {
|
|
223
245
|
const r = await importPaths(ctx, paths, { recursive });
|
|
@@ -234,13 +256,13 @@ var pb_ingest_source_default = defineTool({
|
|
|
234
256
|
});
|
|
235
257
|
|
|
236
258
|
// src/mcp/tools/pb_list_tags.ts
|
|
237
|
-
import { z as
|
|
259
|
+
import { z as z9 } from "zod";
|
|
238
260
|
var pb_list_tags_default = defineTool({
|
|
239
261
|
name: "pb_list_tags",
|
|
240
262
|
description: "Tags and facets in use, with recipe counts, grouped by category. With vocabulary:true also returns the allowed vocabulary (closed categories list every value). Call once before a batch of saves and reuse existing values.",
|
|
241
263
|
schema: {
|
|
242
|
-
category:
|
|
243
|
-
vocabulary:
|
|
264
|
+
category: z9.enum(ALL_CATEGORIES).optional(),
|
|
265
|
+
vocabulary: z9.boolean().optional()
|
|
244
266
|
},
|
|
245
267
|
handler: ({ category, vocabulary }, ctx) => {
|
|
246
268
|
const used = {};
|
|
@@ -259,7 +281,7 @@ var pb_list_tags_default = defineTool({
|
|
|
259
281
|
});
|
|
260
282
|
|
|
261
283
|
// src/mcp/tools/pb_merge_ingredients.ts
|
|
262
|
-
import { z as
|
|
284
|
+
import { z as z10 } from "zod";
|
|
263
285
|
|
|
264
286
|
// src/ingredients/merge.ts
|
|
265
287
|
import fs from "fs";
|
|
@@ -363,8 +385,8 @@ var pb_merge_ingredients_default = defineTool({
|
|
|
363
385
|
name: "pb_merge_ingredients",
|
|
364
386
|
description: "Merge an ingredient into another when they are the same thing (e.g. 'pitted dates' into 'dates'). Rewrites every recipe file that uses it, moves aliases and the pantry entry, and records the alias in ingredients.yaml so future saves resolve to the target. Check pb_save_recipe's new_ingredients after a batch for candidates.",
|
|
365
387
|
schema: {
|
|
366
|
-
from:
|
|
367
|
-
into:
|
|
388
|
+
from: z10.string().min(1),
|
|
389
|
+
into: z10.string().min(1)
|
|
368
390
|
},
|
|
369
391
|
handler: ({ from, into }, ctx) => text(mergeIngredients(ctx, from, into))
|
|
370
392
|
});
|
|
@@ -381,7 +403,7 @@ var pb_open_ui_default = defineTool({
|
|
|
381
403
|
});
|
|
382
404
|
|
|
383
405
|
// src/mcp/tools/pb_pantry.ts
|
|
384
|
-
import { z as
|
|
406
|
+
import { z as z11 } from "zod";
|
|
385
407
|
var pb_pantry_list = defineTool({
|
|
386
408
|
name: "pb_pantry_list",
|
|
387
409
|
description: "What's in the pantry: canonical ingredient names with optional quantity, unit and note.",
|
|
@@ -403,12 +425,12 @@ var pb_pantry_add = defineTool({
|
|
|
403
425
|
name: "pb_pantry_add",
|
|
404
426
|
description: "Add items to the pantry (or update their quantity). Names are matched to the ingredient catalog (aliases, plurals and regional names: 'eggs', 'baby marrow', 'spring onions'); unknown names become new ingredients.",
|
|
405
427
|
schema: {
|
|
406
|
-
items:
|
|
407
|
-
|
|
408
|
-
name:
|
|
409
|
-
qty:
|
|
410
|
-
unit:
|
|
411
|
-
note:
|
|
428
|
+
items: z11.array(
|
|
429
|
+
z11.object({
|
|
430
|
+
name: z11.string().min(1),
|
|
431
|
+
qty: z11.number().nullable().optional(),
|
|
432
|
+
unit: z11.string().nullable().optional(),
|
|
433
|
+
note: z11.string().nullable().optional()
|
|
412
434
|
})
|
|
413
435
|
).min(1)
|
|
414
436
|
},
|
|
@@ -418,8 +440,8 @@ var pb_pantry_remove = defineTool({
|
|
|
418
440
|
name: "pb_pantry_remove",
|
|
419
441
|
description: "Remove items from the pantry by name, or everything with all:true.",
|
|
420
442
|
schema: {
|
|
421
|
-
names:
|
|
422
|
-
all:
|
|
443
|
+
names: z11.array(z11.string()).optional(),
|
|
444
|
+
all: z11.boolean().optional()
|
|
423
445
|
},
|
|
424
446
|
handler: ({ names, all }, ctx) => {
|
|
425
447
|
if (all) return text({ cleared: ctx.store.pantryClear() });
|
|
@@ -428,19 +450,19 @@ var pb_pantry_remove = defineTool({
|
|
|
428
450
|
});
|
|
429
451
|
|
|
430
452
|
// src/mcp/tools/pb_pantry_match.ts
|
|
431
|
-
import { z as
|
|
453
|
+
import { z as z12 } from "zod";
|
|
432
454
|
var pb_pantry_match_default = defineTool({
|
|
433
455
|
name: "pb_pantry_match",
|
|
434
456
|
description: "What can I cook? Ranks recipes by how much of their required ingredients the pantry covers (staples like salt, oil and 'from your kitchen' items, and optional items, never count against a recipe; printed alternatives and the parts of composite ingredients do count). Add `ingredients` for things you have right now; use_pantry:false to match only those. Returns coverage, what's missing, and near misses (1\u20132 items short).",
|
|
435
457
|
schema: {
|
|
436
|
-
ingredients:
|
|
437
|
-
use_pantry:
|
|
438
|
-
min_coverage:
|
|
439
|
-
max_missing:
|
|
440
|
-
limit:
|
|
441
|
-
tags:
|
|
442
|
-
exclude_allergens:
|
|
443
|
-
max_total_min:
|
|
458
|
+
ingredients: z12.array(z12.string()).optional(),
|
|
459
|
+
use_pantry: z12.boolean().optional().describe("default true"),
|
|
460
|
+
min_coverage: z12.number().min(0).max(1).optional().describe("default 0.5"),
|
|
461
|
+
max_missing: z12.number().int().min(0).optional(),
|
|
462
|
+
limit: z12.number().int().min(1).max(50).optional().describe("default 10"),
|
|
463
|
+
tags: z12.partialRecord(z12.enum(ALL_CATEGORIES), z12.array(z12.string())).optional(),
|
|
464
|
+
exclude_allergens: z12.array(z12.string()).optional(),
|
|
465
|
+
max_total_min: z12.number().optional()
|
|
444
466
|
},
|
|
445
467
|
handler: ({ ingredients, use_pantry, min_coverage, max_missing, limit, ...filters }, ctx) => {
|
|
446
468
|
const r = matchRecipes(ctx, {
|
|
@@ -466,13 +488,163 @@ var pb_pantry_match_default = defineTool({
|
|
|
466
488
|
}
|
|
467
489
|
});
|
|
468
490
|
|
|
491
|
+
// src/mcp/tools/pb_plan.ts
|
|
492
|
+
import { z as z13 } from "zod";
|
|
493
|
+
var pb_plan_default = defineTool({
|
|
494
|
+
name: "pb_plan",
|
|
495
|
+
description: "The saved meal plan (shared with the web UI). get: the planned recipes and how much they share. set: replace the plan with `slugs`. add / remove: adjust it. clear: empty it. `servings` sets the portions wanted per recipe (quantities on the shopping list scale from the recipe's own servings).",
|
|
496
|
+
schema: {
|
|
497
|
+
action: z13.enum(["get", "set", "add", "remove", "clear"]),
|
|
498
|
+
slugs: z13.array(z13.string()).optional(),
|
|
499
|
+
servings: z13.record(z13.string(), z13.number().positive()).optional().describe("slug \u2192 portions wanted, e.g. {'harissa-chicken-traybake': 4}")
|
|
500
|
+
},
|
|
501
|
+
handler: ({ action, slugs, servings }, ctx) => {
|
|
502
|
+
const current = getPlan(ctx);
|
|
503
|
+
let items = current.items;
|
|
504
|
+
if (action === "clear") items = [];
|
|
505
|
+
else if (action === "set") items = (slugs ?? []).map((slug) => ({ slug }));
|
|
506
|
+
else if (action === "add") {
|
|
507
|
+
if (!slugs?.length) return errorResult("slugs required");
|
|
508
|
+
const have = new Set(items.map((i) => i.slug));
|
|
509
|
+
items = [...items, ...slugs.filter((s) => !have.has(s)).map((slug) => ({ slug }))];
|
|
510
|
+
} else if (action === "remove") {
|
|
511
|
+
if (!slugs?.length) return errorResult("slugs required");
|
|
512
|
+
const drop = new Set(slugs);
|
|
513
|
+
items = items.filter((i) => !drop.has(i.slug));
|
|
514
|
+
}
|
|
515
|
+
if (servings) {
|
|
516
|
+
items = items.map((i) => servings[i.slug] ? { ...i, servings: servings[i.slug] } : i);
|
|
517
|
+
}
|
|
518
|
+
const unknown = (slugs ?? []).filter((s) => !ctx.store.getRecipe(s));
|
|
519
|
+
const saved = action === "get" ? current : savePlan(ctx, { items, confirmed: current.confirmed });
|
|
520
|
+
const { plan } = suggestRecipes(ctx, { plan: saved.items.map((i) => i.slug), limit: 1 });
|
|
521
|
+
return text({
|
|
522
|
+
recipes: saved.items.map((i) => ({
|
|
523
|
+
slug: i.slug,
|
|
524
|
+
title: plan.recipes.find((r) => r.slug === i.slug)?.title,
|
|
525
|
+
servings: i.servings ?? void 0
|
|
526
|
+
})),
|
|
527
|
+
to_buy: plan.to_buy,
|
|
528
|
+
from_pantry: plan.from_pantry,
|
|
529
|
+
shared: plan.shared,
|
|
530
|
+
reuse: plan.reuse,
|
|
531
|
+
confirmed_pantry: saved.confirmed.length ? saved.confirmed : void 0,
|
|
532
|
+
unknown_slugs: unknown.length ? unknown : void 0
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// src/mcp/tools/pb_plan_shopping_list.ts
|
|
538
|
+
import { z as z14 } from "zod";
|
|
539
|
+
var pb_plan_shopping_list_default = defineTool({
|
|
540
|
+
name: "pb_plan_shopping_list",
|
|
541
|
+
description: "Grocery list for the saved plan (or `slugs`): distinct items to buy with merged quantities, grouped by aisle, each with the recipes that use it. `pantry_check` lists the pantry items the plan relies on \u2014 ask the user to confirm those are really there before the list is final; pass the ones they confirm as `confirmed` and the ones they're out of as `out_of` (those leave the pantry and move onto the list). `needs_confirmation` is empty once everything is settled. Staples (salt, oil\u2026) are listed separately.",
|
|
542
|
+
schema: {
|
|
543
|
+
slugs: z14.array(z14.string()).optional().describe("defaults to the saved plan"),
|
|
544
|
+
servings: z14.record(z14.string(), z14.number().positive()).optional().describe("slug \u2192 portions wanted; scales quantities"),
|
|
545
|
+
confirmed: z14.array(z14.string()).optional().describe("pantry items the user confirmed"),
|
|
546
|
+
out_of: z14.array(z14.string()).optional().describe("pantry items the user is out of"),
|
|
547
|
+
format: z14.enum(["json", "text"]).optional().describe("default json; text is paste-ready")
|
|
548
|
+
},
|
|
549
|
+
handler: ({ slugs, servings, confirmed, out_of, format }, ctx) => {
|
|
550
|
+
const items = slugs?.map((slug) => ({ slug, servings: servings?.[slug] ?? null }));
|
|
551
|
+
const list = shoppingList(ctx, { items, confirmed, out_of });
|
|
552
|
+
if (format === "text") return text(renderShoppingList(list));
|
|
553
|
+
return text({
|
|
554
|
+
recipes: list.recipes.map((r) => ({
|
|
555
|
+
title: r.title,
|
|
556
|
+
servings: r.servings ?? void 0,
|
|
557
|
+
scaled: r.scale !== 1 ? `\xD7${Math.round(r.scale * 100) / 100}` : void 0
|
|
558
|
+
})),
|
|
559
|
+
buy: list.buy.map((l) => ({
|
|
560
|
+
item: l.canonical,
|
|
561
|
+
amount: l.amount ?? void 0,
|
|
562
|
+
aisle: l.aisle,
|
|
563
|
+
for: l.for
|
|
564
|
+
})),
|
|
565
|
+
pantry_check: list.pantry_check.map((c) => ({
|
|
566
|
+
item: c.canonical,
|
|
567
|
+
amount: c.amount ?? void 0,
|
|
568
|
+
in_pantry: c.in_pantry ?? void 0,
|
|
569
|
+
for: c.for,
|
|
570
|
+
confirmed: c.confirmed
|
|
571
|
+
})),
|
|
572
|
+
needs_confirmation: list.needs_confirmation.length ? list.needs_confirmation : void 0,
|
|
573
|
+
optional: list.optional.length ? list.optional.map((l) => l.canonical) : void 0,
|
|
574
|
+
staples: list.staples,
|
|
575
|
+
summary: {
|
|
576
|
+
to_buy: list.summary.to_buy,
|
|
577
|
+
from_pantry: list.summary.from_pantry,
|
|
578
|
+
shared: list.summary.shared,
|
|
579
|
+
reuse: list.summary.reuse
|
|
580
|
+
},
|
|
581
|
+
removed_from_pantry: list.removed_from_pantry.length ? list.removed_from_pantry : void 0,
|
|
582
|
+
unknown_slugs: list.unknown_slugs.length ? list.unknown_slugs : void 0
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// src/mcp/tools/pb_plan_suggest.ts
|
|
588
|
+
import { z as z15 } from "zod";
|
|
589
|
+
var pb_plan_suggest_default = defineTool({
|
|
590
|
+
name: "pb_plan_suggest",
|
|
591
|
+
description: "Meal planning around shared ingredients. Given a seed recipe and/or the current plan, ranks recipes by how much of their shopping is already covered \u2014 `shares` (already on the list for another planned recipe), `from_pantry`, and `new` (what they would add). `reuse` is the covered share (0\u20131), weighted so fresh items count more than dry goods. Repeating a planned dish is penalised unless variety:false. With `auto: n` it instead fills the plan with n more recipes that minimise the whole list (beam search) and returns the resulting slugs \u2014 save them with pb_plan. Nothing is saved by this tool.",
|
|
592
|
+
schema: {
|
|
593
|
+
seed: z15.string().optional().describe("recipe slug to plan around"),
|
|
594
|
+
plan: z15.array(z15.string()).optional().describe("slugs already planned; defaults to the saved plan"),
|
|
595
|
+
auto: z15.number().int().min(1).max(14).optional().describe("fill the plan with this many more"),
|
|
596
|
+
limit: z15.number().int().min(1).max(50).optional().describe("default 8"),
|
|
597
|
+
variety: z15.boolean().optional().describe("default true"),
|
|
598
|
+
use_pantry: z15.boolean().optional().describe("default true"),
|
|
599
|
+
tags: z15.partialRecord(z15.enum(ALL_CATEGORIES), z15.array(z15.string())).optional(),
|
|
600
|
+
exclude_allergens: z15.array(z15.string()).optional(),
|
|
601
|
+
exclude_ingredients: z15.array(z15.string()).optional(),
|
|
602
|
+
max_total_min: z15.number().optional()
|
|
603
|
+
},
|
|
604
|
+
handler: ({ seed, plan, auto, limit, variety, use_pantry, ...filters }, ctx) => {
|
|
605
|
+
if (auto) {
|
|
606
|
+
const r2 = autofillPlan(ctx, { seed, plan, count: auto, variety, use_pantry, filters });
|
|
607
|
+
return text({
|
|
608
|
+
plan_slugs: r2.slugs,
|
|
609
|
+
added: r2.added.map((s) => ({
|
|
610
|
+
slug: s.slug,
|
|
611
|
+
title: s.title,
|
|
612
|
+
reuse: s.reuse,
|
|
613
|
+
shares: s.shares,
|
|
614
|
+
from_pantry: s.from_pantry,
|
|
615
|
+
new: s.new,
|
|
616
|
+
repeats: s.repeats
|
|
617
|
+
})),
|
|
618
|
+
summary: r2.plan,
|
|
619
|
+
unknown_slugs: r2.unknown_slugs.length ? r2.unknown_slugs : void 0,
|
|
620
|
+
next: "Show the user the plan; when they agree, pb_plan {action:'set', slugs: plan_slugs}."
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
const r = suggestRecipes(ctx, { seed, plan, limit: limit ?? 8, variety, use_pantry, filters });
|
|
624
|
+
return text({
|
|
625
|
+
plan: r.plan,
|
|
626
|
+
suggestions: r.suggestions.map((s) => ({
|
|
627
|
+
slug: s.slug,
|
|
628
|
+
title: s.title,
|
|
629
|
+
total_min: s.total_min ?? void 0,
|
|
630
|
+
reuse: s.reuse,
|
|
631
|
+
shares: s.shares,
|
|
632
|
+
from_pantry: s.from_pantry,
|
|
633
|
+
new: s.new,
|
|
634
|
+
repeats: s.repeats
|
|
635
|
+
})),
|
|
636
|
+
unknown_slugs: r.unknown_slugs.length ? r.unknown_slugs : void 0
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
469
641
|
// src/mcp/tools/pb_providers.ts
|
|
470
|
-
import { z as
|
|
642
|
+
import { z as z16 } from "zod";
|
|
471
643
|
var pb_providers_default = defineTool({
|
|
472
644
|
name: "pb_providers",
|
|
473
645
|
description: "Meal-box provider profiles. Without `id`: list them (id, name, how to recognise a card). With `id`: the extraction guide for that provider's cards plus the tag vocabulary \u2014 read it once before extracting a batch of cards.",
|
|
474
646
|
schema: {
|
|
475
|
-
id:
|
|
647
|
+
id: z16.string().optional().describe('e.g. "ucook"; "generic" for anything else')
|
|
476
648
|
},
|
|
477
649
|
handler: ({ id }, ctx) => {
|
|
478
650
|
if (!id) {
|
|
@@ -500,6 +672,7 @@ var pb_providers_default = defineTool({
|
|
|
500
672
|
`- Brands stripped from canonical names: ${p.brands.join(", ") || "none"}`,
|
|
501
673
|
`- Kitchen items treated as equipment: ${Object.keys(p.equipment_terms).join(", ") || "none"}`,
|
|
502
674
|
`- Nutrition basis default: ${p.nutrition_basis}; default servings: ${p.default_servings ?? "none"}`,
|
|
675
|
+
`- Upright card shape: ${p.orientation ?? "varies"} (photos that don't match are flagged with rotation_hint; fix them with pb_inbox_rotate)`,
|
|
503
676
|
"",
|
|
504
677
|
"## Tag vocabulary (closed = only these values; open = top values, new ones allowed)",
|
|
505
678
|
...Object.entries(summary).map(
|
|
@@ -511,13 +684,13 @@ var pb_providers_default = defineTool({
|
|
|
511
684
|
});
|
|
512
685
|
|
|
513
686
|
// src/mcp/tools/pb_rebuild.ts
|
|
514
|
-
import { z as
|
|
687
|
+
import { z as z17 } from "zod";
|
|
515
688
|
var pb_rebuild_default = defineTool({
|
|
516
689
|
name: "pb_rebuild",
|
|
517
690
|
description: "Re-sync the index from pantrybase/recipes/*.md (markdown is the source of truth), reloading ingredients.yaml, vocabulary.yaml and provider profiles. Default is incremental (mtime/hash); full:true re-parses every file (use after editing the vocabulary or catalog); reembed:true also recomputes all embeddings.",
|
|
518
691
|
schema: {
|
|
519
|
-
full:
|
|
520
|
-
reembed:
|
|
692
|
+
full: z17.boolean().optional(),
|
|
693
|
+
reembed: z17.boolean().optional()
|
|
521
694
|
},
|
|
522
695
|
handler: ({ full, reembed }, ctx) => {
|
|
523
696
|
const reloaded = ctx.refresh(true);
|
|
@@ -527,7 +700,7 @@ var pb_rebuild_default = defineTool({
|
|
|
527
700
|
});
|
|
528
701
|
|
|
529
702
|
// src/mcp/tools/pb_save_recipe.ts
|
|
530
|
-
import { z as
|
|
703
|
+
import { z as z18 } from "zod";
|
|
531
704
|
|
|
532
705
|
// src/files/slug.ts
|
|
533
706
|
var MAX_SLUG = 60;
|
|
@@ -628,10 +801,10 @@ var pb_save_recipe_default = defineTool({
|
|
|
628
801
|
description: "Save a recipe (new or, with `slug`, an update). Writes pantrybase/recipes/<slug>.md as the source of truth and indexes it. For meal-box cards read pb_providers {id} first and follow its guide; set provider.id so its rules apply. Pass `ingest` with every inbox photo of the card ({id, side}) \u2014 they are archived to images/<slug>/ and marked done; re-sending already-attached photos is a safe no-op. Ingredients need `raw` (as printed) and `canonical` (generic singular name, no brand) plus source box|kitchen. Steps are objects {title, text, techniques, uses, equipment, temp_c, minutes} (plain strings are accepted). Tags by category (protein, diet, starch, flavor, texture, cuisine, dish, meal, method, time, season, other) are normalised against the vocabulary. Write a `blurb` in your own words for semantic search and record unreadable fields in extraction.uncertain. Returns {saved, slug, \u2026} or {saved:false, duplicate_of:[\u2026]} \u2014 if it is a different recipe pass allow_duplicate:true; if it is the same card, attach the photos to that slug or skip them.",
|
|
629
802
|
schema: {
|
|
630
803
|
...recipeInputSchema.shape,
|
|
631
|
-
ingest:
|
|
804
|
+
ingest: z18.array(z18.object({ id: z18.number().int(), side: z18.enum(IMAGE_SIDES) })).optional().describe(
|
|
632
805
|
"Inbox photos of this card, e.g. [{id: 12, side: 'front'}, {id: 13, side: 'back'}]"
|
|
633
806
|
),
|
|
634
|
-
allow_duplicate:
|
|
807
|
+
allow_duplicate: z18.boolean().optional()
|
|
635
808
|
},
|
|
636
809
|
handler: ({ allow_duplicate, ingest, ...input }, ctx) => {
|
|
637
810
|
return text(saveRecipe(ctx, input, { allow_duplicate, ingest }));
|
|
@@ -639,22 +812,22 @@ var pb_save_recipe_default = defineTool({
|
|
|
639
812
|
});
|
|
640
813
|
|
|
641
814
|
// src/mcp/tools/pb_search.ts
|
|
642
|
-
import { z as
|
|
815
|
+
import { z as z19 } from "zod";
|
|
643
816
|
var pb_search_default = defineTool({
|
|
644
817
|
name: "pb_search",
|
|
645
818
|
description: "Search recipes: hybrid keyword + semantic ranking (RRF) over title, blurb, ingredients and method, with filters. Omit `query` to just filter/browse. Tag filters accept any category \u2014 assigned tags (cuisine, diet, dish, protein, flavor\u2026) and derived facets (allergen, equipment, technique, chef, provider, plan); parents expand (cuisine:asian matches thai). Returns compact summaries; use pb_get_recipe for details.",
|
|
646
819
|
schema: {
|
|
647
|
-
query:
|
|
648
|
-
limit:
|
|
649
|
-
tags:
|
|
650
|
-
exclude_allergens:
|
|
651
|
-
exclude_ingredients:
|
|
652
|
-
max_total_min:
|
|
653
|
-
max_active_min:
|
|
654
|
-
provider:
|
|
655
|
-
source_kind:
|
|
656
|
-
needs_review:
|
|
657
|
-
mode:
|
|
820
|
+
query: z19.string().optional().describe("Free text: dish, ingredient, technique, mood, occasion\u2026"),
|
|
821
|
+
limit: z19.number().int().min(1).max(100).optional().describe("default 10"),
|
|
822
|
+
tags: z19.partialRecord(z19.enum(ALL_CATEGORIES), z19.array(z19.string())).optional().describe("e.g. {cuisine:['thai'], diet:['vegetarian']} \u2014 OR within a category, AND across"),
|
|
823
|
+
exclude_allergens: z19.array(z19.string()).optional().describe('e.g. ["dairy", "tree-nuts"]'),
|
|
824
|
+
exclude_ingredients: z19.array(z19.string()).optional(),
|
|
825
|
+
max_total_min: z19.number().optional(),
|
|
826
|
+
max_active_min: z19.number().optional().describe("Hands-on minutes"),
|
|
827
|
+
provider: z19.string().optional(),
|
|
828
|
+
source_kind: z19.enum(SOURCE_KINDS).optional(),
|
|
829
|
+
needs_review: z19.boolean().optional().describe("true = only recipes flagged for review"),
|
|
830
|
+
mode: z19.enum(["hybrid", "lexical", "semantic"]).optional()
|
|
658
831
|
},
|
|
659
832
|
handler: async ({ query, limit, mode, ...filters }, ctx) => {
|
|
660
833
|
const res = await searchRecipes(ctx, { query, limit, mode, filters });
|
|
@@ -695,7 +868,7 @@ var pb_stats_default = defineTool({
|
|
|
695
868
|
});
|
|
696
869
|
|
|
697
870
|
// src/mcp/tools/pb_synthesis_context.ts
|
|
698
|
-
import { z as
|
|
871
|
+
import { z as z20 } from "zod";
|
|
699
872
|
|
|
700
873
|
// src/synthesis/context.ts
|
|
701
874
|
var STEP_CHARS = 220;
|
|
@@ -829,14 +1002,14 @@ var pb_synthesis_context_default = defineTool({
|
|
|
829
1002
|
name: "pb_synthesis_context",
|
|
830
1003
|
description: "Material for inventing a new recipe from the collection: a few relevant source recipes in compact form, the ingredients, pairings, techniques, cuisines and flavours they share, and (with use_pantry) what the pantry covers and what is commonly missing. Pick sources by `query`, explicit `slugs`, pantry fit, or tag filters. Then write the recipe and save it with pb_save_recipe (source.kind 'synthesized', derived_from the slugs you drew on).",
|
|
831
1004
|
schema: {
|
|
832
|
-
query:
|
|
833
|
-
slugs:
|
|
834
|
-
use_pantry:
|
|
835
|
-
ingredients:
|
|
836
|
-
tags:
|
|
837
|
-
exclude_allergens:
|
|
838
|
-
max_total_min:
|
|
839
|
-
n:
|
|
1005
|
+
query: z20.string().optional().describe('What to make, e.g. "quick spicy noodle dinner"'),
|
|
1006
|
+
slugs: z20.array(z20.string()).optional(),
|
|
1007
|
+
use_pantry: z20.boolean().optional(),
|
|
1008
|
+
ingredients: z20.array(z20.string()).optional().describe("Extra things available right now"),
|
|
1009
|
+
tags: z20.partialRecord(z20.enum(ALL_CATEGORIES), z20.array(z20.string())).optional(),
|
|
1010
|
+
exclude_allergens: z20.array(z20.string()).optional(),
|
|
1011
|
+
max_total_min: z20.number().optional(),
|
|
1012
|
+
n: z20.number().int().min(1).max(12).optional().describe("Number of source recipes (default 5)")
|
|
840
1013
|
},
|
|
841
1014
|
handler: async ({ query, slugs, use_pantry, ingredients, n, ...filters }, ctx) => text(await synthesisContext(ctx, { query, slugs, use_pantry, ingredients, n, filters }))
|
|
842
1015
|
});
|
|
@@ -853,11 +1026,16 @@ var tools = [
|
|
|
853
1026
|
pb_pantry_list,
|
|
854
1027
|
pb_pantry_add,
|
|
855
1028
|
pb_pantry_remove,
|
|
1029
|
+
// meal planning
|
|
1030
|
+
pb_plan_suggest_default,
|
|
1031
|
+
pb_plan_default,
|
|
1032
|
+
pb_plan_shopping_list_default,
|
|
856
1033
|
// capture
|
|
857
1034
|
pb_providers_default,
|
|
858
1035
|
pb_ingest_source_default,
|
|
859
1036
|
pb_inbox_next_default,
|
|
860
1037
|
pb_inbox_image_default,
|
|
1038
|
+
pb_inbox_rotate_default,
|
|
861
1039
|
pb_inbox_resolve_default,
|
|
862
1040
|
pb_inbox_list_default,
|
|
863
1041
|
pb_save_recipe_default,
|