picnic-mcp 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.
Files changed (54) hide show
  1. package/README.md +20 -10
  2. package/dist/cli.js +10 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/errors.js +3 -0
  5. package/dist/errors.js.map +1 -1
  6. package/dist/format/cart.d.ts +18 -4
  7. package/dist/format/cart.js +18 -5
  8. package/dist/format/cart.js.map +1 -1
  9. package/dist/format/deliveries.d.ts +18 -0
  10. package/dist/format/deliveries.js +23 -0
  11. package/dist/format/deliveries.js.map +1 -0
  12. package/dist/format/prices.d.ts +8 -0
  13. package/dist/format/prices.js +30 -12
  14. package/dist/format/prices.js.map +1 -1
  15. package/dist/format/product.d.ts +29 -0
  16. package/dist/format/product.js +49 -0
  17. package/dist/format/product.js.map +1 -0
  18. package/dist/format/recipes.d.ts +40 -0
  19. package/dist/format/recipes.js +37 -0
  20. package/dist/format/recipes.js.map +1 -0
  21. package/dist/format/slots.d.ts +29 -0
  22. package/dist/format/slots.js +40 -0
  23. package/dist/format/slots.js.map +1 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/server.d.ts +6 -3
  28. package/dist/server.js +8 -12
  29. package/dist/server.js.map +1 -1
  30. package/dist/tools/cart.js +25 -11
  31. package/dist/tools/cart.js.map +1 -1
  32. package/dist/tools/deliveries.d.ts +3 -0
  33. package/dist/tools/deliveries.js +40 -0
  34. package/dist/tools/deliveries.js.map +1 -0
  35. package/dist/tools/groups.d.ts +14 -0
  36. package/dist/tools/groups.js +64 -0
  37. package/dist/tools/groups.js.map +1 -0
  38. package/dist/tools/inputs.d.ts +4 -0
  39. package/dist/tools/inputs.js +9 -0
  40. package/dist/tools/inputs.js.map +1 -0
  41. package/dist/tools/product.d.ts +3 -0
  42. package/dist/tools/product.js +15 -0
  43. package/dist/tools/product.js.map +1 -0
  44. package/dist/tools/recipes.d.ts +3 -0
  45. package/dist/tools/recipes.js +84 -0
  46. package/dist/tools/recipes.js.map +1 -0
  47. package/dist/tools/replacements.d.ts +9 -0
  48. package/dist/tools/replacements.js +33 -0
  49. package/dist/tools/replacements.js.map +1 -0
  50. package/dist/tools/slots.d.ts +3 -0
  51. package/dist/tools/slots.js +61 -0
  52. package/dist/tools/slots.js.map +1 -0
  53. package/dist/types.d.ts +8 -0
  54. package/package.json +2 -2
package/README.md CHANGED
@@ -6,14 +6,24 @@ Unofficial: not made or supported by Picnic. Built on [picnic-api](https://githu
6
6
 
7
7
  ## Tools
8
8
 
9
- | Tool | What it does |
10
- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
11
- | `search_products` | Searches products. Returns a `bestMatch` when one product fits (usually one you bought before), or 2 or 3 `choices` to ask you about. |
12
- | `add_to_cart` | Adds a product by its id. Returns the quantity in the cart and the cart total. |
13
- | `remove_from_cart` | Removes a product by its id. |
14
- | `get_cart` | Shows the cart: products, quantities, prices, total and delivery window. |
15
-
16
- Output is compact JSON with only what a model needs. It contains products, prices and your cart, but not your name, address or other account details. Keep in mind that whatever the tools return is sent to the model your MCP client uses.
9
+ The tools come in groups. `cart`, `slots` and `product` are on by default; turn groups on or off with `PICNIC_MCP_TOOLS` (see Setup).
10
+
11
+ | Group | Tool | What it does |
12
+ | ------------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `cart` | `search_products` | Searches products. Returns a `bestMatch` when one product fits (usually one you bought before), or 2 or 3 `choices` to ask you about. |
14
+ | | `add_to_cart` | Adds a product by its id. Returns the quantity in the cart and the cart total, and replacements when the product can't be delivered. |
15
+ | | `remove_from_cart` | Removes a product by its id. |
16
+ | | `get_cart` | Shows the cart: products, quantities, prices, total and delivery window, and replacements for products that can't be delivered. |
17
+ | `slots` | `get_delivery_slots` | Lists the delivery slots, two days at a time or for one date. |
18
+ | | `select_delivery_slot` | Chooses the delivery slot of the cart. |
19
+ | `product` | `get_product` | Shows a product's ingredients, allergens, nutrition, storage info, promotion and price. |
20
+ | `deliveries` | `get_deliveries` | Shows upcoming deliveries: window, number of products, total, and the expected arrival while the driver is on the way. Optionally the products. |
21
+ | `recipes` | `search_recipes` | Searches Picnic's recipes. |
22
+ | | `get_recipe` | Shows a recipe's ingredients, allergens and steps. |
23
+ | | `add_recipe_to_cart` | Adds a recipe's ingredients to the cart for a number of portions. |
24
+ | | `remove_recipe_from_cart` | Removes a recipe and its ingredients from the cart. |
25
+
26
+ Output is compact JSON with only what a model needs. It leaves out your name, address, payment details, order numbers and other account details. Keep in mind that whatever the tools return is sent to the model your MCP client uses.
17
27
 
18
28
  ## Setup
19
29
 
@@ -46,7 +56,7 @@ Most clients take a configuration like this:
46
56
  }
