pantrybase 0.2.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 +1 -1
- package/README.md +4 -0
- package/dist/{chunk-QUWSBZ2V.js → chunk-IUQQ3I2E.js} +512 -3
- package/dist/cli.js +1 -1
- package/dist/server.js +190 -30
- package/dist/web/assets/{graph-qSs25SSD.js → graph-70CI6xc-.js} +1 -1
- package/dist/web/assets/{index-Dsc3USAJ.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/skills/cook/SKILL.md +9 -1
- package/dist/web/assets/index-lHc1E7AV.js +0 -1
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,15 +28,19 @@ import {
|
|
|
26
28
|
recipeInputSchema,
|
|
27
29
|
reconcile,
|
|
28
30
|
removeFromPantry,
|
|
31
|
+
renderShoppingList,
|
|
29
32
|
resolveItem,
|
|
30
33
|
resolveItems,
|
|
31
34
|
rotateItem,
|
|
35
|
+
savePlan,
|
|
32
36
|
scoreRecipes,
|
|
33
37
|
searchRecipes,
|
|
38
|
+
shoppingList,
|
|
34
39
|
startUiServer,
|
|
40
|
+
suggestRecipes,
|
|
35
41
|
viewItems,
|
|
36
42
|
writeRecipeFile
|
|
37
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-IUQQ3I2E.js";
|
|
38
44
|
|
|
39
45
|
// src/server.ts
|
|
40
46
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -482,13 +488,163 @@ var pb_pantry_match_default = defineTool({
|
|
|
482
488
|
}
|
|
483
489
|
});
|
|
484
490
|
|
|
485
|
-
// src/mcp/tools/
|
|
491
|
+
// src/mcp/tools/pb_plan.ts
|
|
486
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
|
+
|
|
641
|
+
// src/mcp/tools/pb_providers.ts
|
|
642
|
+
import { z as z16 } from "zod";
|
|
487
643
|
var pb_providers_default = defineTool({
|
|
488
644
|
name: "pb_providers",
|
|
489
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.",
|
|
490
646
|
schema: {
|
|
491
|
-
id:
|
|
647
|
+
id: z16.string().optional().describe('e.g. "ucook"; "generic" for anything else')
|
|
492
648
|
},
|
|
493
649
|
handler: ({ id }, ctx) => {
|
|
494
650
|
if (!id) {
|
|
@@ -528,13 +684,13 @@ var pb_providers_default = defineTool({
|
|
|
528
684
|
});
|
|
529
685
|
|
|
530
686
|
// src/mcp/tools/pb_rebuild.ts
|
|
531
|
-
import { z as
|
|
687
|
+
import { z as z17 } from "zod";
|
|
532
688
|
var pb_rebuild_default = defineTool({
|
|
533
689
|
name: "pb_rebuild",
|
|
534
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.",
|
|
535
691
|
schema: {
|
|
536
|
-
full:
|
|
537
|
-
reembed:
|
|
692
|
+
full: z17.boolean().optional(),
|
|
693
|
+
reembed: z17.boolean().optional()
|
|
538
694
|
},
|
|
539
695
|
handler: ({ full, reembed }, ctx) => {
|
|
540
696
|
const reloaded = ctx.refresh(true);
|
|
@@ -544,7 +700,7 @@ var pb_rebuild_default = defineTool({
|
|
|
544
700
|
});
|
|
545
701
|
|
|
546
702
|
// src/mcp/tools/pb_save_recipe.ts
|
|
547
|
-
import { z as
|
|
703
|
+
import { z as z18 } from "zod";
|
|
548
704
|
|
|
549
705
|
// src/files/slug.ts
|
|
550
706
|
var MAX_SLUG = 60;
|
|
@@ -645,10 +801,10 @@ var pb_save_recipe_default = defineTool({
|
|
|
645
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.",
|
|
646
802
|
schema: {
|
|
647
803
|
...recipeInputSchema.shape,
|
|
648
|
-
ingest:
|
|
804
|
+
ingest: z18.array(z18.object({ id: z18.number().int(), side: z18.enum(IMAGE_SIDES) })).optional().describe(
|
|
649
805
|
"Inbox photos of this card, e.g. [{id: 12, side: 'front'}, {id: 13, side: 'back'}]"
|
|
650
806
|
),
|
|
651
|
-
allow_duplicate:
|
|
807
|
+
allow_duplicate: z18.boolean().optional()
|
|
652
808
|
},
|
|
653
809
|
handler: ({ allow_duplicate, ingest, ...input }, ctx) => {
|
|
654
810
|
return text(saveRecipe(ctx, input, { allow_duplicate, ingest }));
|
|
@@ -656,22 +812,22 @@ var pb_save_recipe_default = defineTool({
|
|
|
656
812
|
});
|
|
657
813
|
|
|
658
814
|
// src/mcp/tools/pb_search.ts
|
|
659
|
-
import { z as
|
|
815
|
+
import { z as z19 } from "zod";
|
|
660
816
|
var pb_search_default = defineTool({
|
|
661
817
|
name: "pb_search",
|
|
662
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.",
|
|
663
819
|
schema: {
|
|
664
|
-
query:
|
|
665
|
-
limit:
|
|
666
|
-
tags:
|
|
667
|
-
exclude_allergens:
|
|
668
|
-
exclude_ingredients:
|
|
669
|
-
max_total_min:
|
|
670
|
-
max_active_min:
|
|
671
|
-
provider:
|
|
672
|
-
source_kind:
|
|
673
|
-
needs_review:
|
|
674
|
-
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()
|
|
675
831
|
},
|
|
676
832
|
handler: async ({ query, limit, mode, ...filters }, ctx) => {
|
|
677
833
|
const res = await searchRecipes(ctx, { query, limit, mode, filters });
|
|
@@ -712,7 +868,7 @@ var pb_stats_default = defineTool({
|
|
|
712
868
|
});
|
|
713
869
|
|
|
714
870
|
// src/mcp/tools/pb_synthesis_context.ts
|
|
715
|
-
import { z as
|
|
871
|
+
import { z as z20 } from "zod";
|
|
716
872
|
|
|
717
873
|
// src/synthesis/context.ts
|
|
718
874
|
var STEP_CHARS = 220;
|
|
@@ -846,14 +1002,14 @@ var pb_synthesis_context_default = defineTool({
|
|
|
846
1002
|
name: "pb_synthesis_context",
|
|
847
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).",
|
|
848
1004
|
schema: {
|
|
849
|
-
query:
|
|
850
|
-
slugs:
|
|
851
|
-
use_pantry:
|
|
852
|
-
ingredients:
|
|
853
|
-
tags:
|
|
854
|
-
exclude_allergens:
|
|
855
|
-
max_total_min:
|
|
856
|
-
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)")
|
|
857
1013
|
},
|
|
858
1014
|
handler: async ({ query, slugs, use_pantry, ingredients, n, ...filters }, ctx) => text(await synthesisContext(ctx, { query, slugs, use_pantry, ingredients, n, filters }))
|
|
859
1015
|
});
|
|
@@ -870,6 +1026,10 @@ var tools = [
|
|
|
870
1026
|
pb_pantry_list,
|
|
871
1027
|
pb_pantry_add,
|
|
872
1028
|
pb_pantry_remove,
|
|
1029
|
+
// meal planning
|
|
1030
|
+
pb_plan_suggest_default,
|
|
1031
|
+
pb_plan_default,
|
|
1032
|
+
pb_plan_shopping_list_default,
|
|
873
1033
|
// capture
|
|
874
1034
|
pb_providers_default,
|
|
875
1035
|
pb_ingest_source_default,
|