albert-heijn-mcp 1.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 (71) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +376 -0
  3. package/assets/icon.png +0 -0
  4. package/deploy/albert-heijn-mcp.service +42 -0
  5. package/dist/ahapi/auth.js +18 -0
  6. package/dist/ahapi/auth.js.map +1 -0
  7. package/dist/ahapi/bonus.js +53 -0
  8. package/dist/ahapi/bonus.js.map +1 -0
  9. package/dist/ahapi/client.js +115 -0
  10. package/dist/ahapi/client.js.map +1 -0
  11. package/dist/ahapi/favorites.js +67 -0
  12. package/dist/ahapi/favorites.js.map +1 -0
  13. package/dist/ahapi/index.js +12 -0
  14. package/dist/ahapi/index.js.map +1 -0
  15. package/dist/ahapi/member.js +23 -0
  16. package/dist/ahapi/member.js.map +1 -0
  17. package/dist/ahapi/orders.js +85 -0
  18. package/dist/ahapi/orders.js.map +1 -0
  19. package/dist/ahapi/products.js +103 -0
  20. package/dist/ahapi/products.js.map +1 -0
  21. package/dist/ahapi/receipts.js +37 -0
  22. package/dist/ahapi/receipts.js.map +1 -0
  23. package/dist/ahapi/recipes.js +57 -0
  24. package/dist/ahapi/recipes.js.map +1 -0
  25. package/dist/ahapi/shoppingList.js +58 -0
  26. package/dist/ahapi/shoppingList.js.map +1 -0
  27. package/dist/ahapi/stores.js +35 -0
  28. package/dist/ahapi/stores.js.map +1 -0
  29. package/dist/auth/login.js +15 -0
  30. package/dist/auth/login.js.map +1 -0
  31. package/dist/auth/session.js +125 -0
  32. package/dist/auth/session.js.map +1 -0
  33. package/dist/auth/tokens.js +132 -0
  34. package/dist/auth/tokens.js.map +1 -0
  35. package/dist/config.js +61 -0
  36. package/dist/config.js.map +1 -0
  37. package/dist/index.js +70 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/log.js +70 -0
  40. package/dist/log.js.map +1 -0
  41. package/dist/server/http.js +99 -0
  42. package/dist/server/http.js.map +1 -0
  43. package/dist/tools/account.js +126 -0
  44. package/dist/tools/account.js.map +1 -0
  45. package/dist/tools/bonus.js +151 -0
  46. package/dist/tools/bonus.js.map +1 -0
  47. package/dist/tools/cache.js +44 -0
  48. package/dist/tools/cache.js.map +1 -0
  49. package/dist/tools/cart.js +127 -0
  50. package/dist/tools/cart.js.map +1 -0
  51. package/dist/tools/common.js +173 -0
  52. package/dist/tools/common.js.map +1 -0
  53. package/dist/tools/favorites.js +103 -0
  54. package/dist/tools/favorites.js.map +1 -0
  55. package/dist/tools/index.js +24 -0
  56. package/dist/tools/index.js.map +1 -0
  57. package/dist/tools/orders.js +141 -0
  58. package/dist/tools/orders.js.map +1 -0
  59. package/dist/tools/products.js +165 -0
  60. package/dist/tools/products.js.map +1 -0
  61. package/dist/tools/receipts.js +52 -0
  62. package/dist/tools/receipts.js.map +1 -0
  63. package/dist/tools/recipes.js +261 -0
  64. package/dist/tools/recipes.js.map +1 -0
  65. package/dist/tools/shoppingList.js +149 -0
  66. package/dist/tools/shoppingList.js.map +1 -0
  67. package/dist/tools/stores.js +108 -0
  68. package/dist/tools/stores.js.map +1 -0
  69. package/dist/tools/views.js +100 -0
  70. package/dist/tools/views.js.map +1 -0
  71. package/package.json +53 -0