47
57
  ```
48
58
 
49
- `PICNIC_COUNTRY_CODE` is `NL`, `DE` or `FR` (default `NL`). The server runs over stdio.
59
+ `PICNIC_COUNTRY_CODE` is `NL`, `DE` or `FR` (default `NL`). To choose the tool groups, add `PICNIC_MCP_TOOLS` with a comma-separated list, e.g. `"cart,slots,product,deliveries,recipes"` (default `cart,slots,product`). The server runs over stdio.
50
60
 
51
61
  When the key expires, the tools say so; run `npx picnic-mcp login` again.
52
62
 
@@ -61,4 +71,4 @@ const server = createServer({ authKey: "<auth key>", countryCode: "NL" });
61
71
  await server.connect(transport);
62
72
  ```
63
73
 
64
- Pass `client` instead of `authKey` to use a picnic-api client you already have, and `onError` to handle errors from Picnic yourself (by default they're logged to stderr).
74
+ Pass `tools` to choose the tool groups (e.g. `tools: ["cart", "recipes"]`), `client` instead of `authKey` to use a picnic-api client you already have, and `onError` to handle errors from Picnic yourself (by default they're logged to stderr).
package/dist/cli.js CHANGED
@@ -4,10 +4,15 @@ import { createInterface } from "node:readline/promises";
4
4
  import { Writable } from "node:stream";
5
5
  import PicnicClient from "picnic-api";
6
6
  import { createServer } from "./server.js";
7
+ import { DEFAULT_TOOL_GROUPS, TOOL_GROUPS, parseToolGroups } from "./tools/groups.js";
7
8
  import { COUNTRY_CODES } from "./types.js";
8
9
  const USAGE = `Usage:
9
- picnic-mcp Serve the MCP server over stdio. Reads PICNIC_AUTH_KEY and
10
- PICNIC_COUNTRY_CODE (NL, DE or FR; default NL).
10
+ picnic-mcp Serve the MCP server over stdio. Reads:
11
+ PICNIC_AUTH_KEY the auth key (required)
12
+ PICNIC_COUNTRY_CODE NL, DE or FR (default NL)
13
+ PICNIC_MCP_TOOLS tool groups, comma-separated, from
14
+ ${TOOL_GROUPS.join(", ")}
15
+ (default ${DEFAULT_TOOL_GROUPS.join(",")})
11
16
  picnic-mcp login Sign in to Picnic and print an auth key for PICNIC_AUTH_KEY.`;
12
17
  function parseCountryCode(value) {
13
18
  const code = (value ?? "NL").trim().toUpperCase();
@@ -23,7 +28,9 @@ async function serve() {
23
28
  process.exit(1);
24
29
  }
25
30
  const countryCode = parseCountryCode(process.env.PICNIC_COUNTRY_CODE);
26
- serveStdio(() => createServer({ authKey, countryCode }));
31
+ const toolsEnv = process.env.PICNIC_MCP_TOOLS?.trim();
32
+ const tools = toolsEnv ? parseToolGroups(toolsEnv) : DEFAULT_TOOL_GROUPS;
33
+ serveStdio(() => createServer({ authKey, countryCode, tools }));
27
34
  }
28
35
  /** Asks for the login details on the terminal and prints the auth key. Stores nothing. */
29
36
  async function login() {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/errors.js CHANGED
@@ -14,6 +14,9 @@ export function describeError(error) {
14
14
  }
15
15
  return `Picnic answered with HTTP ${error.status}${error.code ? ` (${error.code})` : ""}.`;
16
16
  }
17
+ if (PicnicClient.isProductNotFoundError(error)) {
18
+ return `Product ${error.productId} doesn't exist or isn't available here. Check the product id with search_products.`;
19
+ }
17
20
  if (PicnicClient.isUnexpectedPageFormatError(error)) {
18
21
  return "Picnic changed the format of a page this server reads. Update picnic-mcp, or report it if this is the latest version.";
19
22
  }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,YAAY,CAAC;AAEtC;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,YAAY,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;QACzC,sFAAsF;QACtF,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,OAAO,kEAAkE,CAAC;QAC5E,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACjD,OAAO,kHAAkH,CAAC;QAC5H,CAAC;QACD,OAAO,6BAA6B,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC;IAC7F,CAAC;IACD,IAAI,YAAY,CAAC,2BAA2B,CAAC,KAAK,CAAC,EAAE,CAAC;QACpD,OAAO,uHAAuH,CAAC;IACjI,CAAC;IACD,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;QAC5D,OAAO,2CAA2C,CAAC;IACrD,CAAC;IACD,OAAO,0CAA0C,CAAC;AACpD,CAAC"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,YAAY,CAAC;AAEtC;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,YAAY,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;QACzC,sFAAsF;QACtF,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,OAAO,kEAAkE,CAAC;QAC5E,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACjD,OAAO,kHAAkH,CAAC;QAC5H,CAAC;QACD,OAAO,6BAA6B,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC;IAC7F,CAAC;IACD,IAAI,YAAY,CAAC,sBAAsB,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,OAAO,WAAW,KAAK,CAAC,SAAS,oFAAoF,CAAC;IACxH,CAAC;IACD,IAAI,YAAY,CAAC,2BAA2B,CAAC,KAAK,CAAC,EAAE,CAAC;QACpD,OAAO,uHAAuH,CAAC;IACjI,CAAC;IACD,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;QAC5D,OAAO,2CAA2C,CAAC;IACrD,CAAC;IACD,OAAO,0CAA0C,CAAC;AACpD,CAAC"}
@@ -1,16 +1,24 @@
1
1
  import type { Cart, CartLineItem, CountryCode } from "../types.js";
2
+ import type { ProductOutput } from "./products.js";
2
3
  /** A cart line as a tool shows it to a model. Optional fields are left out when empty. */
3
4
  export type CartLineOutput = {
4
5
  productId: string;
5
6
  name: string;
6
7
  size?: string;
7
8
  quantity: number;
8
- /** The price of the line, after discounts. */
9
- price: string;
9
+ /**
10
+ * The price of the line, after discounts. Left out when the product can't be delivered: Picnic
11
+ * doesn't charge it then, and shows a price of 0.
12
+ */
13
+ price?: string;
10
14
  labels?: string[];
11
15
  /** Why the product can't be delivered, when it can't. */
12
16
  unavailable?: string;
17
+ /** Products Picnic offers instead, when the product can't be delivered. */
18
+ replacements?: ProductOutput[];
13
19
  };
20
+ /** The replacements of the products that can't be delivered, by product id. */
21
+ export type ReplacementsByProduct = ReadonlyMap<string, ProductOutput[]>;
14
22
  /** The totals of the cart. */
15
23
  export type CartSummaryOutput = {
16
24
  itemCount: number;
@@ -28,6 +36,12 @@ export type CartOutput = CartSummaryOutput & {
28
36
  * Builds the model-facing cart from an allow-list of fields. The cart id, its timestamp, images
29
37
  * and other fields a model doesn't need stay out, as do fields picnic-api adds later.
30
38
  */
31
- export declare function formatCart(cart: Cart, countryCode: CountryCode): CartOutput;
32
- export declare function formatCartLine(item: CartLineItem, countryCode: CountryCode): CartLineOutput;
39
+ export declare function formatCart(cart: Cart, countryCode: CountryCode, replacements?: ReplacementsByProduct): CartOutput;
40
+ export declare function formatCartLine(item: CartLineItem, countryCode: CountryCode, replacements?: ProductOutput[]): CartLineOutput;
41
+ /**
42
+ * Says why a cart line can't be delivered and what Picnic offers instead, or nothing when the line
43
+ * can be delivered. The cart response has no usable replacement names or prices, so the
44
+ * replacements come from `cart.getReplacements`.
45
+ */
46
+ export declare function formatUnavailability(item: CartLineItem, replacements?: ProductOutput[]): Pick<CartLineOutput, "unavailable" | "replacements">;
33
47
  export declare function formatCartSummary(cart: Cart, countryCode: CountryCode): CartSummaryOutput;
@@ -3,22 +3,35 @@ import { formatPrice, formatWindow } from "./prices.js";
3
3
  * Builds the model-facing cart from an allow-list of fields. The cart id, its timestamp, images
4
4
  * and other fields a model doesn't need stay out, as do fields picnic-api adds later.
5
5
  */
6
- export function formatCart(cart, countryCode) {
6
+ export function formatCart(cart, countryCode, replacements = new Map()) {
7
7
  return {
8
- items: cart.items.map((item) => formatCartLine(item, countryCode)),
8
+ items: cart.items.map((item) => formatCartLine(item, countryCode, replacements.get(item.productId))),
9
9
  ...formatCartSummary(cart, countryCode),
10
10
  };
11
11
  }
12
- export function formatCartLine(item, countryCode) {
12
+ export function formatCartLine(item, countryCode, replacements) {
13
13
  const labels = item.labels.map((label) => label.text).filter(Boolean);
14
14
  return {
15
15
  productId: item.productId,
16
16
  name: item.name,
17
17
  ...(item.unitQuantity && { size: item.unitQuantity }),
18
18
  quantity: item.quantity,
19
- price: formatPrice(item.displayPrice, countryCode),
19
+ ...(!item.isUnavailable && { price: formatPrice(item.displayPrice, countryCode) }),
20
20
  ...(labels.length > 0 && { labels }),
21
- ...(item.isUnavailable && { unavailable: item.unavailableReason ?? "not available" }),
21
+ ...formatUnavailability(item, replacements),
22
+ };
23
+ }
24
+ /**
25
+ * Says why a cart line can't be delivered and what Picnic offers instead, or nothing when the line
26
+ * can be delivered. The cart response has no usable replacement names or prices, so the
27
+ * replacements come from `cart.getReplacements`.
28
+ */
29
+ export function formatUnavailability(item, replacements = []) {
30
+ if (!item.isUnavailable)
31
+ return {};
32
+ return {
33
+ unavailable: item.unavailableReason ?? "not available",
34
+ ...(replacements.length > 0 && { replacements }),
22
35
  };
23
36
  }
24
37
  export function formatCartSummary(cart, countryCode) {
@@ -1 +1 @@
1
- {"version":3,"file":"cart.js","sourceRoot":"","sources":["../../src/format/cart.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA4BxD;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAU,EAAE,WAAwB;IAC7D,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QAClE,GAAG,iBAAiB,CAAC,IAAI,EAAE,WAAW,CAAC;KACxC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAkB,EAAE,WAAwB;IACzE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtE,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,GAAG,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC;QACrD,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC;QAClD,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QACpC,GAAG,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,iBAAiB,IAAI,eAAe,EAAE,CAAC;KACtF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAU,EAAE,WAAwB;IACpE,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,iBAAiB,CAAC;IAC/D,MAAM,MAAM,GAAG,YAAY,CACzB,IAAI,CAAC,YAAY,EAAE,WAAW,IAAI,IAAI,EACtC,IAAI,CAAC,YAAY,EAAE,SAAS,IAAI,IAAI,CACrC,CAAC;IACF,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,UAAU;QAC1B,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC;QAChD,GAAG,CAAC,OAAO,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,EAAE,CAAC;QAClE,GAAG,CAAC,MAAM,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC;QACzC,GAAG,CAAC,IAAI,CAAC,iBAAiB,KAAK,IAAI,IAAI;YACrC,YAAY,EAAE,WAAW,CAAC,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC;SAC/D,CAAC;KACH,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"cart.js","sourceRoot":"","sources":["../../src/format/cart.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAqCxD;;;GAGG;AACH,MAAM,UAAU,UAAU,CACxB,IAAU,EACV,WAAwB,EACxB,YAAY,GAA0B,IAAI,GAAG,EAAE;IAE/C,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAC7B,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CACpE;QACD,GAAG,iBAAiB,CAAC,IAAI,EAAE,WAAW,CAAC;KACxC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,IAAkB,EAClB,WAAwB,EACxB,YAA8B;IAE9B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtE,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,GAAG,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC;QACrD,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,EAAE,CAAC;QAClF,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QACpC,GAAG,oBAAoB,CAAC,IAAI,EAAE,YAAY,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAkB,EAClB,YAAY,GAAoB,EAAE;IAElC,IAAI,CAAC,IAAI,CAAC,aAAa;QAAE,OAAO,EAAE,CAAC;IACnC,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,iBAAiB,IAAI,eAAe;QACtD,GAAG,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC;KACjD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAU,EAAE,WAAwB;IACpE,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,iBAAiB,CAAC;IAC/D,MAAM,MAAM,GAAG,YAAY,CACzB,IAAI,CAAC,YAAY,EAAE,WAAW,IAAI,IAAI,EACtC,IAAI,CAAC,YAAY,EAAE,SAAS,IAAI,IAAI,CACrC,CAAC;IACF,OAAO;QACL,SAAS,EAAE,IAAI,CAAC,UAAU;QAC1B,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC;QAChD,GAAG,CAAC,OAAO,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,EAAE,CAAC;QAClE,GAAG,CAAC,MAAM,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,CAAC;QACzC,GAAG,CAAC,IAAI,CAAC,iBAAiB,KAAK,IAAI,IAAI;YACrC,YAAY,EAAE,WAAW,CAAC,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC;SAC/D,CAAC;KACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { CountryCode, Delivery, DeliveryPosition, DeliverySummary } from "../types.js";
2
+ import { type CartLineOutput } from "./cart.js";
3
+ /**
4
+ * An upcoming delivery as a tool shows it to a model. Delivery and order ids, payment details, the
5
+ * driver, the address and the route stay out.
6
+ */
7
+ export type DeliveryOutput = {
8
+ /** The delivery window, e.g. "Thu 23 Oct, 18:00-19:00". */
9
+ window?: string;
10
+ itemCount: number;
11
+ total: string;
12
+ /** Set when the driver is on the way. */
13
+ onTheWay?: true;
14
+ /** The expected arrival, e.g. "18:12-18:32", while the driver is on the way. */
15
+ expectedArrival?: string;
16
+ products?: CartLineOutput[];
17
+ };
18
+ export declare function formatDelivery(summary: DeliverySummary, position: DeliveryPosition | null, delivery: Delivery | null, countryCode: CountryCode): DeliveryOutput;
@@ -0,0 +1,23 @@
1
+ import { formatCartLine } from "./cart.js";
2
+ import { formatPrice, formatTime, formatWindow } from "./prices.js";
3
+ export function formatDelivery(summary, position, delivery, countryCode) {
4
+ const window = formatWindow(summary.windowStart, summary.windowEnd);
5
+ const arrival = position?.inProgress ? expectedArrival(position) : null;
6
+ return {
7
+ ...(window && { window }),
8
+ itemCount: summary.itemCount,
9
+ total: formatPrice(summary.totalPrice, countryCode),
10
+ ...(position?.inProgress && { onTheWay: true }),
11
+ ...(arrival && { expectedArrival: arrival }),
12
+ ...(delivery && {
13
+ products: delivery.orders.flatMap((order) => order.items.map((item) => formatCartLine(item, countryCode))),
14
+ }),
15
+ };
16
+ }
17
+ function expectedArrival(position) {
18
+ if (position.etaWindowStart && position.etaWindowEnd) {
19
+ return `${formatTime(position.etaWindowStart)}-${formatTime(position.etaWindowEnd)}`;
20
+ }
21
+ return position.eta === null ? null : formatTime(new Date(position.eta).toISOString());
22
+ }
23
+ //# sourceMappingURL=deliveries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deliveries.js","sourceRoot":"","sources":["../../src/format/deliveries.ts"],"names":[],"mappings":"AACA,OAAO,EAAuB,cAAc,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAkBpE,MAAM,UAAU,cAAc,CAC5B,OAAwB,EACxB,QAAiC,EACjC,QAAyB,EACzB,WAAwB;IAExB,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IACpE,MAAM,OAAO,GAAG,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxE,OAAO;QACL,GAAG,CAAC,MAAM,IAAI,EAAE,MAAM,EAAE,CAAC;QACzB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC;QACnD,GAAG,CAAC,QAAQ,EAAE,UAAU,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAC/C,GAAG,CAAC,OAAO,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC;QAC5C,GAAG,CAAC,QAAQ,IAAI;YACd,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAC1C,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,CAC7D;SACF,CAAC;KACH,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,QAA0B;IACjD,IAAI,QAAQ,CAAC,cAAc,IAAI,QAAQ,CAAC,YAAY,EAAE,CAAC;QACrD,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;IACvF,CAAC;IACD,OAAO,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AACzF,CAAC"}
@@ -1,5 +1,13 @@
1
1
  import type { CountryCode } from "../types.js";
2
2
  /** Formats an amount in cents as the country shows prices, e.g. "€ 2,49" for NL. */
3
3
  export declare function formatPrice(cents: number, countryCode: CountryCode): string;
4
+ /** The date of a timestamp in Picnic's time zone, e.g. "2026-10-23". */
5
+ export declare function formatDate(timestamp: string): string;
6
+ /** The day of a timestamp, e.g. "Fri 23 Oct". */
7
+ export declare function formatDay(timestamp: string): string;
8
+ /** The time of a timestamp, e.g. "18:00". */
9
+ export declare function formatTime(timestamp: string): string;
10
+ /** A day and time, e.g. "Thu 22 Oct, 13:00". */
11
+ export declare function formatDateTime(timestamp: string): string;
4
12
  /** Formats a delivery window as e.g. "Thu 23 Oct, 18:00-19:00", or null without a start and end. */
5
13
  export declare function formatWindow(start: string | null, end: string | null): string | null;
@@ -1,27 +1,45 @@
1
1
  const LOCALES = { NL: "nl-NL", DE: "de-DE", FR: "fr-FR" };
2
2
  /** Picnic serves every supported country from the Central European time zone. */
3
3
  const TIME_ZONE = "Europe/Amsterdam";
4
+ const DAY = new Intl.DateTimeFormat("en-GB", {
5
+ timeZone: TIME_ZONE,
6
+ weekday: "short",
7
+ day: "numeric",
8
+ month: "short",
9
+ });
10
+ const TIME = new Intl.DateTimeFormat("en-GB", {
11
+ timeZone: TIME_ZONE,
12
+ hour: "2-digit",
13
+ minute: "2-digit",
14
+ });
15
+ // en-CA formats dates as YYYY-MM-DD.
16
+ const DATE = new Intl.DateTimeFormat("en-CA", { timeZone: TIME_ZONE });
4
17
  /** Formats an amount in cents as the country shows prices, e.g. "€ 2,49" for NL. */
5
18
  export function formatPrice(cents, countryCode) {
6
19
  return new Intl.NumberFormat(LOCALES[countryCode], { style: "currency", currency: "EUR" })
7
20
  .format(cents / 100)
8
21
  .replace(/\s/g, " "); // Intl uses non-breaking spaces
9
22
  }
23
+ /** The date of a timestamp in Picnic's time zone, e.g. "2026-10-23". */
24
+ export function formatDate(timestamp) {
25
+ return DATE.format(new Date(timestamp));
26
+ }
27
+ /** The day of a timestamp, e.g. "Fri 23 Oct". */
28
+ export function formatDay(timestamp) {
29
+ return DAY.format(new Date(timestamp));
30
+ }
31
+ /** The time of a timestamp, e.g. "18:00". */
32
+ export function formatTime(timestamp) {
33
+ return TIME.format(new Date(timestamp));
34
+ }
35
+ /** A day and time, e.g. "Thu 22 Oct, 13:00". */
36
+ export function formatDateTime(timestamp) {
37
+ return `${formatDay(timestamp)}, ${formatTime(timestamp)}`;
38
+ }
10
39
  /** Formats a delivery window as e.g. "Thu 23 Oct, 18:00-19:00", or null without a start and end. */
11
40
  export function formatWindow(start, end) {
12
41
  if (!start || !end)
13
42
  return null;
14
- const day = new Intl.DateTimeFormat("en-GB", {
15
- timeZone: TIME_ZONE,
16
- weekday: "short",
17
- day: "numeric",
18
- month: "short",
19
- }).format(new Date(start));
20
- const time = new Intl.DateTimeFormat("en-GB", {
21
- timeZone: TIME_ZONE,
22
- hour: "2-digit",
23
- minute: "2-digit",
24
- });
25
- return `${day}, ${time.format(new Date(start))}-${time.format(new Date(end))}`;
43
+ return `${formatDay(start)}, ${formatTime(start)}-${formatTime(end)}`;
26
44
  }
27
45
  //# sourceMappingURL=prices.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prices.js","sourceRoot":"","sources":["../../src/format/prices.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,GAAgC,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC;AAEvF,iFAAiF;AACjF,MAAM,SAAS,GAAG,kBAAkB,CAAC;AAErC,oFAAoF;AACpF,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,WAAwB;IACjE,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;SACvF,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,gCAAgC;AAC1D,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,YAAY,CAAC,KAAoB,EAAE,GAAkB;IACnE,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;QAC3C,QAAQ,EAAE,SAAS;QACnB,OAAO,EAAE,OAAO;QAChB,GAAG,EAAE,SAAS;QACd,KAAK,EAAE,OAAO;KACf,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;QAC5C,QAAQ,EAAE,SAAS;QACnB,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS;KAClB,CAAC,CAAC;IACH,OAAO,GAAG,GAAG,KAAK,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;AACjF,CAAC"}
1
+ {"version":3,"file":"prices.js","sourceRoot":"","sources":["../../src/format/prices.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,GAAgC,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC;AAEvF,iFAAiF;AACjF,MAAM,SAAS,GAAG,kBAAkB,CAAC;AAErC,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;IAC3C,QAAQ,EAAE,SAAS;IACnB,OAAO,EAAE,OAAO;IAChB,GAAG,EAAE,SAAS;IACd,KAAK,EAAE,OAAO;CACf,CAAC,CAAC;AACH,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE;IAC5C,QAAQ,EAAE,SAAS;IACnB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;CAClB,CAAC,CAAC;AACH,qCAAqC;AACrC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC;AAEvE,oFAAoF;AACpF,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,WAAwB;IACjE,OAAO,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;SACvF,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC;SACnB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,gCAAgC;AAC1D,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,UAAU,CAAC,SAAiB;IAC1C,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,SAAS,CAAC,SAAiB;IACzC,OAAO,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,UAAU,CAAC,SAAiB;IAC1C,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,OAAO,GAAG,SAAS,CAAC,SAAS,CAAC,KAAK,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;AAC7D,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,YAAY,CAAC,KAAoB,EAAE,GAAkB;IACnE,IAAI,CAAC,KAAK,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IAChC,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,KAAK,UAAU,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;AACxE,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { CountryCode, Product } from "../types.js";
2
+ /** A product's details as a tool shows them to a model. Optional fields are left out when empty. */
3
+ export type ProductDetailsOutput = {
4
+ id: string;
5
+ name: string;
6
+ brand?: string;
7
+ size?: string;
8
+ /** Left out when picnic-api couldn't read it (it returns 0 then). */
9
+ price?: string;
10
+ wasPrice?: string;
11
+ unitPrice?: string;
12
+ promotion?: string;
13
+ /** Badges, labels and tags, e.g. "XL", "Biologisch", "Diepvries". */
14
+ labels?: string[];
15
+ description?: string;
16
+ allergens: {
17
+ contains: string[];
18
+ mayContain: string[];
19
+ };
20
+ /** E.g. "Vet: 1,6g". */
21
+ nutrition?: string[];
22
+ /** Sections such as the ingredients and storage info, as markdown. */
23
+ info?: {
24
+ title: string;
25
+ content: string;
26
+ }[];
27
+ };
28
+ /** Builds the model-facing product details from an allow-list of fields. */
29
+ export declare function formatProductDetails(product: Product, countryCode: CountryCode): ProductDetailsOutput;
@@ -0,0 +1,49 @@
1
+ import { formatPrice } from "./prices.js";
2
+ /** Long texts are cut at this many characters, to keep the output short. */
3
+ const MAX_TEXT = 1500;
4
+ /** Builds the model-facing product details from an allow-list of fields. */
5
+ export function formatProductDetails(product, countryCode) {
6
+ const labels = [...product.quantityBadges, ...product.labels, ...product.tags]
7
+ .map((label) => label.text)
8
+ .filter(Boolean);
9
+ const nutrition = product.nutritionRows.map((row) => row.value === null ? row.label : `${row.label}: ${row.value}`);
10
+ // The page also shows the nutrition table as an info section; leave that one out.
11
+ const nutritionText = lines(product.nutritionRows.flatMap((row) => row.value === null ? [row.label] : [row.label, row.value]));
12
+ const info = product.infoSections
13
+ .filter((section) => section.content.trim() && lines(section.content.split("\n")) !== nutritionText)
14
+ .map((section) => ({ title: section.title, content: cut(section.content) }));
15
+ return {
16
+ id: product.id,
17
+ name: product.name,
18
+ ...(product.brand && { brand: product.brand }),
19
+ ...(product.unitQuantity && { size: product.unitQuantity }),
20
+ ...(product.displayPrice > 0 && { price: formatPrice(product.displayPrice, countryCode) }),
21
+ ...(product.displayPrice > 0 &&
22
+ product.originalPrice !== null &&
23
+ product.originalPrice > product.displayPrice && {
24
+ wasPrice: formatPrice(product.originalPrice, countryCode),
25
+ }),
26
+ ...(product.unitPrice && { unitPrice: product.unitPrice }),
27
+ ...(product.promotion && { promotion: product.promotion.label }),
28
+ ...(labels.length > 0 && { labels }),
29
+ ...(product.description && { description: cut(product.description) }),
30
+ allergens: {
31
+ contains: product.allergenGroups.contains.map((allergen) => allergen.text),
32
+ mayContain: product.allergenGroups.mayContain.map((allergen) => allergen.text),
33
+ },
34
+ ...(nutrition.length > 0 && { nutrition }),
35
+ ...(info.length > 0 && { info }),
36
+ };
37
+ }
38
+ /** Joins texts as trimmed, non-empty lines, to compare them. */
39
+ function lines(texts) {
40
+ return texts
41
+ .map((text) => text.trim())
42
+ .filter(Boolean)
43
+ .join("\n");
44
+ }
45
+ function cut(text) {
46
+ const trimmed = text.trim();
47
+ return trimmed.length > MAX_TEXT ? `${trimmed.slice(0, MAX_TEXT)}…` : trimmed;
48
+ }
49
+ //# sourceMappingURL=product.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product.js","sourceRoot":"","sources":["../../src/format/product.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { CountryCode, Recipe, SearchRecipe } from "../types.js";
2
+ export type RecipeSummaryOutput = {
3
+ recipeId: string;
4
+ name: string;
5
+ subtitle?: string;
6
+ /** E.g. "15 min". */
7
+ tags?: string[];
8
+ };
9
+ export type RecipeIngredientOutput = {
10
+ name?: string;
11
+ /** The product that is added for it. */
12
+ productId?: string;
13
+ /** How many of the product are added. */
14
+ quantity: number;
15
+ /** How much the recipe needs, e.g. "(250 g nodig)". */
16
+ needed?: string;
17
+ price?: string;
18
+ /** False when the ingredient isn't added by default. */
19
+ added: boolean;
20
+ /** Set for ingredients most people have at home, such as oil or salt. */
21
+ cupboard?: true;
22
+ /** Why the ingredient can't be delivered, when it can't. */
23
+ unavailable?: string;
24
+ };
25
+ /** A recipe as a tool shows it to a model. The user's own note stays out. */
26
+ export type RecipeOutput = {
27
+ recipeId: string;
28
+ name: string;
29
+ portions: number;
30
+ cookingTimeMinutes?: number;
31
+ description?: string;
32
+ allergens: {
33
+ contains: string[];
34
+ mayContain: string[];
35
+ };
36
+ ingredients: RecipeIngredientOutput[];
37
+ steps?: string[];
38
+ };
39
+ export declare function formatRecipeSummary(recipe: SearchRecipe): RecipeSummaryOutput;
40
+ export declare function formatRecipe(recipe: Recipe, countryCode: CountryCode): RecipeOutput;
@@ -0,0 +1,37 @@
1
+ import { formatPrice } from "./prices.js";
2
+ /** Steps are cut at this many characters each, to keep the output short. */
3
+ const MAX_STEP = 600;
4
+ export function formatRecipeSummary(recipe) {
5
+ return {
6
+ recipeId: recipe.id,
7
+ name: recipe.name,
8
+ ...(recipe.subtitle && { subtitle: recipe.subtitle }),
9
+ ...(recipe.tags.length > 0 && { tags: recipe.tags }),
10
+ };
11
+ }
12
+ export function formatRecipe(recipe, countryCode) {
13
+ return {
14
+ recipeId: recipe.id,
15
+ name: recipe.name,
16
+ portions: recipe.portions,
17
+ ...(recipe.cookingTimeMinutes !== null && { cookingTimeMinutes: recipe.cookingTimeMinutes }),
18
+ ...(recipe.description && { description: recipe.description }),
19
+ allergens: { contains: recipe.allergens.contains, mayContain: recipe.allergens.mayContain },
20
+ ingredients: recipe.ingredients.map((ingredient) => ({
21
+ ...(ingredient.name && { name: ingredient.name }),
22
+ ...(ingredient.sellingUnitId && { productId: ingredient.sellingUnitId }),
23
+ quantity: ingredient.quantity,
24
+ ...(ingredient.neededText && { needed: ingredient.neededText }),
25
+ ...(ingredient.price !== null && { price: formatPrice(ingredient.price, countryCode) }),
26
+ added: ingredient.checked,
27
+ ...(ingredient.type === "CUPBOARD" && { cupboard: true }),
28
+ ...(!ingredient.isAvailable && {
29
+ unavailable: ingredient.unavailableReason ?? "not available",
30
+ }),
31
+ })),
32
+ ...(recipe.steps.length > 0 && {
33
+ steps: recipe.steps.map((step) => step.length > MAX_STEP ? `${step.slice(0, MAX_STEP)}…` : step),
34
+ }),
35
+ };
36
+ }
37
+ //# sourceMappingURL=recipes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipes.js","sourceRoot":"","sources":["../../src/format/recipes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,4EAA4E;AAC5E,MAAM,QAAQ,GAAG,GAAG,CAAC;AAuCrB,MAAM,UAAU,mBAAmB,CAAC,MAAoB;IACtD,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,EAAE;QACnB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,GAAG,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;QACrD,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;KACrD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,WAAwB;IACnE,OAAO;QACL,QAAQ,EAAE,MAAM,CAAC,EAAE;QACnB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,CAAC,MAAM,CAAC,kBAAkB,KAAK,IAAI,IAAI,EAAE,kBAAkB,EAAE,MAAM,CAAC,kBAAkB,EAAE,CAAC;QAC5F,GAAG,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC;QAC9D,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE;QAC3F,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YACnD,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;YACjD,GAAG,CAAC,UAAU,CAAC,aAAa,IAAI,EAAE,SAAS,EAAE,UAAU,CAAC,aAAa,EAAE,CAAC;YACxE,QAAQ,EAAE,UAAU,CAAC,QAAQ;YAC7B,GAAG,CAAC,UAAU,CAAC,UAAU,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,UAAU,EAAE,CAAC;YAC/D,GAAG,CAAC,UAAU,CAAC,KAAK,KAAK,IAAI,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,UAAU,CAAC,KAAK,EAAE,WAAW,CAAC,EAAE,CAAC;YACvF,KAAK,EAAE,UAAU,CAAC,OAAO;YACzB,GAAG,CAAC,UAAU,CAAC,IAAI,KAAK,UAAU,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YACzD,GAAG,CAAC,CAAC,UAAU,CAAC,WAAW,IAAI;gBAC7B,WAAW,EAAE,UAAU,CAAC,iBAAiB,IAAI,eAAe;aAC7D,CAAC;SACH,CAAC,CAAC;QACH,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI;YAC7B,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAC/B,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAC9D;SACF,CAAC;KACH,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { CountryCode, DeliverySlots } from "../types.js";
2
+ /** A delivery slot as a tool shows it to a model. */
3
+ export type SlotOutput = {
4
+ slotId: string;
5
+ /** E.g. "18:00-19:00". */
6
+ time: string;
7
+ /** Set when it differs from the day's. */
8
+ orderBefore?: string;
9
+ /** Set when it differs from the day's. */
10
+ minimumOrder?: string;
11
+ /** Picnic's green choice: a longer window that lets Picnic plan a more efficient route. */
12
+ greenChoice?: true;
13
+ selected?: true;
14
+ };
15
+ /** The slots of one day. The order deadline and minimum are given once when all slots share them. */
16
+ export type SlotDayOutput = {
17
+ /** E.g. "2026-10-23", to ask for this day. */
18
+ date: string;
19
+ /** E.g. "Fri 23 Oct". */
20
+ day: string;
21
+ orderBefore?: string;
22
+ minimumOrder?: string;
23
+ slots: SlotOutput[];
24
+ };
25
+ /**
26
+ * Groups the available slots by day, in Picnic's order. The selected slot is marked, also when
27
+ * Picnic preselected it: the slot list only flags a slot the user picked.
28
+ */
29
+ export declare function slotDays(slots: DeliverySlots, countryCode: CountryCode): SlotDayOutput[];
@@ -0,0 +1,40 @@
1
+ import { formatDate, formatDateTime, formatDay, formatPrice, formatTime } from "./prices.js";
2
+ /**
3
+ * Groups the available slots by day, in Picnic's order. The selected slot is marked, also when
4
+ * Picnic preselected it: the slot list only flags a slot the user picked.
5
+ */
6
+ export function slotDays(slots, countryCode) {
7
+ const selectedId = slots.selectedSlot?.slotId ?? null;
8
+ const byDate = new Map();
9
+ for (const slot of slots.slots.filter((slot) => slot.isAvailable)) {
10
+ const date = formatDate(slot.windowStart);
11
+ byDate.set(date, [...(byDate.get(date) ?? []), slot]);
12
+ }
13
+ return [...byDate].map(([date, daySlots]) => formatSlotDay(date, daySlots, selectedId, countryCode));
14
+ }
15
+ function formatSlotDay(date, slots, selectedId, countryCode) {
16
+ const orderBefore = shared(slots.map((slot) => formatDateTime(slot.cutOffTime)));
17
+ const minimum = shared(slots.map((slot) => slot.minimumOrderValue === null ? "" : formatPrice(slot.minimumOrderValue, countryCode)));
18
+ return {
19
+ date,
20
+ day: formatDay(slots[0].windowStart),
21
+ ...(orderBefore && { orderBefore }),
22
+ ...(minimum && { minimumOrder: minimum }),
23
+ slots: slots.map((slot) => {
24
+ const minimumOrder = slot.minimumOrderValue === null ? null : formatPrice(slot.minimumOrderValue, countryCode);
25
+ return {
26
+ slotId: slot.slotId,
27
+ time: `${formatTime(slot.windowStart)}-${formatTime(slot.windowEnd)}`,
28
+ ...(!orderBefore && { orderBefore: formatDateTime(slot.cutOffTime) }),
29
+ ...(!minimum && minimumOrder && { minimumOrder }),
30
+ ...(slot.isGreenChoice && { greenChoice: true }),
31
+ ...((slot.isSelected || slot.slotId === selectedId) && { selected: true }),
32
+ };
33
+ }),
34
+ };
35
+ }
36
+ /** The value all items share, or null when they differ. */
37
+ function shared(values) {
38
+ return values.every((value) => value === values[0]) && values[0] ? values[0] : null;
39
+ }
40
+ //# sourceMappingURL=slots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slots.js","sourceRoot":"","sources":["../../src/format/slots.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA2B7F;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAoB,EAAE,WAAwB;IACrE,MAAM,UAAU,GAAG,KAAK,CAAC,YAAY,EAAE,MAAM,IAAI,IAAI,CAAC;IACtD,MAAM,MAAM,GAAG,IAAI,GAAG,EAA0B,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC1C,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE,CAC1C,aAAa,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,CAAC,CACvD,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CACpB,IAAY,EACZ,KAAqB,EACrB,UAAyB,EACzB,WAAwB;IAExB,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACjF,MAAM,OAAO,GAAG,MAAM,CACpB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACjB,IAAI,CAAC,iBAAiB,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,CACxF,CACF,CAAC;IACF,OAAO;QACL,IAAI;QACJ,GAAG,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;QACpC,GAAG,CAAC,WAAW,IAAI,EAAE,WAAW,EAAE,CAAC;QACnC,GAAG,CAAC,OAAO,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;QACzC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YACxB,MAAM,YAAY,GAChB,IAAI,CAAC,iBAAiB,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,CAAC;YAC5F,OAAO;gBACL,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,IAAI,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE;gBACrE,GAAG,CAAC,CAAC,WAAW,IAAI,EAAE,WAAW,EAAE,cAAc,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;gBACrE,GAAG,CAAC,CAAC,OAAO,IAAI,YAAY,IAAI,EAAE,YAAY,EAAE,CAAC;gBACjD,GAAG,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;gBAChD,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,MAAM,KAAK,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;aAC3E,CAAC;QACJ,CAAC,CAAC;KACH,CAAC;AACJ,CAAC;AAED,2DAA2D;AAC3D,SAAS,MAAM,CAAC,MAAgB;IAC9B,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACtF,CAAC"}
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export { createServer, type CreateServerOptions } from "./server.js";
2
2
  export type { CountryCode } from "./types.js";
3
3
  export type { ProductOutput } from "./format/products.js";
4
4
  export type { CartOutput, CartLineOutput, CartSummaryOutput } from "./format/cart.js";
5
+ export { DEFAULT_TOOL_GROUPS, TOOL_GROUPS, type ToolGroup } from "./tools/groups.js";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { createServer } from "./server.js";
2
+ export { DEFAULT_TOOL_GROUPS, TOOL_GROUPS } from "./tools/groups.js";
2
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA4B,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA4B,MAAM,aAAa,CAAC;AAIrE,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAkB,MAAM,mBAAmB,CAAC"}