pantrybase 0.1.0 → 0.2.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/dist/{chunk-2EMC6TJB.js → chunk-QUWSBZ2V.js} +94 -6
- package/dist/cli.js +1 -1
- package/dist/server.js +79 -61
- package/dist/web/assets/{graph-Mpnwdw0-.js → graph-qSs25SSD.js} +1 -1
- package/dist/web/assets/{index-PLKhwbDz.css → index-Dsc3USAJ.css} +1 -1
- package/dist/web/assets/index-lHc1E7AV.js +1 -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/ingest/SKILL.md +6 -5
- package/dist/web/assets/index-BQVf10Ok.js +0 -1
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantrybase",
|
|
3
3
|
"displayName": "Pantrybase",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Local-first recipe knowledge base for Claude Code: capture meal-box recipe cards from photos, search by ingredient, tag, allergen or mood, match recipes to your pantry, and synthesize new recipes. SQLite FTS5 + vector search, markdown recipes as the source of truth.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Ryan Dick"
|
|
8
8
|
},
|
|
9
9
|
"license": "MIT",
|
|
10
|
-
"keywords": [
|
|
10
|
+
"keywords": [
|
|
11
|
+
"recipes",
|
|
12
|
+
"pantry",
|
|
13
|
+
"meal-kit",
|
|
14
|
+
"knowledge-graph",
|
|
15
|
+
"search"
|
|
16
|
+
],
|
|
11
17
|
"mcpServers": {
|
|
12
18
|
"pantrybase": {
|
|
13
19
|
"command": "${CLAUDE_PLUGIN_ROOT}/bin/pb-server",
|
|
@@ -1056,6 +1056,7 @@ function loadProfile(dir) {
|
|
|
1056
1056
|
recognise: raw.recognise ?? [],
|
|
1057
1057
|
sides: { front: raw.sides?.front ?? [], back: raw.sides?.back ?? [] },
|
|
1058
1058
|
pairing: raw.pairing ?? "content",
|
|
1059
|
+
orientation: raw.orientation ?? null,
|
|
1059
1060
|
nutrition_basis: raw.nutrition_basis ?? "per_serving",
|
|
1060
1061
|
default_servings: raw.default_servings ?? null,
|
|
1061
1062
|
kitchen_section: raw.kitchen_section ?? null,
|
|
@@ -2421,6 +2422,25 @@ async function openImage(file) {
|
|
|
2421
2422
|
const input = HEIF_EXTS.has(path6.extname(file).toLowerCase()) ? await heifToJpeg(file) : file;
|
|
2422
2423
|
return sharp(input, { failOn: "none" }).rotate();
|
|
2423
2424
|
}
|
|
2425
|
+
async function rotateFile(file, degrees) {
|
|
2426
|
+
const ext = path6.extname(file).toLowerCase();
|
|
2427
|
+
const upright = await (await openImage(file)).toBuffer();
|
|
2428
|
+
let pipeline = sharp(upright, { failOn: "none" }).rotate(degrees);
|
|
2429
|
+
let target = file;
|
|
2430
|
+
if (ext === ".png") pipeline = pipeline.png();
|
|
2431
|
+
else if (ext === ".webp") pipeline = pipeline.webp({ quality: 92 });
|
|
2432
|
+
else if (ext === ".tif" || ext === ".tiff") pipeline = pipeline.tiff();
|
|
2433
|
+
else {
|
|
2434
|
+
pipeline = pipeline.jpeg({ quality: 92, mozjpeg: true });
|
|
2435
|
+
if (HEIF_EXTS.has(ext)) target = `${file.slice(0, -ext.length)}.jpg`;
|
|
2436
|
+
}
|
|
2437
|
+
const out = await pipeline.toBuffer();
|
|
2438
|
+
const tmp = `${target}.rotating-${process.pid}`;
|
|
2439
|
+
fs9.writeFileSync(tmp, out);
|
|
2440
|
+
fs9.renameSync(tmp, target);
|
|
2441
|
+
if (target !== file) fs9.rmSync(file, { force: true });
|
|
2442
|
+
return target;
|
|
2443
|
+
}
|
|
2424
2444
|
async function imageForModel(file, maxPx = MODEL_MAX_PX) {
|
|
2425
2445
|
const base = await (await openImage(file)).resize(maxPx, maxPx, { fit: "inside", withoutEnlargement: true }).toBuffer({ resolveWithObject: true });
|
|
2426
2446
|
let data = base.data;
|
|
@@ -2686,6 +2706,29 @@ async function addUpload(ctx, name, data) {
|
|
|
2686
2706
|
await scanInbox(ctx);
|
|
2687
2707
|
return ctx.store.ingestByPath(relativeToData(ctx.paths, target));
|
|
2688
2708
|
}
|
|
2709
|
+
function shapeOf(item) {
|
|
2710
|
+
const w = item.width ?? 0;
|
|
2711
|
+
const h = item.height ?? 0;
|
|
2712
|
+
if (w > h * 1.1) return "landscape";
|
|
2713
|
+
if (h > w * 1.1) return "portrait";
|
|
2714
|
+
return "square";
|
|
2715
|
+
}
|
|
2716
|
+
function dominantShape(all) {
|
|
2717
|
+
const counts = { landscape: 0, portrait: 0, square: 0 };
|
|
2718
|
+
for (const i of all) if (i.width && i.height) counts[shapeOf(i)]++;
|
|
2719
|
+
const total = counts.landscape + counts.portrait + counts.square;
|
|
2720
|
+
if (total < 3) return null;
|
|
2721
|
+
const [shape, n] = Object.entries(counts).sort(
|
|
2722
|
+
(a, b) => b[1] - a[1]
|
|
2723
|
+
)[0] ?? ["square", 0];
|
|
2724
|
+
return shape !== "square" && n / total >= 0.7 ? shape : null;
|
|
2725
|
+
}
|
|
2726
|
+
function rotationHint(item, dominant) {
|
|
2727
|
+
if (!dominant) return void 0;
|
|
2728
|
+
const shape = shapeOf(item);
|
|
2729
|
+
if (shape === "square" || shape === dominant) return void 0;
|
|
2730
|
+
return `${shape} while most photos are ${dominant} \u2014 it may be sideways; if the text doesn't read upright, pb_inbox_rotate it`;
|
|
2731
|
+
}
|
|
2689
2732
|
function retakeHint(item, all) {
|
|
2690
2733
|
let best;
|
|
2691
2734
|
for (const other of all) {
|
|
@@ -2698,26 +2741,63 @@ function retakeHint(item, all) {
|
|
|
2698
2741
|
}
|
|
2699
2742
|
return best;
|
|
2700
2743
|
}
|
|
2701
|
-
function viewItem(item, all) {
|
|
2744
|
+
function viewItem(item, all, dominant = all ? dominantShape(all) : null) {
|
|
2702
2745
|
const v = {
|
|
2703
2746
|
id: item.id,
|
|
2704
2747
|
name: item.original_name,
|
|
2705
2748
|
captured_at: item.captured_at,
|
|
2706
2749
|
size: `${item.width}x${item.height}`,
|
|
2707
|
-
status: item.status
|
|
2750
|
+
status: item.status,
|
|
2751
|
+
version: item.mtime
|
|
2708
2752
|
};
|
|
2709
2753
|
if (item.recipe_slug) v.recipe_slug = item.recipe_slug;
|
|
2710
2754
|
if (item.reason) v.reason = item.reason;
|
|
2711
2755
|
if (all) {
|
|
2712
2756
|
const r = retakeHint(item, all);
|
|
2713
2757
|
if (r) v.retake_of = r;
|
|
2758
|
+
const hint = rotationHint(item, dominant);
|
|
2759
|
+
if (hint) v.rotation_hint = hint;
|
|
2714
2760
|
}
|
|
2715
2761
|
return v;
|
|
2716
2762
|
}
|
|
2763
|
+
function viewItems(items, all) {
|
|
2764
|
+
const dominant = dominantShape(all);
|
|
2765
|
+
return items.map((i) => viewItem(i, all, dominant));
|
|
2766
|
+
}
|
|
2717
2767
|
function nextBatch(ctx, window = 4) {
|
|
2718
2768
|
const all = ctx.store.allIngest();
|
|
2719
2769
|
const items = all.filter((i) => i.status === "pending").slice(0, Math.max(1, window));
|
|
2720
|
-
return { progress: ctx.store.ingestCounts(), items: items
|
|
2770
|
+
return { progress: ctx.store.ingestCounts(), items: viewItems(items, all) };
|
|
2771
|
+
}
|
|
2772
|
+
async function rotateItem(ctx, id, degrees) {
|
|
2773
|
+
const [item] = ctx.store.ingestByIds([id]);
|
|
2774
|
+
if (!item) throw new Error(`no photo #${id}`);
|
|
2775
|
+
const abs = path7.join(ctx.paths.data, item.path);
|
|
2776
|
+
if (!fs10.existsSync(abs)) throw new Error(`photo #${id} is missing on disk (${item.path})`);
|
|
2777
|
+
const heif = /\.(heic|heif)$/i.test(abs);
|
|
2778
|
+
if (item.status === "done" && heif) {
|
|
2779
|
+
throw new Error(
|
|
2780
|
+
`photo #${id} is an archived HEIC; rotating would rename the file ${item.recipe_slug} uses`
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
const target = await rotateFile(abs, degrees);
|
|
2784
|
+
const info = await inspectImage(target);
|
|
2785
|
+
const rel = relativeToData(ctx.paths, target);
|
|
2786
|
+
const clash = ctx.store.ingestBySha(info.sha256);
|
|
2787
|
+
if (clash && clash.id !== item.id)
|
|
2788
|
+
throw new Error(`rotated photo #${id} is identical to #${clash.id}`);
|
|
2789
|
+
ctx.store.updateIngest(item.id, {
|
|
2790
|
+
path: rel,
|
|
2791
|
+
sha256: info.sha256,
|
|
2792
|
+
phash: info.phash,
|
|
2793
|
+
width: info.width,
|
|
2794
|
+
height: info.height,
|
|
2795
|
+
bytes: info.bytes,
|
|
2796
|
+
mtime: info.mtime,
|
|
2797
|
+
mime: info.mime
|
|
2798
|
+
});
|
|
2799
|
+
const updated = ctx.store.ingestByIds([id])[0] ?? item;
|
|
2800
|
+
return viewItem(updated, ctx.store.allIngest());
|
|
2721
2801
|
}
|
|
2722
2802
|
function resolveItems(ctx, ids, status, reason) {
|
|
2723
2803
|
const out = { moved: 0, refused: [] };
|
|
@@ -3159,7 +3239,7 @@ function routes(ctx, version) {
|
|
|
3159
3239
|
});
|
|
3160
3240
|
return {
|
|
3161
3241
|
counts: ctx.store.ingestCounts(),
|
|
3162
|
-
items: items.map((
|
|
3242
|
+
items: viewItems(items, all).map((v, n) => ({ ...v, path: items[n]?.path ?? "" }))
|
|
3163
3243
|
};
|
|
3164
3244
|
}),
|
|
3165
3245
|
r("POST", /^\/api\/inbox\/upload$/, async (req) => {
|
|
@@ -3170,6 +3250,13 @@ function routes(ctx, version) {
|
|
|
3170
3250
|
const item = await addUpload(ctx, name, data);
|
|
3171
3251
|
return { item: item ? viewItem(item) : null, counts: ctx.store.ingestCounts() };
|
|
3172
3252
|
}),
|
|
3253
|
+
r("POST", /^\/api\/inbox\/rotate$/, async (req) => {
|
|
3254
|
+
const body = await readJson(req);
|
|
3255
|
+
if (typeof body.id !== "number" || ![90, 180, 270].includes(body.degrees ?? 0)) {
|
|
3256
|
+
throw new HttpError(400, "id and degrees (90|180|270, clockwise) required");
|
|
3257
|
+
}
|
|
3258
|
+
return { item: await rotateItem(ctx, body.id, body.degrees) };
|
|
3259
|
+
}),
|
|
3173
3260
|
r("POST", /^\/api\/inbox\/resolve$/, async (req) => {
|
|
3174
3261
|
const body = await readJson(req);
|
|
3175
3262
|
if (!body.ids?.length || !["pending", "skipped", "failed"].includes(body.status ?? "")) {
|
|
@@ -3323,8 +3410,9 @@ export {
|
|
|
3323
3410
|
imageForModel,
|
|
3324
3411
|
maybeScanInbox,
|
|
3325
3412
|
importPaths,
|
|
3326
|
-
|
|
3413
|
+
viewItems,
|
|
3327
3414
|
nextBatch,
|
|
3415
|
+
rotateItem,
|
|
3328
3416
|
resolveItems,
|
|
3329
3417
|
planAttach,
|
|
3330
3418
|
finalizeAttach,
|
|
@@ -3338,4 +3426,4 @@ export {
|
|
|
3338
3426
|
matchRecipes,
|
|
3339
3427
|
startUiServer
|
|
3340
3428
|
};
|
|
3341
|
-
//# sourceMappingURL=chunk-
|
|
3429
|
+
//# sourceMappingURL=chunk-QUWSBZ2V.js.map
|
package/dist/cli.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -28,12 +28,13 @@ import {
|
|
|
28
28
|
removeFromPantry,
|
|
29
29
|
resolveItem,
|
|
30
30
|
resolveItems,
|
|
31
|
+
rotateItem,
|
|
31
32
|
scoreRecipes,
|
|
32
33
|
searchRecipes,
|
|
33
34
|
startUiServer,
|
|
34
|
-
|
|
35
|
+
viewItems,
|
|
35
36
|
writeRecipeFile
|
|
36
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-QUWSBZ2V.js";
|
|
37
38
|
|
|
38
39
|
// src/server.ts
|
|
39
40
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -136,7 +137,7 @@ import path from "path";
|
|
|
136
137
|
import { z as z3 } from "zod";
|
|
137
138
|
var pb_inbox_image_default = defineTool({
|
|
138
139
|
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.",
|
|
140
|
+
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
141
|
schema: {
|
|
141
142
|
id: z3.number().int(),
|
|
142
143
|
max_px: z3.number().int().min(400).max(2400).optional()
|
|
@@ -175,7 +176,10 @@ var pb_inbox_list_default = defineTool({
|
|
|
175
176
|
},
|
|
176
177
|
handler: ({ status, limit, offset }, ctx) => {
|
|
177
178
|
const items = ctx.store.listIngest({ status, limit: limit ?? 20, offset });
|
|
178
|
-
return text({
|
|
179
|
+
return text({
|
|
180
|
+
counts: ctx.store.ingestCounts(),
|
|
181
|
+
items: viewItems(items, ctx.store.allIngest())
|
|
182
|
+
});
|
|
179
183
|
}
|
|
180
184
|
});
|
|
181
185
|
|
|
@@ -183,7 +187,7 @@ var pb_inbox_list_default = defineTool({
|
|
|
183
187
|
import { z as z5 } from "zod";
|
|
184
188
|
var pb_inbox_next_default = defineTool({
|
|
185
189
|
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.",
|
|
190
|
+
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
191
|
schema: {
|
|
188
192
|
window: z5.number().int().min(1).max(12).optional().describe("How many photos to return (default 4)")
|
|
189
193
|
},
|
|
@@ -210,14 +214,26 @@ var pb_inbox_resolve_default = defineTool({
|
|
|
210
214
|
}
|
|
211
215
|
});
|
|
212
216
|
|
|
213
|
-
// src/mcp/tools/
|
|
217
|
+
// src/mcp/tools/pb_inbox_rotate.ts
|
|
214
218
|
import { z as z7 } from "zod";
|
|
219
|
+
var pb_inbox_rotate_default = defineTool({
|
|
220
|
+
name: "pb_inbox_rotate",
|
|
221
|
+
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.",
|
|
222
|
+
schema: {
|
|
223
|
+
id: z7.number().int(),
|
|
224
|
+
degrees: z7.union([z7.literal(90), z7.literal(180), z7.literal(270)])
|
|
225
|
+
},
|
|
226
|
+
handler: async ({ id, degrees }, ctx) => text({ rotated: degrees, item: await rotateItem(ctx, id, degrees) })
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// src/mcp/tools/pb_ingest_source.ts
|
|
230
|
+
import { z as z8 } from "zod";
|
|
215
231
|
var pb_ingest_source_default = defineTool({
|
|
216
232
|
name: "pb_ingest_source",
|
|
217
233
|
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
234
|
schema: {
|
|
219
|
-
paths:
|
|
220
|
-
recursive:
|
|
235
|
+
paths: z8.array(z8.string()).min(1).describe("Files or folders; relative to the project, absolute, or ~/\u2026"),
|
|
236
|
+
recursive: z8.boolean().optional()
|
|
221
237
|
},
|
|
222
238
|
handler: async ({ paths, recursive }, ctx) => {
|
|
223
239
|
const r = await importPaths(ctx, paths, { recursive });
|
|
@@ -234,13 +250,13 @@ var pb_ingest_source_default = defineTool({
|
|
|
234
250
|
});
|
|
235
251
|
|
|
236
252
|
// src/mcp/tools/pb_list_tags.ts
|
|
237
|
-
import { z as
|
|
253
|
+
import { z as z9 } from "zod";
|
|
238
254
|
var pb_list_tags_default = defineTool({
|
|
239
255
|
name: "pb_list_tags",
|
|
240
256
|
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
257
|
schema: {
|
|
242
|
-
category:
|
|
243
|
-
vocabulary:
|
|
258
|
+
category: z9.enum(ALL_CATEGORIES).optional(),
|
|
259
|
+
vocabulary: z9.boolean().optional()
|
|
244
260
|
},
|
|
245
261
|
handler: ({ category, vocabulary }, ctx) => {
|
|
246
262
|
const used = {};
|
|
@@ -259,7 +275,7 @@ var pb_list_tags_default = defineTool({
|
|
|
259
275
|
});
|
|
260
276
|
|
|
261
277
|
// src/mcp/tools/pb_merge_ingredients.ts
|
|
262
|
-
import { z as
|
|
278
|
+
import { z as z10 } from "zod";
|
|
263
279
|
|
|
264
280
|
// src/ingredients/merge.ts
|
|
265
281
|
import fs from "fs";
|
|
@@ -363,8 +379,8 @@ var pb_merge_ingredients_default = defineTool({
|
|
|
363
379
|
name: "pb_merge_ingredients",
|
|
364
380
|
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
381
|
schema: {
|
|
366
|
-
from:
|
|
367
|
-
into:
|
|
382
|
+
from: z10.string().min(1),
|
|
383
|
+
into: z10.string().min(1)
|
|
368
384
|
},
|
|
369
385
|
handler: ({ from, into }, ctx) => text(mergeIngredients(ctx, from, into))
|
|
370
386
|
});
|
|
@@ -381,7 +397,7 @@ var pb_open_ui_default = defineTool({
|
|
|
381
397
|
});
|
|
382
398
|
|
|
383
399
|
// src/mcp/tools/pb_pantry.ts
|
|
384
|
-
import { z as
|
|
400
|
+
import { z as z11 } from "zod";
|
|
385
401
|
var pb_pantry_list = defineTool({
|
|
386
402
|
name: "pb_pantry_list",
|
|
387
403
|
description: "What's in the pantry: canonical ingredient names with optional quantity, unit and note.",
|
|
@@ -403,12 +419,12 @@ var pb_pantry_add = defineTool({
|
|
|
403
419
|
name: "pb_pantry_add",
|
|
404
420
|
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
421
|
schema: {
|
|
406
|
-
items:
|
|
407
|
-
|
|
408
|
-
name:
|
|
409
|
-
qty:
|
|
410
|
-
unit:
|
|
411
|
-
note:
|
|
422
|
+
items: z11.array(
|
|
423
|
+
z11.object({
|
|
424
|
+
name: z11.string().min(1),
|
|
425
|
+
qty: z11.number().nullable().optional(),
|
|
426
|
+
unit: z11.string().nullable().optional(),
|
|
427
|
+
note: z11.string().nullable().optional()
|
|
412
428
|
})
|
|
413
429
|
).min(1)
|
|
414
430
|
},
|
|
@@ -418,8 +434,8 @@ var pb_pantry_remove = defineTool({
|
|
|
418
434
|
name: "pb_pantry_remove",
|
|
419
435
|
description: "Remove items from the pantry by name, or everything with all:true.",
|
|
420
436
|
schema: {
|
|
421
|
-
names:
|
|
422
|
-
all:
|
|
437
|
+
names: z11.array(z11.string()).optional(),
|
|
438
|
+
all: z11.boolean().optional()
|
|
423
439
|
},
|
|
424
440
|
handler: ({ names, all }, ctx) => {
|
|
425
441
|
if (all) return text({ cleared: ctx.store.pantryClear() });
|
|
@@ -428,19 +444,19 @@ var pb_pantry_remove = defineTool({
|
|
|
428
444
|
});
|
|
429
445
|
|
|
430
446
|
// src/mcp/tools/pb_pantry_match.ts
|
|
431
|
-
import { z as
|
|
447
|
+
import { z as z12 } from "zod";
|
|
432
448
|
var pb_pantry_match_default = defineTool({
|
|
433
449
|
name: "pb_pantry_match",
|
|
434
450
|
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
451
|
schema: {
|
|
436
|
-
ingredients:
|
|
437
|
-
use_pantry:
|
|
438
|
-
min_coverage:
|
|
439
|
-
max_missing:
|
|
440
|
-
limit:
|
|
441
|
-
tags:
|
|
442
|
-
exclude_allergens:
|
|
443
|
-
max_total_min:
|
|
452
|
+
ingredients: z12.array(z12.string()).optional(),
|
|
453
|
+
use_pantry: z12.boolean().optional().describe("default true"),
|
|
454
|
+
min_coverage: z12.number().min(0).max(1).optional().describe("default 0.5"),
|
|
455
|
+
max_missing: z12.number().int().min(0).optional(),
|
|
456
|
+
limit: z12.number().int().min(1).max(50).optional().describe("default 10"),
|
|
457
|
+
tags: z12.partialRecord(z12.enum(ALL_CATEGORIES), z12.array(z12.string())).optional(),
|
|
458
|
+
exclude_allergens: z12.array(z12.string()).optional(),
|
|
459
|
+
max_total_min: z12.number().optional()
|
|
444
460
|
},
|
|
445
461
|
handler: ({ ingredients, use_pantry, min_coverage, max_missing, limit, ...filters }, ctx) => {
|
|
446
462
|
const r = matchRecipes(ctx, {
|
|
@@ -467,12 +483,12 @@ var pb_pantry_match_default = defineTool({
|
|
|
467
483
|
});
|
|
468
484
|
|
|
469
485
|
// src/mcp/tools/pb_providers.ts
|
|
470
|
-
import { z as
|
|
486
|
+
import { z as z13 } from "zod";
|
|
471
487
|
var pb_providers_default = defineTool({
|
|
472
488
|
name: "pb_providers",
|
|
473
489
|
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
490
|
schema: {
|
|
475
|
-
id:
|
|
491
|
+
id: z13.string().optional().describe('e.g. "ucook"; "generic" for anything else')
|
|
476
492
|
},
|
|
477
493
|
handler: ({ id }, ctx) => {
|
|
478
494
|
if (!id) {
|
|
@@ -500,6 +516,7 @@ var pb_providers_default = defineTool({
|
|
|
500
516
|
`- Brands stripped from canonical names: ${p.brands.join(", ") || "none"}`,
|
|
501
517
|
`- Kitchen items treated as equipment: ${Object.keys(p.equipment_terms).join(", ") || "none"}`,
|
|
502
518
|
`- Nutrition basis default: ${p.nutrition_basis}; default servings: ${p.default_servings ?? "none"}`,
|
|
519
|
+
`- Upright card shape: ${p.orientation ?? "varies"} (photos that don't match are flagged with rotation_hint; fix them with pb_inbox_rotate)`,
|
|
503
520
|
"",
|
|
504
521
|
"## Tag vocabulary (closed = only these values; open = top values, new ones allowed)",
|
|
505
522
|
...Object.entries(summary).map(
|
|
@@ -511,13 +528,13 @@ var pb_providers_default = defineTool({
|
|
|
511
528
|
});
|
|
512
529
|
|
|
513
530
|
// src/mcp/tools/pb_rebuild.ts
|
|
514
|
-
import { z as
|
|
531
|
+
import { z as z14 } from "zod";
|
|
515
532
|
var pb_rebuild_default = defineTool({
|
|
516
533
|
name: "pb_rebuild",
|
|
517
534
|
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
535
|
schema: {
|
|
519
|
-
full:
|
|
520
|
-
reembed:
|
|
536
|
+
full: z14.boolean().optional(),
|
|
537
|
+
reembed: z14.boolean().optional()
|
|
521
538
|
},
|
|
522
539
|
handler: ({ full, reembed }, ctx) => {
|
|
523
540
|
const reloaded = ctx.refresh(true);
|
|
@@ -527,7 +544,7 @@ var pb_rebuild_default = defineTool({
|
|
|
527
544
|
});
|
|
528
545
|
|
|
529
546
|
// src/mcp/tools/pb_save_recipe.ts
|
|
530
|
-
import { z as
|
|
547
|
+
import { z as z15 } from "zod";
|
|
531
548
|
|
|
532
549
|
// src/files/slug.ts
|
|
533
550
|
var MAX_SLUG = 60;
|
|
@@ -628,10 +645,10 @@ var pb_save_recipe_default = defineTool({
|
|
|
628
645
|
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
646
|
schema: {
|
|
630
647
|
...recipeInputSchema.shape,
|
|
631
|
-
ingest:
|
|
648
|
+
ingest: z15.array(z15.object({ id: z15.number().int(), side: z15.enum(IMAGE_SIDES) })).optional().describe(
|
|
632
649
|
"Inbox photos of this card, e.g. [{id: 12, side: 'front'}, {id: 13, side: 'back'}]"
|
|
633
650
|
),
|
|
634
|
-
allow_duplicate:
|
|
651
|
+
allow_duplicate: z15.boolean().optional()
|
|
635
652
|
},
|
|
636
653
|
handler: ({ allow_duplicate, ingest, ...input }, ctx) => {
|
|
637
654
|
return text(saveRecipe(ctx, input, { allow_duplicate, ingest }));
|
|
@@ -639,22 +656,22 @@ var pb_save_recipe_default = defineTool({
|
|
|
639
656
|
});
|
|
640
657
|
|
|
641
658
|
// src/mcp/tools/pb_search.ts
|
|
642
|
-
import { z as
|
|
659
|
+
import { z as z16 } from "zod";
|
|
643
660
|
var pb_search_default = defineTool({
|
|
644
661
|
name: "pb_search",
|
|
645
662
|
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
663
|
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:
|
|
664
|
+
query: z16.string().optional().describe("Free text: dish, ingredient, technique, mood, occasion\u2026"),
|
|
665
|
+
limit: z16.number().int().min(1).max(100).optional().describe("default 10"),
|
|
666
|
+
tags: z16.partialRecord(z16.enum(ALL_CATEGORIES), z16.array(z16.string())).optional().describe("e.g. {cuisine:['thai'], diet:['vegetarian']} \u2014 OR within a category, AND across"),
|
|
667
|
+
exclude_allergens: z16.array(z16.string()).optional().describe('e.g. ["dairy", "tree-nuts"]'),
|
|
668
|
+
exclude_ingredients: z16.array(z16.string()).optional(),
|
|
669
|
+
max_total_min: z16.number().optional(),
|
|
670
|
+
max_active_min: z16.number().optional().describe("Hands-on minutes"),
|
|
671
|
+
provider: z16.string().optional(),
|
|
672
|
+
source_kind: z16.enum(SOURCE_KINDS).optional(),
|
|
673
|
+
needs_review: z16.boolean().optional().describe("true = only recipes flagged for review"),
|
|
674
|
+
mode: z16.enum(["hybrid", "lexical", "semantic"]).optional()
|
|
658
675
|
},
|
|
659
676
|
handler: async ({ query, limit, mode, ...filters }, ctx) => {
|
|
660
677
|
const res = await searchRecipes(ctx, { query, limit, mode, filters });
|
|
@@ -695,7 +712,7 @@ var pb_stats_default = defineTool({
|
|
|
695
712
|
});
|
|
696
713
|
|
|
697
714
|
// src/mcp/tools/pb_synthesis_context.ts
|
|
698
|
-
import { z as
|
|
715
|
+
import { z as z17 } from "zod";
|
|
699
716
|
|
|
700
717
|
// src/synthesis/context.ts
|
|
701
718
|
var STEP_CHARS = 220;
|
|
@@ -829,14 +846,14 @@ var pb_synthesis_context_default = defineTool({
|
|
|
829
846
|
name: "pb_synthesis_context",
|
|
830
847
|
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
848
|
schema: {
|
|
832
|
-
query:
|
|
833
|
-
slugs:
|
|
834
|
-
use_pantry:
|
|
835
|
-
ingredients:
|
|
836
|
-
tags:
|
|
837
|
-
exclude_allergens:
|
|
838
|
-
max_total_min:
|
|
839
|
-
n:
|
|
849
|
+
query: z17.string().optional().describe('What to make, e.g. "quick spicy noodle dinner"'),
|
|
850
|
+
slugs: z17.array(z17.string()).optional(),
|
|
851
|
+
use_pantry: z17.boolean().optional(),
|
|
852
|
+
ingredients: z17.array(z17.string()).optional().describe("Extra things available right now"),
|
|
853
|
+
tags: z17.partialRecord(z17.enum(ALL_CATEGORIES), z17.array(z17.string())).optional(),
|
|
854
|
+
exclude_allergens: z17.array(z17.string()).optional(),
|
|
855
|
+
max_total_min: z17.number().optional(),
|
|
856
|
+
n: z17.number().int().min(1).max(12).optional().describe("Number of source recipes (default 5)")
|
|
840
857
|
},
|
|
841
858
|
handler: async ({ query, slugs, use_pantry, ingredients, n, ...filters }, ctx) => text(await synthesisContext(ctx, { query, slugs, use_pantry, ingredients, n, filters }))
|
|
842
859
|
});
|
|
@@ -858,6 +875,7 @@ var tools = [
|
|
|
858
875
|
pb_ingest_source_default,
|
|
859
876
|
pb_inbox_next_default,
|
|
860
877
|
pb_inbox_image_default,
|
|
878
|
+
pb_inbox_rotate_default,
|
|
861
879
|
pb_inbox_resolve_default,
|
|
862
880
|
pb_inbox_list_default,
|
|
863
881
|
pb_save_recipe_default,
|