@@ -0,0 +1,173 @@
1
+ import { z } from "zod";
2
+ import { getMember, hasStatus } from "../ahapi/index.js";
3
+ import { NotLoggedInError } from "../auth/session.js";
4
+ import { log } from "../log.js";
5
+ import { TtlCache } from "./cache.js";
6
+ export function newToolContext(session, remote) {
7
+ return {
8
+ session,
9
+ remote,
10
+ searches: new TtlCache(5 * 60_000),
11
+ products: new TtlCache(10 * 60_000),
12
+ nutrition: new TtlCache(60 * 60_000),
13
+ bonuses: new TtlCache(10 * 60_000),
14
+ bonusPeriods: new TtlCache(10 * 60_000),
15
+ members: new TtlCache(30 * 60_000),
16
+ };
17
+ }
18
+ function annotations(title, kind) {
19
+ switch (kind) {
20
+ case "readOnly":
21
+ return { title, readOnlyHint: true };
22
+ case "additive":
23
+ return { title, readOnlyHint: false, destructiveHint: false };
24
+ case "destructive":
25
+ return { title, readOnlyHint: false, destructiveHint: true };
26
+ }
27
+ }
28
+ export function text(value) {
29
+ return { content: [{ type: "text", text: value }] };
30
+ }
31
+ /** A result with structured data, also sent as JSON text for clients without structured output; message replaces the text. */
32
+ export function structured(data, message) {
33
+ // AH sends null for some missing values; as "absent" they fit the optional fields of the output schemas.
34
+ const json = JSON.stringify(data, (_key, value) => (value === null ? undefined : value), 2);
35
+ return { content: [{ type: "text", text: message ?? json }], structuredContent: JSON.parse(json) };
36
+ }
37
+ function errorResult(err) {
38
+ return { content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }], isError: true };
39
+ }
40
+ /** Registers a tool. Thrown errors become error results; every call is logged. */
41
+ export function addTool(server, def, handler) {
42
+ const callback = async (args) => {
43
+ const start = Date.now();
44
+ try {
45
+ const result = await handler(args);
46
+ log.info("tool call", { tool: def.name, duration_ms: Date.now() - start });
47
+ return result;
48
+ }
49
+ catch (err) {
50
+ log.warn("tool call returned error", { tool: def.name, duration_ms: Date.now() - start, err });
51
+ return errorResult(err);
52
+ }
53
+ };
54
+ server.registerTool(def.name, {
55
+ title: def.title,
56
+ description: def.description + (hasUrl(def.output) ? LINK_HINT : ""),
57
+ inputSchema: def.input ?? {},
58
+ outputSchema: def.output,
59
+ annotations: annotations(def.title, def.kind),
60
+ },
61
+ // Typed via Args<Shape> above; the SDK's generic callback type doesn't infer here.
62
+ callback);
63
+ }
64
+ /** Appended to tools whose results have ah.nl links; tool descriptions reach the model in every client. */
65
+ const LINK_HINT = " When you mention a product or recipe from the result, link its name to its url.";
66
+ /** Reports whether results of this output shape include a url field anywhere. */
67
+ function hasUrl(output) {
68
+ return output !== undefined && JSON.stringify(z.toJSONSchema(z.object(output))).includes('"url"');
69
+ }
70
+ const NOT_AUTHENTICATED = JSON.stringify({
71
+ error: "not_authenticated",
72
+ message: "Not logged in. Call ah_login first.",
73
+ });
74
+ /** Registers a tool that needs a login. On a 401, refreshes the token and retries once. */
75
+ export function addAuthedTool(server, ctx, def, handler) {
76
+ addTool(server, def, async (args) => {
77
+ try {
78
+ const c = await ctx.session.client();
79
+ try {
80
+ return await handler(c, args);
81
+ }
82
+ catch (err) {
83
+ if (!isUnauthorized(err) || !c.token)
84
+ throw err;
85
+ return await handler(await ctx.session.refreshAfterRejection(c.token), args);
86
+ }
87
+ }
88
+ catch (err) {
89
+ throw err instanceof NotLoggedInError ? new Error(NOT_AUTHENTICATED) : err;
90
+ }
91
+ });
92
+ }
93
+ /** Reports whether err, or any error in its cause chain, is an HTTP 401 from AH. */
94
+ export function isUnauthorized(err) {
95
+ for (let e = err; e instanceof Error; e = e.cause) {
96
+ if (hasStatus(e, 401))
97
+ return true;
98
+ }
99
+ return false;
100
+ }
101
+ export { wrapError } from "../ahapi/index.js";
102
+ /** Runs fn; a non-401 error goes to onError. 401s propagate so addAuthedTool can refresh the token. */
103
+ export async function nonFatal(fn, onError) {
104
+ try {
105
+ return await fn();
106
+ }
107
+ catch (err) {
108
+ if (isUnauthorized(err))
109
+ throw err;
110
+ return onError(err);
111
+ }
112
+ }
113
+ /** The logged-in member's profile, cached. */
114
+ export function cachedMember(ctx, c) {
115
+ return ctx.members.getOrLoad(c.token ?? "", () => getMember(c));
116
+ }
117
+ /** v if positive, else def. */
118
+ export function orDefault(v, def) {
119
+ return v && v > 0 ? v : def;
120
+ }
121
+ /** Runs fn(0..n-1) with at most limit in flight. */
122
+ export async function parallel(n, limit, fn) {
123
+ let next = 0;
124
+ const worker = async () => {
125
+ while (next < n)
126
+ await fn(next++);
127
+ };
128
+ await Promise.all(Array.from({ length: Math.min(limit, n) }, worker));
129
+ }
130
+ const amsterdam = new Intl.DateTimeFormat("sv-SE", {
131
+ timeZone: "Europe/Amsterdam",
132
+ year: "numeric",
133
+ month: "2-digit",
134
+ day: "2-digit",
135
+ hour: "2-digit",
136
+ minute: "2-digit",
137
+ });
138
+ /** ISO 8601 → "YYYY-MM-DD[ HH:MM]" in Dutch time; returns s unchanged if unparsable. */
139
+ export function formatDate(s, withTime) {
140
+ if (!s)
141
+ return "";
142
+ const d = new Date(s);
143
+ if (Number.isNaN(d.getTime()))
144
+ return s;
145
+ const formatted = amsterdam.format(d); // "YYYY-MM-DD HH:MM"
146
+ return withTime ? formatted : formatted.slice(0, 10);
147
+ }
148
+ /** ISO 8601 → "HH:MM" in Dutch time. */
149
+ export function formatTime(s) {
150
+ return formatDate(s, true).slice(11);
151
+ }
152
+ /** Up to 3 attempts on rate limiting (429), waiting 1 s then 2 s. */
153
+ export async function withRetry(tool, fn) {
154
+ for (let attempt = 0;; attempt++) {
155
+ try {
156
+ return await fn();
157
+ }
158
+ catch (err) {
159
+ if (attempt === 2 || !isRateLimited(err))
160
+ throw err;
161
+ const waitMs = 1000 * 2 ** attempt;
162
+ log.warn("rate limited by AH, retrying", { tool, wait_ms: waitMs, attempt: attempt + 1 });
163
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
164
+ }
165
+ }
166
+ }
167
+ function isRateLimited(err) {
168
+ if (hasStatus(err, 429))
169
+ return true;
170
+ const msg = err instanceof Error ? err.message.toLowerCase() : "";
171
+ return msg.includes("rate limit") || msg.includes("too many requests");
172
+ }
173
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../../src/tools/common.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ import { z } from "zod";
2
+ import { addToFavoriteList, createFavoriteList, deleteFavoriteList, getFavoriteLists, GraphQLError, removeFromFavoriteList, } from "../ahapi/index.js";
3
+ import { addAuthedTool, orDefault, structured, text, withRetry } from "./common.js";
4
+ import { confirmInput } from "./shoppingList.js";
5
+ import { favoriteListView, viewFavoriteList } from "./views.js";
6
+ const listId = z.string().describe("Favorite list ID from ah_get_favorite_lists");
7
+ export function registerFavoriteListTools(server, ctx) {
8
+ addAuthedTool(server, ctx, {
9
+ name: "ah_get_favorite_lists",
10
+ title: "Albert Heijn: View Favourite Lists",
11
+ kind: "readOnly",
12
+ output: { lists: z.array(favoriteListView) },
13
+ description: "List all Albert Heijn favorite/saved shopping lists with their names and item counts. " +
14
+ "Use the returned list ID with ah_add_to_favorite_list, ah_remove_from_favorite_list or ah_delete_favorite_list.",
15
+ }, async (c) => {
16
+ const lists = await getFavoriteLists(c);
17
+ return structured({ lists: lists.map(viewFavoriteList) }, lists.length ? undefined : "You have no favorite lists.");
18
+ });
19
+ addAuthedTool(server, ctx, {
20
+ name: "ah_add_to_favorite_list",
21
+ title: "Albert Heijn: Add to Favourite List",
22
+ kind: "additive",
23
+ description: "Add products to a named Albert Heijn favorite list. Get list_id from ah_get_favorite_lists.",
24
+ input: {
25
+ list_id: listId,
26
+ items: z
27
+ .array(z.object({ product_id: z.number().int(), quantity: z.number().int() }))
28
+ .describe('Items to add, e.g. [{"product_id": 123456, "quantity": 1}]. Quantity 0 is treated as 1.'),
29
+ },
30
+ }, async (c, args) => {
31
+ if (!args.list_id)
32
+ throw new Error("list_id is required");
33
+ const quantities = new Map();
34
+ for (const it of args.items)
35
+ if (it.product_id > 0)
36
+ quantities.set(it.product_id, orDefault(it.quantity, 1));
37
+ if (quantities.size === 0)
38
+ throw new Error("no valid items provided");
39
+ await addToFavoriteList(c, args.list_id, quantities);
40
+ return text(`Added ${quantities.size} item(s) to favorite list ${args.list_id}.`);
41
+ });
42
+ addAuthedTool(server, ctx, {
43
+ name: "ah_remove_from_favorite_list",
44
+ title: "Albert Heijn: Remove from Favourite List",
45
+ kind: "destructive",
46
+ description: "Remove products from a named Albert Heijn favorite list. Get list_id from ah_get_favorite_lists.",
47
+ input: {
48
+ list_id: listId,
49
+ product_ids: z.array(z.number().int()).describe("Product IDs to remove, e.g. [123456, 789012]"),
50
+ },
51
+ }, async (c, args) => {
52
+ if (!args.list_id)
53
+ throw new Error("list_id is required");
54
+ const ids = args.product_ids.filter((id) => id > 0);
55
+ if (ids.length === 0)
56
+ throw new Error("no valid product_ids provided");
57
+ const removed = await removeFromFavoriteList(c, args.list_id, ids);
58
+ return text(`Removed ${removed} product(s) from favorite list ${args.list_id}.`);
59
+ });
60
+ addAuthedTool(server, ctx, {
61
+ name: "ah_create_favorite_list",
62
+ title: "Albert Heijn: Create Favourite List",
63
+ kind: "additive",
64
+ output: favoriteListView.shape,
65
+ description: "Create a new, empty Albert Heijn favorite list with the given name; AH drops some punctuation, such as '-'. " +
66
+ "Returns its id; add products with ah_add_to_favorite_list.",
67
+ input: { name: z.string().describe("Name of the list, e.g. 'Weekend' or 'Pasta night'") },
68
+ }, async (c, args) => {
69
+ const name = args.name.trim();
70
+ if (!name)
71
+ throw new Error("name is required");
72
+ return structured(viewFavoriteList(await createFavoriteList(c, name)));
73
+ });
74
+ addAuthedTool(server, ctx, {
75
+ name: "ah_delete_favorite_list",
76
+ title: "Albert Heijn: Delete Favourite List",
77
+ kind: "destructive",
78
+ description: 'Delete an Albert Heijn favorite list and everything on it. Irreversible — requires confirm="yes". ' +
79
+ "Get list_id from ah_get_favorite_lists.",
80
+ input: { list_id: listId, ...confirmInput },
81
+ }, async (c, args) => {
82
+ if (args.confirm !== "yes")
83
+ throw new Error('confirm must be "yes" to delete the list');
84
+ if (!args.list_id)
85
+ throw new Error("list_id is required");
86
+ // AH answers an unknown id with the same redacted error as an outage; all lists tells them apart.
87
+ const list = await withRetry("ah_delete_favorite_list", async () => {
88
+ try {
89
+ return (await getFavoriteLists(c, [args.list_id]))[0];
90
+ }
91
+ catch (err) {
92
+ if (!(err instanceof GraphQLError))
93
+ throw err;
94
+ return (await getFavoriteLists(c)).find((l) => l.id === args.list_id);
95
+ }
96
+ });
97
+ if (!list)
98
+ throw new Error(`favorite list ${args.list_id} not found`);
99
+ await deleteFavoriteList(c, list.id);
100
+ return text(`Deleted favorite list "${list.name}" (${list.itemCount} items).`);
101
+ });
102
+ }
103
+ //# sourceMappingURL=favorites.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"favorites.js","sourceRoot":"","sources":["../../src/tools/favorites.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { registerAccountTools } from "./account.js";
2
+ import { registerBonusTools } from "./bonus.js";
3
+ import { registerCartTools } from "./cart.js";
4
+ import { registerFavoriteListTools } from "./favorites.js";
5
+ import { registerOrderTools } from "./orders.js";
6
+ import { registerProductTools } from "./products.js";
7
+ import { registerReceiptTools } from "./receipts.js";
8
+ import { registerRecipeTools } from "./recipes.js";
9
+ import { registerShoppingListTools } from "./shoppingList.js";
10
+ import { registerStoreTools } from "./stores.js";
11
+ export { newToolContext } from "./common.js";
12
+ export function registerTools(server, ctx) {
13
+ registerAccountTools(server, ctx);
14
+ registerProductTools(server, ctx);
15
+ registerBonusTools(server, ctx);
16
+ registerRecipeTools(server, ctx);
17
+ registerStoreTools(server, ctx);
18
+ registerShoppingListTools(server, ctx);
19
+ registerFavoriteListTools(server, ctx);
20
+ registerCartTools(server, ctx);
21
+ registerOrderTools(server, ctx);
22
+ registerReceiptTools(server, ctx);
23
+ }
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAE9C,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAE,cAAc,EAAoB,MAAM,aAAa,CAAC;AAE/D,MAAM,UAAU,aAAa,CAAC,MAAiB,EAAE,GAAgB;IAC/D,oBAAoB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAClC,oBAAoB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAClC,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,mBAAmB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,yBAAyB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,yBAAyB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,iBAAiB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,oBAAoB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,141 @@
1
+ import { z } from "zod";
2
+ import { getFulfillments, getOrderDetails, productUrl } from "../ahapi/index.js";
3
+ import { log } from "../log.js";
4
+ import { addAuthedTool, nonFatal, orDefault, parallel, structured, withRetry } from "./common.js";
5
+ import { orderView, viewOrder } from "./views.js";
6
+ /** Status of a cancelled order; AH reports statuses in Dutch ("Geannuleerd"). */
7
+ const CANCELLED = /geannuleerd|cancel/i;
8
+ export function registerOrderTools(server, ctx) {
9
+ addAuthedTool(server, ctx, {
10
+ name: "ah_get_orders",
11
+ title: "Albert Heijn: Orders",
12
+ kind: "readOnly",
13
+ output: {
14
+ orders: z.array(z.object({
15
+ id: z.number(),
16
+ date: z.string().optional(),
17
+ time_window: z.string().optional(),
18
+ total_price: z.number().optional(),
19
+ status: z.string().optional(),
20
+ shopping_type: z.string().optional(),
21
+ modifiable: z.boolean().optional(),
22
+ })),
23
+ },
24
+ description: "List Albert Heijn online delivery orders. " +
25
+ "By default returns upcoming orders (with a modifiable flag); set past=true for delivered orders. " +
26
+ "Returns id, date, time_window, total_price, status. " +
27
+ "Use the returned id with ah_get_order_details to see the items.",
28
+ input: {
29
+ past: z.boolean().optional().describe("Return delivered orders instead of upcoming ones"),
30
+ limit: z.number().int().optional().describe("Maximum number of orders to return (default 10)"),
31
+ },
32
+ }, async (c, { past, limit }) => {
33
+ const fulfillments = await getFulfillments(c, Boolean(past));
34
+ const empty = past ? "No past orders found." : "No upcoming orders.";
35
+ return structured({
36
+ orders: fulfillments.slice(0, orDefault(limit, 10)).map((f) => ({
37
+ id: f.orderId,
38
+ date: f.dateDisplay || f.date || undefined,
39
+ time_window: f.timeDisplay || undefined,
40
+ total_price: f.totalPrice ?? undefined,
41
+ status: f.status,
42
+ shopping_type: f.shoppingType || undefined,
43
+ modifiable: past ? undefined : f.modifiable,
44
+ })),
45
+ }, fulfillments.length ? undefined : empty);
46
+ });
47
+ addAuthedTool(server, ctx, {
48
+ name: "ah_get_order_details",
49
+ title: "Albert Heijn: Order Details",
50
+ kind: "readOnly",
51
+ output: orderView.shape,
52
+ description: "Get the full item list for a specific Albert Heijn delivery order by its ID. " +
53
+ "Returns all products with names, quantities, and prices. " +
54
+ "Get order_id from ah_get_orders.",
55
+ input: { order_id: z.number().int().describe("Numeric order ID from ah_get_orders") },
56
+ }, async (c, { order_id }) => {
57
+ if (order_id <= 0)
58
+ throw new Error("order_id is required");
59
+ return structured(viewOrder(await getOrderDetails(c, order_id)));
60
+ });
61
+ addAuthedTool(server, ctx, {
62
+ name: "ah_get_frequent_items",
63
+ title: "Albert Heijn: Frequently Ordered Items",
64
+ kind: "readOnly",
65
+ output: {
66
+ products: z.array(z.object({
67
+ product_name: z.string(),
68
+ product_id: z.number(),
69
+ order_count: z.number(),
70
+ last_ordered_date: z.string().optional(),
71
+ url: z.string(),
72
+ })),
73
+ },
74
+ description: "Get frequently ordered products by analysing order history. " +
75
+ "Fetches all fulfillments, expands each order's items, counts per product, " +
76
+ "and returns products ordered at least min_order_count times. " +
77
+ "Returns product_name, product_id, order_count, last_ordered_date.",
78
+ input: {
79
+ min_order_count: z
80
+ .number()
81
+ .int()
82
+ .optional()
83
+ .describe("Minimum number of orders a product must appear in (default 3)"),
84
+ },
85
+ }, async (c, args) => {
86
+ const minCount = orDefault(args.min_order_count, 3);
87
+ const [open, closed] = await Promise.all([
88
+ getFulfillments(c, false),
89
+ nonFatal(() => getFulfillments(c, true), (err) => {
90
+ // Continue with open orders only.
91
+ log.warn("could not fetch past orders", { tool: "ah_get_frequent_items", err });
92
+ return [];
93
+ }),
94
+ ]);
95
+ // Cancelled orders were never delivered, so they don't count as purchases.
96
+ const refs = [...open, ...closed.filter((f) => !CANCELLED.test(f.status))];
97
+ const orders = new Array(refs.length);
98
+ await parallel(refs.length, 5, async (i) => {
99
+ const id = refs[i].orderId;
100
+ orders[i] = await nonFatal(() => withRetry("ah_get_frequent_items", () => getOrderDetails(c, id)), (err) => {
101
+ log.warn("could not fetch order", { tool: "ah_get_frequent_items", order_id: id, err });
102
+ return undefined;
103
+ });
104
+ });
105
+ const stats = new Map();
106
+ orders.forEach((order, i) => {
107
+ if (!order)
108
+ return;
109
+ const date = refs[i].date ?? "";
110
+ // First title per product; an order lists a product at most once in the count.
111
+ const titles = new Map();
112
+ for (const it of order.items)
113
+ if (!titles.has(it.productId))
114
+ titles.set(it.productId, it.title);
115
+ for (const [productId, title] of titles) {
116
+ const name = title || String(productId);
117
+ let s = stats.get(productId);
118
+ if (!s) {
119
+ s = { product_name: name, product_id: productId, order_count: 0, last_ordered_date: "" };
120
+ stats.set(productId, s);
121
+ }
122
+ s.order_count++;
123
+ if (date > s.last_ordered_date) {
124
+ s.last_ordered_date = date;
125
+ s.product_name = name;
126
+ }
127
+ }
128
+ });
129
+ return structured({
130
+ products: [...stats.values()]
131
+ .filter((s) => s.order_count >= minCount)
132
+ .sort((a, b) => b.order_count - a.order_count)
133
+ .map((s) => ({
134
+ ...s,
135
+ url: productUrl(s.product_id, s.product_name),
136
+ last_ordered_date: s.last_ordered_date || undefined,
137
+ })),
138
+ });
139
+ });
140
+ }
141
+ //# sourceMappingURL=orders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orders.js","sourceRoot":"","sources":["../../src/tools/orders.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,165 @@
1
+ import { z } from "zod";
2
+ import { getNutrition, getProduct, getProductAlternatives, searchProducts, } from "../ahapi/index.js";
3
+ import { addAuthedTool, nonFatal, orDefault, parallel, structured, withRetry } from "./common.js";
4
+ import { detailProduct, productDetail, productSummary, summarizeProduct } from "./views.js";
5
+ const MAX_QUERIES = 10;
6
+ const MAX_PRODUCT_IDS = 20;
7
+ /** Search filters → AH property ids. */
8
+ const FILTERS = {
9
+ organic: "np_biologisch",
10
+ prijsfavoriet: "np_verbluffen",
11
+ new: "np_nieuw",
12
+ frozen: "diepvries",
13
+ vegan: "sp_include_dieet_veganistisch",
14
+ vegetarian: "sp_include_dieet_vegetarisch",
15
+ low_sugar: "sp_include_dieet_laag_suiker",
16
+ low_fat: "sp_include_dieet_laag_vet",
17
+ low_salt: "sp_include_dieet_laag_zout",
18
+ gluten_free: "sp_include_intolerance_geen_gluten",
19
+ lactose_free: "sp_include_intolerance_geen_lactose",
20
+ milk_free: "sp_include_intolerance_geen_melk",
21
+ egg_free: "sp_include_intolerance_geen_eieren",
22
+ nut_free: "sp_include_intolerance_geen_noten",
23
+ peanut_free: "sp_include_intolerance_geen_pindas",
24
+ soy_free: "sp_include_intolerance_geen_soja",
25
+ fish_free: "sp_include_intolerance_geen_vis",
26
+ shellfish_free: "sp_include_intolerance_geen_schelpdieren",
27
+ sesame_free: "sp_include_intolerance_geen_sesam",
28
+ celery_free: "sp_include_intolerance_geen_selderij",
29
+ mustard_free: "sp_include_intolerance_geen_mosterd",
30
+ lupin_free: "sp_include_intolerance_geen_lupine",
31
+ sulphite_free: "sp_include_intolerance_geen_sulfiet",
32
+ };
33
+ const SORTS = {
34
+ relevance: "RELEVANCE",
35
+ price_low_high: "PRICELOWHIGH",
36
+ price_high_low: "PRICEHIGHLOW",
37
+ most_bought: "PURCHASE_FREQUENCY",
38
+ nutriscore: "NUTRISCORE",
39
+ };
40
+ export function registerProductTools(server, ctx) {
41
+ addAuthedTool(server, ctx, {
42
+ name: "ah_search_products",
43
+ title: "Albert Heijn: Search Products",
44
+ kind: "readOnly",
45
+ description: "Search Albert Heijn (Dutch supermarket) products by keyword. " +
46
+ "Pass one or more queries (max 10) to search for several products in one call. " +
47
+ "Prefer Dutch search terms for best results: " +
48
+ "e.g. 'melk' (milk), 'kaas' (cheese), 'brood' (bread), 'kip' (chicken), 'appel' (apple). " +
49
+ "Set bonus=true to return only products currently on promotion. " +
50
+ "filters narrows results to products matching all of them, e.g. ['organic', 'gluten_free']: " +
51
+ "organic, prijsfavoriet (AH's budget label), new, frozen, a diet (vegan, vegetarian, low_sugar, low_fat, low_salt), " +
52
+ "or free from an allergen (gluten_free, lactose_free, milk_free, egg_free, nut_free, peanut_free, soy_free, " +
53
+ "fish_free, shellfish_free, sesame_free, celery_free, mustard_free, lupin_free, sulphite_free). " +
54
+ "sort orders results: relevance (default), price_low_high, price_high_low, most_bought (by this user), nutriscore. " +
55
+ "Returns, per query: id, title, price, bonus_price, unit, is_bonus, bonus_mechanism, image_url.",
56
+ input: {
57
+ queries: z
58
+ .array(z.string())
59
+ .describe('Search queries in Dutch or English, e.g. ["melk"] or ["melk", "kaas", "brood"] (max 10)'),
60
+ limit: z
61
+ .number()
62
+ .int()
63
+ .optional()
64
+ .describe("Maximum results per query (default 10 for one query, 5 for several; max 30)"),
65
+ bonus: z.boolean().optional().describe("Return only products currently on bonus/promotion"),
66
+ filters: z
67
+ .array(z.enum(Object.keys(FILTERS)))
68
+ .optional()
69
+ .describe('Only products matching all of these, e.g. ["organic", "vegan"]'),
70
+ sort: z
71
+ .enum(Object.keys(SORTS))
72
+ .optional()
73
+ .describe("Result order (default relevance)"),
74
+ },
75
+ output: {
76
+ searches: z.array(z.object({ query: z.string(), results: z.array(productSummary), error: z.string().optional() })),
77
+ },
78
+ }, async (c, { queries, limit, bonus, filters, sort }) => {
79
+ const qs = queries.filter(Boolean).slice(0, MAX_QUERIES);
80
+ if (qs.length === 0)
81
+ throw new Error("provide at least one search query");
82
+ const perQuery = Math.min(orDefault(limit, qs.length > 1 ? 5 : 10), 30);
83
+ const results = qs.map((query) => ({
84
+ query,
85
+ results: [],
86
+ error: undefined,
87
+ }));
88
+ const opts = {
89
+ bonus: Boolean(bonus),
90
+ properties: [...new Set(filters ?? [])].map((f) => FILTERS[f]).sort(),
91
+ sort: SORTS[sort ?? "relevance"],
92
+ };
93
+ await parallel(qs.length, 3, async (i) => {
94
+ const r = results[i];
95
+ await nonFatal(async () => {
96
+ r.results = (await search(ctx, c, r.query, perQuery, opts)).map(summarizeProduct);
97
+ }, (err) => {
98
+ r.error = err.message;
99
+ });
100
+ });
101
+ return structured({ searches: results });
102
+ });
103
+ addAuthedTool(server, ctx, {
104
+ name: "ah_get_products",
105
+ title: "Albert Heijn: Product Details",
106
+ kind: "readOnly",
107
+ description: "Get detailed information about one or more Albert Heijn products by ID (max 20). " +
108
+ "Returns title, brand, category, description, price, unit size, bonus info, NutriScore, property icons. " +
109
+ "Set include_nutritional_info=true to also return calories, fat, protein, etc. " +
110
+ "Get product IDs from ah_search_products.",
111
+ input: {
112
+ product_ids: z
113
+ .array(z.number().int())
114
+ .describe("Product IDs from ah_search_products, e.g. [123456] or [123456, 789012] (max 20)"),
115
+ include_nutritional_info: z.boolean().optional().describe("Include nutritional values (default false)"),
116
+ },
117
+ output: {
118
+ products: z.array(z.union([productDetail, z.object({ id: z.number(), error: z.string() })])),
119
+ },
120
+ }, async (c, { product_ids, include_nutritional_info }) => {
121
+ const ids = product_ids.filter((id) => id > 0).slice(0, MAX_PRODUCT_IDS);
122
+ if (ids.length === 0)
123
+ throw new Error("provide at least one product ID");
124
+ const results = new Array(ids.length);
125
+ await parallel(ids.length, 5, async (i) => {
126
+ const id = ids[i];
127
+ const key = String(id);
128
+ results[i] = await nonFatal(async () => {
129
+ const p = await ctx.products.getOrLoad(key, () => withRetry("ah_get_products", () => getProduct(c, id)));
130
+ const nutrition = include_nutritional_info
131
+ ? await ctx.nutrition.getOrLoad(key, () => withRetry("ah_get_products", () => getNutrition(c, id)))
132
+ : undefined;
133
+ return detailProduct(p, nutrition);
134
+ }, (err) => ({ id, error: err.message }));
135
+ });
136
+ return structured({ products: results });
137
+ });
138
+ addAuthedTool(server, ctx, {
139
+ name: "ah_get_product_alternatives",
140
+ title: "Albert Heijn: Product Alternatives",
141
+ kind: "readOnly",
142
+ description: "Get products Albert Heijn suggests instead of a given product: similar products and substitutes, " +
143
+ "e.g. when it is sold out or to find a cheaper or organic option. " +
144
+ "Returns the same fields as ah_search_products.",
145
+ input: {
146
+ product_id: z.number().int().describe("Product ID, e.g. from ah_search_products or ah_get_shopping_list"),
147
+ limit: z.number().int().optional().describe("Maximum number of alternatives (default 10, max 30)"),
148
+ },
149
+ output: { products: z.array(productSummary) },
150
+ }, async (c, args) => {
151
+ if (args.product_id <= 0)
152
+ throw new Error("product_id is required");
153
+ const size = Math.min(orDefault(args.limit, 10), 30);
154
+ const alternatives = await ctx.searches.getOrLoad(`alternatives:${args.product_id}:${size}`, () => withRetry("ah_get_product_alternatives", () => getProductAlternatives(c, args.product_id, size)));
155
+ const message = alternatives.length ? undefined : `AH suggests no alternatives for product ${args.product_id}.`;
156
+ return structured({ products: alternatives.map(summarizeProduct) }, message);
157
+ });
158
+ }
159
+ /** Cached, retried product search. */
160
+ export function search(ctx, c, query, limit, opts) {
161
+ // most_bought orders by this account's purchases, so it is cached per login.
162
+ const account = opts.sort === "PURCHASE_FREQUENCY" ? c.token : undefined;
163
+ return ctx.searches.getOrLoad(JSON.stringify([query, limit, opts, account]), () => withRetry("ah_search_products", () => searchProducts(c, query, limit, opts)));
164
+ }
165
+ //# sourceMappingURL=products.js.map