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.
- package/LICENSE +201 -0
- package/README.md +376 -0
- package/assets/icon.png +0 -0
- package/deploy/albert-heijn-mcp.service +42 -0
- package/dist/ahapi/auth.js +18 -0
- package/dist/ahapi/auth.js.map +1 -0
- package/dist/ahapi/bonus.js +53 -0
- package/dist/ahapi/bonus.js.map +1 -0
- package/dist/ahapi/client.js +115 -0
- package/dist/ahapi/client.js.map +1 -0
- package/dist/ahapi/favorites.js +67 -0
- package/dist/ahapi/favorites.js.map +1 -0
- package/dist/ahapi/index.js +12 -0
- package/dist/ahapi/index.js.map +1 -0
- package/dist/ahapi/member.js +23 -0
- package/dist/ahapi/member.js.map +1 -0
- package/dist/ahapi/orders.js +85 -0
- package/dist/ahapi/orders.js.map +1 -0
- package/dist/ahapi/products.js +103 -0
- package/dist/ahapi/products.js.map +1 -0
- package/dist/ahapi/receipts.js +37 -0
- package/dist/ahapi/receipts.js.map +1 -0
- package/dist/ahapi/recipes.js +57 -0
- package/dist/ahapi/recipes.js.map +1 -0
- package/dist/ahapi/shoppingList.js +58 -0
- package/dist/ahapi/shoppingList.js.map +1 -0
- package/dist/ahapi/stores.js +35 -0
- package/dist/ahapi/stores.js.map +1 -0
- package/dist/auth/login.js +15 -0
- package/dist/auth/login.js.map +1 -0
- package/dist/auth/session.js +125 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/auth/tokens.js +132 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/config.js +61 -0
- package/dist/config.js.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/log.js +70 -0
- package/dist/log.js.map +1 -0
- package/dist/server/http.js +99 -0
- package/dist/server/http.js.map +1 -0
- package/dist/tools/account.js +126 -0
- package/dist/tools/account.js.map +1 -0
- package/dist/tools/bonus.js +151 -0
- package/dist/tools/bonus.js.map +1 -0
- package/dist/tools/cache.js +44 -0
- package/dist/tools/cache.js.map +1 -0
- package/dist/tools/cart.js +127 -0
- package/dist/tools/cart.js.map +1 -0
- package/dist/tools/common.js +173 -0
- package/dist/tools/common.js.map +1 -0
- package/dist/tools/favorites.js +103 -0
- package/dist/tools/favorites.js.map +1 -0
- package/dist/tools/index.js +24 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/orders.js +141 -0
- package/dist/tools/orders.js.map +1 -0
- package/dist/tools/products.js +165 -0
- package/dist/tools/products.js.map +1 -0
- package/dist/tools/receipts.js +52 -0
- package/dist/tools/receipts.js.map +1 -0
- package/dist/tools/recipes.js +261 -0
- package/dist/tools/recipes.js.map +1 -0
- package/dist/tools/shoppingList.js +149 -0
- package/dist/tools/shoppingList.js.map +1 -0
- package/dist/tools/stores.js +108 -0
- package/dist/tools/stores.js.map +1 -0
- package/dist/tools/views.js +100 -0
- package/dist/tools/views.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { log } from "../log.js";
|
|
5
|
+
const MCP_PATH = "/mcp";
|
|
6
|
+
const MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
7
|
+
function isLoopbackHost(host) {
|
|
8
|
+
const h = host.replace(/^\[|\]$/g, "");
|
|
9
|
+
return h === "localhost" || h === "::1" || /^127\./.test(h);
|
|
10
|
+
}
|
|
11
|
+
/** Reports whether url's host is localhost or loopback; an unparsable url is not. */
|
|
12
|
+
function isLocalUrl(url) {
|
|
13
|
+
return URL.canParse(url) && isLoopbackHost(new URL(url).hostname);
|
|
14
|
+
}
|
|
15
|
+
const sha256 = (s) => createHash("sha256").update(s).digest();
|
|
16
|
+
/** Checks "Authorization: Bearer <token>" or "?token=<token>" in constant time. */
|
|
17
|
+
function hasToken(req, token) {
|
|
18
|
+
// Hashing makes the lengths equal, so the comparison doesn't reveal the token's length either.
|
|
19
|
+
const expected = sha256(token);
|
|
20
|
+
const matches = (candidate) => Boolean(candidate) && timingSafeEqual(sha256(candidate), expected);
|
|
21
|
+
const auth = req.headers.authorization;
|
|
22
|
+
if (auth?.startsWith("Bearer ") && matches(auth.slice("Bearer ".length)))
|
|
23
|
+
return true;
|
|
24
|
+
return matches(new URL(req.url ?? "/", "http://localhost").searchParams.get("token"));
|
|
25
|
+
}
|
|
26
|
+
async function readJsonBody(req) {
|
|
27
|
+
const chunks = [];
|
|
28
|
+
let size = 0;
|
|
29
|
+
for await (const chunk of req) {
|
|
30
|
+
size += chunk.length;
|
|
31
|
+
if (size > MAX_BODY_BYTES)
|
|
32
|
+
throw new Error("request body too large");
|
|
33
|
+
chunks.push(chunk);
|
|
34
|
+
}
|
|
35
|
+
return chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : undefined;
|
|
36
|
+
}
|
|
37
|
+
/** Serves MCP at /mcp, stateless: each request gets a new server. Resolves once listening. */
|
|
38
|
+
export async function serveHttp(newServer, opts) {
|
|
39
|
+
// Anyone who reaches the server acts as the logged-in AH account, and a
|
|
40
|
+
// reverse proxy can make a loopback listener public without the server knowing.
|
|
41
|
+
const { token } = opts;
|
|
42
|
+
if (!token)
|
|
43
|
+
throw new Error("AH_MCP_TOKEN is required for the HTTP transport; generate one with: openssl rand -hex 32");
|
|
44
|
+
// DNS-rebinding protection for local-only setups. When the server is
|
|
45
|
+
// reachable under another name (reverse proxy, or listening on a LAN
|
|
46
|
+
// address), the Host header differs, so the check is off.
|
|
47
|
+
const local = isLocalUrl(opts.baseUrl) && isLoopbackHost(opts.host);
|
|
48
|
+
// Clients omit the port for port 80; a local proxy may listen on another port (the base URL's).
|
|
49
|
+
const allowedHosts = local
|
|
50
|
+
? [
|
|
51
|
+
...new Set([
|
|
52
|
+
...["localhost", "127.0.0.1", "[::1]"].flatMap((h) => (opts.port === 80 ? [h, `${h}:80`] : [`${h}:${opts.port}`])),
|
|
53
|
+
new URL(opts.baseUrl).host,
|
|
54
|
+
]),
|
|
55
|
+
]
|
|
56
|
+
: undefined;
|
|
57
|
+
const handle = async (req, res) => {
|
|
58
|
+
if (new URL(req.url ?? "/", "http://localhost").pathname !== MCP_PATH) {
|
|
59
|
+
res.writeHead(404).end("Not Found");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!hasToken(req, token)) {
|
|
63
|
+
res.writeHead(401).end("Unauthorized");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const body = req.method === "POST" ? await readJsonBody(req) : undefined;
|
|
67
|
+
const server = newServer();
|
|
68
|
+
const transport = new StreamableHTTPServerTransport({
|
|
69
|
+
sessionIdGenerator: undefined,
|
|
70
|
+
enableDnsRebindingProtection: local,
|
|
71
|
+
allowedHosts,
|
|
72
|
+
});
|
|
73
|
+
res.on("close", () => {
|
|
74
|
+
void transport.close();
|
|
75
|
+
void server.close();
|
|
76
|
+
});
|
|
77
|
+
await server.connect(transport);
|
|
78
|
+
await transport.handleRequest(req, res, body);
|
|
79
|
+
};
|
|
80
|
+
const httpServer = createServer((req, res) => {
|
|
81
|
+
handle(req, res).catch((err) => {
|
|
82
|
+
log.error("HTTP request failed", { err });
|
|
83
|
+
if (!res.headersSent)
|
|
84
|
+
res.writeHead(400).end("Bad Request");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
httpServer.headersTimeout = 10_000;
|
|
88
|
+
await new Promise((resolve, reject) => {
|
|
89
|
+
httpServer.once("error", reject);
|
|
90
|
+
httpServer.listen(opts.port, opts.host, resolve);
|
|
91
|
+
});
|
|
92
|
+
log.info("starting server", {
|
|
93
|
+
transport: "streamable-http",
|
|
94
|
+
addr: `${opts.host}:${opts.port}${MCP_PATH}`,
|
|
95
|
+
base_url: opts.baseUrl,
|
|
96
|
+
});
|
|
97
|
+
return httpServer;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/server/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getMember, LOGIN_URL } from "../ahapi/index.js";
|
|
4
|
+
import { extractCode } from "../auth/login.js";
|
|
5
|
+
import { SessionExpiredError } from "../auth/session.js";
|
|
6
|
+
import { addAuthedTool, addTool, isUnauthorized, structured, text } from "./common.js";
|
|
7
|
+
export function registerAccountTools(server, ctx) {
|
|
8
|
+
addTool(server, {
|
|
9
|
+
name: "ah_login",
|
|
10
|
+
title: "Albert Heijn: Log In",
|
|
11
|
+
kind: "additive",
|
|
12
|
+
description: "Log in to Albert Heijn in two steps. " +
|
|
13
|
+
"1) Call without arguments: returns the AH login link and instructions to show the user. " +
|
|
14
|
+
"2) After the user logs in, the browser shows a link like appie://login-exit?code=...; " +
|
|
15
|
+
"call ah_login again with code set to that link (or just the code) to finish. " +
|
|
16
|
+
"Codes are single-use and expire quickly. " +
|
|
17
|
+
"If already logged in, returns the account name.",
|
|
18
|
+
input: {
|
|
19
|
+
code: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("The appie://login-exit?code=... link the user copied after logging in, or just the code. Omit to start a login."),
|
|
23
|
+
},
|
|
24
|
+
}, async ({ code }) => {
|
|
25
|
+
if (code) {
|
|
26
|
+
await ctx.session.completeLogin(extractCode(code));
|
|
27
|
+
const name = await memberName(ctx);
|
|
28
|
+
return text(name ? `Login successful! Connected as ${name}.` : "Login successful!");
|
|
29
|
+
}
|
|
30
|
+
// A stored session that AH no longer accepts (e.g. a revoked refresh token) gets a new login.
|
|
31
|
+
let stale = false;
|
|
32
|
+
if (await ctx.session.isAuthenticated()) {
|
|
33
|
+
try {
|
|
34
|
+
return text(`Already connected as ${fullName(await currentMember(ctx))}.`);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (!(err instanceof SessionExpiredError)) {
|
|
38
|
+
return text(`Already logged in (could not fetch member name: ${err.message}).`);
|
|
39
|
+
}
|
|
40
|
+
stale = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!ctx.remote)
|
|
44
|
+
openBrowser(LOGIN_URL);
|
|
45
|
+
return text(`${stale ? "The stored login no longer works, so log in again.\n\n" : ""}To log in to Albert Heijn:
|
|
46
|
+
|
|
47
|
+
1. Open this link${ctx.remote ? "" : " (it should have opened in your browser)"} and log in:
|
|
48
|
+
${LOGIN_URL}
|
|
49
|
+
2. After logging in, the page stays put: the browser cannot open the AH app link it redirects to.
|
|
50
|
+
Open the developer console (Chrome: Cmd+Option+J on Mac, Ctrl+Shift+J on Windows/Linux)
|
|
51
|
+
and copy the link from the "Failed to launch 'appie://login-exit?code=...'" message.
|
|
52
|
+
3. Paste that link here. The code is single-use and expires quickly, so do this right away.`);
|
|
53
|
+
});
|
|
54
|
+
addTool(server, {
|
|
55
|
+
name: "ah_logout",
|
|
56
|
+
title: "Albert Heijn: Log Out",
|
|
57
|
+
kind: "destructive",
|
|
58
|
+
description: "Log out of Albert Heijn by deleting the stored tokens. " +
|
|
59
|
+
"Use this to switch accounts or reset a broken session. " +
|
|
60
|
+
"After logout, call ah_login to authenticate again.",
|
|
61
|
+
}, async () => (await ctx.session.logout())
|
|
62
|
+
? text("Logged out. Call ah_login to authenticate again.")
|
|
63
|
+
: text("Already logged out (no active session)."));
|
|
64
|
+
addAuthedTool(server, ctx, {
|
|
65
|
+
name: "ah_get_member_profile",
|
|
66
|
+
title: "Albert Heijn: Member Profile",
|
|
67
|
+
kind: "readOnly",
|
|
68
|
+
output: { name: z.string(), email: z.string().optional(), bonus_card_number: z.string().optional() },
|
|
69
|
+
description: "Get your Albert Heijn member profile. " +
|
|
70
|
+
"Returns name, email (masked), and bonus_card_number (last 4 digits only).",
|
|
71
|
+
}, async (c) => {
|
|
72
|
+
const m = await getMember(c);
|
|
73
|
+
const card = m.bonusCardNumber;
|
|
74
|
+
return structured({
|
|
75
|
+
name: fullName(m),
|
|
76
|
+
email: maskEmail(m.email) || undefined,
|
|
77
|
+
bonus_card_number: card ? (card.length > 4 ? `****${card.slice(-4)}` : card) : undefined,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** "jan.jansen@gmail.com" → "jan…@gmail.com": enough to recognise the account. */
|
|
82
|
+
function maskEmail(email) {
|
|
83
|
+
const at = email.lastIndexOf("@");
|
|
84
|
+
if (at < 1)
|
|
85
|
+
return email ? "…" : "";
|
|
86
|
+
return `${email.slice(0, Math.min(3, at - 1))}…${email.slice(at)}`;
|
|
87
|
+
}
|
|
88
|
+
function fullName(m) {
|
|
89
|
+
return `${m.firstName} ${m.lastName}`.trim();
|
|
90
|
+
}
|
|
91
|
+
/** The logged-in member; on a 401, refreshes the token and retries once. */
|
|
92
|
+
async function currentMember(ctx) {
|
|
93
|
+
const c = await ctx.session.client();
|
|
94
|
+
try {
|
|
95
|
+
return await getMember(c);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (!isUnauthorized(err) || !c.token)
|
|
99
|
+
throw err;
|
|
100
|
+
return getMember(await ctx.session.refreshAfterRejection(c.token));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Member name, or "" on failure. */
|
|
104
|
+
async function memberName(ctx) {
|
|
105
|
+
try {
|
|
106
|
+
return fullName(await currentMember(ctx));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Opens url in the default browser; failures are ignored (the link is also returned). */
|
|
113
|
+
function openBrowser(url) {
|
|
114
|
+
const [cmd, args] = process.platform === "darwin"
|
|
115
|
+
? ["open", [url]]
|
|
116
|
+
: process.platform === "win32"
|
|
117
|
+
? ["rundll32", ["url.dll,FileProtocolHandler", url]]
|
|
118
|
+
: ["xdg-open", [url]];
|
|
119
|
+
try {
|
|
120
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => { }).unref();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Ignored.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=account.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.js","sourceRoot":"","sources":["../../src/tools/account.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { bonusGroupUrl, getBonusGroupProducts, getBonusPeriods, getBonusSection, getPreviouslyBoughtBonus, productUrl, } from "../ahapi/index.js";
|
|
3
|
+
import { log } from "../log.js";
|
|
4
|
+
import { addAuthedTool, nonFatal, orDefault, parallel, structured, withRetry, wrapError } from "./common.js";
|
|
5
|
+
import { productSummary, summarizeProduct } from "./views.js";
|
|
6
|
+
const period = z.enum(["current", "next"]).optional();
|
|
7
|
+
const offerView = z.object({
|
|
8
|
+
id: z.number().optional(),
|
|
9
|
+
bonus_segment_id: z.string().optional(),
|
|
10
|
+
title: z.string(),
|
|
11
|
+
url: z.string().optional(),
|
|
12
|
+
original_price: z.number().optional(),
|
|
13
|
+
bonus_price: z.number().optional(),
|
|
14
|
+
discount_percentage: z.number().optional(),
|
|
15
|
+
bonus_mechanism: z.string().optional(),
|
|
16
|
+
});
|
|
17
|
+
export function registerBonusTools(server, ctx) {
|
|
18
|
+
addAuthedTool(server, ctx, {
|
|
19
|
+
name: "ah_get_bonus_offers",
|
|
20
|
+
title: "Albert Heijn: Bonus Offers",
|
|
21
|
+
kind: "readOnly",
|
|
22
|
+
description: "Get Albert Heijn bonus/promotional offers. " +
|
|
23
|
+
"Use this (not ah_search_products) when the user asks what is on bonus/sale/discount. " +
|
|
24
|
+
"Returns this week's bonus by default; set period='next' for next week's bonus, " +
|
|
25
|
+
"which AH publishes a few days before the new bonus week starts. " +
|
|
26
|
+
"Supports optional keyword filter to find e.g. cheese on bonus: set query='kaas'. " +
|
|
27
|
+
"Set previously_bought=true for offers on products the user bought before (AH's 'Eerder gekocht'). " +
|
|
28
|
+
"Group deals (e.g. '2+1 gratis', 'Alle yoghurt 25% korting') have no id but a bonus_segment_id — " +
|
|
29
|
+
"pass that to ah_get_bonus_group_products to see the individual products in the group. " +
|
|
30
|
+
"Returns id, bonus_segment_id, title, original_price, bonus_price, discount_percentage, bonus_mechanism.",
|
|
31
|
+
input: {
|
|
32
|
+
limit: z.number().int().optional().describe("Maximum number of results to return (default 20)"),
|
|
33
|
+
query: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Optional keyword filter (Dutch or English) applied client-side, e.g. 'kaas', 'vlees', 'bier'"),
|
|
37
|
+
period: period.describe("Which bonus week to return: 'current' (default) or 'next'"),
|
|
38
|
+
previously_bought: z
|
|
39
|
+
.boolean()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Only offers on products the user bought before"),
|
|
42
|
+
},
|
|
43
|
+
output: { offers: z.array(offerView) },
|
|
44
|
+
}, async (c, args) => {
|
|
45
|
+
const p = await bonusPeriod(ctx, c, args.period);
|
|
46
|
+
if (typeof p === "string")
|
|
47
|
+
return structured({ offers: [] }, p);
|
|
48
|
+
const limit = orDefault(args.limit, 20);
|
|
49
|
+
const query = (args.query ?? "").toLowerCase();
|
|
50
|
+
const offers = [];
|
|
51
|
+
const add = (o) => {
|
|
52
|
+
if (offers.length >= limit || (query && !o.title.toLowerCase().includes(query)))
|
|
53
|
+
return;
|
|
54
|
+
const discount = o.was && o.now ? Math.round((1 - o.now / o.was) * 100) : undefined;
|
|
55
|
+
offers.push({
|
|
56
|
+
id: o.id || undefined,
|
|
57
|
+
bonus_segment_id: o.segment || undefined,
|
|
58
|
+
title: o.title,
|
|
59
|
+
url: o.id ? productUrl(o.id, o.title) : o.segment ? bonusGroupUrl(o.segment) : undefined,
|
|
60
|
+
original_price: o.was || undefined,
|
|
61
|
+
// Absent for deals like "2 voor 1.19"; bonus_mechanism describes those.
|
|
62
|
+
bonus_price: o.now || undefined,
|
|
63
|
+
discount_percentage: discount || undefined,
|
|
64
|
+
bonus_mechanism: o.mechanism || undefined,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const all = args.previously_bought ? await getPreviouslyBoughtBonus(c, p.start) : await bonusOffers(ctx, c, p);
|
|
68
|
+
for (const bo of all) {
|
|
69
|
+
const products = bo.product ? [bo.product] : (bo.group?.products ?? []);
|
|
70
|
+
for (const pr of products) {
|
|
71
|
+
add({ id: pr.webshopId, title: pr.title, was: pr.priceBeforeBonus, now: pr.currentPrice, mechanism: pr.bonusMechanism });
|
|
72
|
+
}
|
|
73
|
+
if (bo.group && !bo.group.products?.length) {
|
|
74
|
+
const g = bo.group;
|
|
75
|
+
add({ segment: g.id, title: g.segmentDescription, was: g.exampleFromPrice, now: g.exampleForPrice, mechanism: g.discountDescription });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return structured({ offers });
|
|
79
|
+
});
|
|
80
|
+
addAuthedTool(server, ctx, {
|
|
81
|
+
name: "ah_get_bonus_group_products",
|
|
82
|
+
title: "Albert Heijn: Bonus Group Products",
|
|
83
|
+
kind: "readOnly",
|
|
84
|
+
description: "Get all individual products belonging to a specific Albert Heijn bonus promotion group. " +
|
|
85
|
+
"Use this to drill into a deal like '2+1 gratis kaas' or 'Alle yoghurt 25% korting'. " +
|
|
86
|
+
"Get segment_id from the bonus_segment_id field in ah_get_bonus_offers results. " +
|
|
87
|
+
"For an offer from next week's bonus, set period='next'. " +
|
|
88
|
+
"Returns the same fields as ah_search_products.",
|
|
89
|
+
input: {
|
|
90
|
+
segment_id: z.string().describe("Bonus segment ID from the bonus_segment_id field in ah_get_bonus_offers"),
|
|
91
|
+
period: period.describe("Bonus week of the offer: 'current' (default) or 'next'"),
|
|
92
|
+
},
|
|
93
|
+
output: { products: z.array(productSummary) },
|
|
94
|
+
}, async (c, args) => {
|
|
95
|
+
if (!args.segment_id)
|
|
96
|
+
throw new Error("segment_id is required");
|
|
97
|
+
const p = await bonusPeriod(ctx, c, args.period);
|
|
98
|
+
if (typeof p === "string")
|
|
99
|
+
return structured({ products: [] }, p);
|
|
100
|
+
return structured({ products: (await getBonusGroupProducts(c, args.segment_id, p)).map(summarizeProduct) });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** The requested period, or a message if next week's bonus is not published yet. */
|
|
104
|
+
async function bonusPeriod(ctx, c, name) {
|
|
105
|
+
const periods = await ctx.bonusPeriods.getOrLoad("", () => getBonusPeriods(c));
|
|
106
|
+
const current = periods[0];
|
|
107
|
+
if (!current?.start)
|
|
108
|
+
throw new Error("AH returned no current bonus period");
|
|
109
|
+
if (name !== "next")
|
|
110
|
+
return current;
|
|
111
|
+
if (periods[1]?.start)
|
|
112
|
+
return periods[1];
|
|
113
|
+
const when = current.nextVisibleFrom
|
|
114
|
+
? `It becomes visible on ${current.nextVisibleFrom}.`
|
|
115
|
+
: "It usually appears a few days before the new bonus week starts.";
|
|
116
|
+
return `Next week's bonus has not been published by Albert Heijn yet. ${when} This week's bonus runs until ${current.end}.`;
|
|
117
|
+
}
|
|
118
|
+
/** All national offers of a period, cached. Failed categories are skipped; partial results aren't cached. */
|
|
119
|
+
async function bonusOffers(ctx, c, p) {
|
|
120
|
+
const cached = ctx.bonuses.get(p.start);
|
|
121
|
+
if (cached)
|
|
122
|
+
return cached;
|
|
123
|
+
const sections = new Array(p.categories.length);
|
|
124
|
+
const errors = [];
|
|
125
|
+
await parallel(p.categories.length, 4, async (i) => {
|
|
126
|
+
const category = p.categories[i];
|
|
127
|
+
sections[i] = await nonFatal(() => withRetry("ah_get_bonus_offers", () => getBonusSection(c, category, p.start)), (err) => {
|
|
128
|
+
errors.push(err);
|
|
129
|
+
log.warn("skipping bonus category", { category, err });
|
|
130
|
+
return undefined;
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
if (p.categories.length > 0 && errors.length === p.categories.length) {
|
|
134
|
+
throw wrapError("failed to get bonus offers", errors[0]);
|
|
135
|
+
}
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const offers = [];
|
|
138
|
+
for (const o of sections.flat()) {
|
|
139
|
+
if (!o)
|
|
140
|
+
continue;
|
|
141
|
+
const key = o.product ? `p${o.product.webshopId}` : o.group ? `g${o.group.id}` : "";
|
|
142
|
+
if (key && !seen.has(key)) {
|
|
143
|
+
seen.add(key);
|
|
144
|
+
offers.push(o);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (errors.length === 0)
|
|
148
|
+
ctx.bonuses.set(p.start, offers);
|
|
149
|
+
return offers;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=bonus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bonus.js","sourceRoot":"","sources":["../../src/tools/bonus.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Entries kept at most; the oldest are evicted first. */
|
|
2
|
+
const MAX_ENTRIES = 1000;
|
|
3
|
+
/** In-memory cache with a fixed TTL. */
|
|
4
|
+
export class TtlCache {
|
|
5
|
+
#ttlMs;
|
|
6
|
+
/** Insertion order is expiry order: every entry has the same TTL and set() re-inserts. */
|
|
7
|
+
#entries = new Map();
|
|
8
|
+
constructor(ttlMs) {
|
|
9
|
+
this.#ttlMs = ttlMs;
|
|
10
|
+
}
|
|
11
|
+
/** undefined if missing or expired; use has() to detect a cached undefined. */
|
|
12
|
+
get(key) {
|
|
13
|
+
return this.has(key) ? this.#entries.get(key)?.value : undefined;
|
|
14
|
+
}
|
|
15
|
+
has(key) {
|
|
16
|
+
const e = this.#entries.get(key);
|
|
17
|
+
if (!e)
|
|
18
|
+
return false;
|
|
19
|
+
if (Date.now() > e.expiresAt) {
|
|
20
|
+
this.#entries.delete(key);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
set(key, value) {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
this.#entries.delete(key);
|
|
28
|
+
for (const [k, e] of this.#entries) {
|
|
29
|
+
if (now <= e.expiresAt && this.#entries.size < MAX_ENTRIES)
|
|
30
|
+
break;
|
|
31
|
+
this.#entries.delete(k);
|
|
32
|
+
}
|
|
33
|
+
this.#entries.set(key, { value, expiresAt: now + this.#ttlMs });
|
|
34
|
+
}
|
|
35
|
+
/** The cached value, or load()'s result, which is then cached. */
|
|
36
|
+
async getOrLoad(key, load) {
|
|
37
|
+
if (this.has(key))
|
|
38
|
+
return this.get(key);
|
|
39
|
+
const value = await load();
|
|
40
|
+
this.set(key, value);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/tools/cache.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,MAAM,WAAW,GAAG,IAAI,CAAC;AAEzB,wCAAwC;AACxC,MAAM,OAAO,QAAQ;IACV,MAAM,CAAS;IACxB,0FAA0F;IACjF,QAAQ,GAAG,IAAI,GAAG,EAA2C,CAAC;IAEvE,YAAY,KAAa;QACvB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,+EAA+E;IAC/E,GAAG,CAAC,GAAW;QACb,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IACnE,CAAC;IAED,GAAG,CAAC,GAAW;QACb,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;QACrB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC;YAC7B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1B,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAQ;QACvB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,IAAI,GAAG,IAAI,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,WAAW;gBAAE,MAAM;YAClE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC1B,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,kEAAkE;IAClE,KAAK,CAAC,SAAS,CAAC,GAAW,EAAE,IAAsB;QACjD,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAM,CAAC;QAC7C,MAAM,KAAK,GAAG,MAAM,IAAI,EAAE,CAAC;QAC3B,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACrB,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// "Cart" tools act on the active delivery order. Before a delivery slot is
|
|
2
|
+
// chosen, the AH app's basket is the shopping list.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getActiveOrder, getDeliverySlots, isNoActiveOrder, setOrderItems, } from "../ahapi/index.js";
|
|
5
|
+
import { addAuthedTool, cachedMember, formatDate, formatTime, structured, orDefault, text, wrapError, } from "./common.js";
|
|
6
|
+
import { confirmInput } from "./shoppingList.js";
|
|
7
|
+
import { orderView, viewOrder } from "./views.js";
|
|
8
|
+
/** AH accepts changes only to an active order, which its API can't create. */
|
|
9
|
+
const NO_ACTIVE_ORDER = "there is no active delivery order to change: " +
|
|
10
|
+
"to collect products, use the shopping list (ah_add_to_shopping_list); " +
|
|
11
|
+
"choosing a delivery slot in the AH app or on ah.nl turns the list into an order";
|
|
12
|
+
/** undefined if there is no active order. */
|
|
13
|
+
async function activeOrder(c) {
|
|
14
|
+
try {
|
|
15
|
+
return await getActiveOrder(c);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
if (isNoActiveOrder(err))
|
|
19
|
+
return undefined;
|
|
20
|
+
throw wrapError("failed to get active order", err);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function registerCartTools(server, ctx) {
|
|
24
|
+
addAuthedTool(server, ctx, {
|
|
25
|
+
name: "ah_get_cart",
|
|
26
|
+
title: "Albert Heijn: View Order Cart",
|
|
27
|
+
kind: "readOnly",
|
|
28
|
+
output: { order: orderView.optional() },
|
|
29
|
+
description: "View the items in the active Albert Heijn delivery order (the order cart). " +
|
|
30
|
+
"An order only exists after the user has chosen a delivery or pick-up slot; " +
|
|
31
|
+
"the products the user is collecting before that are on the shopping list (use ah_get_shopping_list). " +
|
|
32
|
+
"Returns the order state, items with names and quantities, total price, and total discount.",
|
|
33
|
+
}, async (c) => {
|
|
34
|
+
const order = await activeOrder(c);
|
|
35
|
+
if (!order) {
|
|
36
|
+
return structured({}, "There is no active delivery order, so the order cart is empty. " +
|
|
37
|
+
"Products the user is collecting are on the shopping list (ah_get_shopping_list).");
|
|
38
|
+
}
|
|
39
|
+
return structured({ order: viewOrder(order) });
|
|
40
|
+
});
|
|
41
|
+
addAuthedTool(server, ctx, {
|
|
42
|
+
name: "ah_update_cart_item",
|
|
43
|
+
title: "Albert Heijn: Update Order Item",
|
|
44
|
+
kind: "destructive",
|
|
45
|
+
description: "Set the quantity of a product in the active Albert Heijn delivery order. " +
|
|
46
|
+
"Requires an order (started by choosing a delivery slot in the AH app or on ah.nl, which moves the " +
|
|
47
|
+
"shopping list into it); to collect products before that, use ah_add_to_shopping_list. " +
|
|
48
|
+
"Use product_id from ah_search_products or ah_get_cart. " +
|
|
49
|
+
"Set quantity=0 to remove the item (or use ah_remove_from_cart).",
|
|
50
|
+
input: {
|
|
51
|
+
product_id: z.number().int().describe("Numeric product ID"),
|
|
52
|
+
quantity: z.number().int().describe("New quantity (0 removes the item)"),
|
|
53
|
+
},
|
|
54
|
+
}, async (c, { product_id, quantity }) => {
|
|
55
|
+
if (product_id <= 0)
|
|
56
|
+
throw new Error("product_id is required");
|
|
57
|
+
if (quantity < 0)
|
|
58
|
+
throw new Error("quantity must be >= 0");
|
|
59
|
+
const order = await activeOrder(c);
|
|
60
|
+
if (!order)
|
|
61
|
+
throw new Error(NO_ACTIVE_ORDER);
|
|
62
|
+
await setOrderItems(c, order.id, new Map([[product_id, quantity]]));
|
|
63
|
+
return text(quantity === 0
|
|
64
|
+
? `Product ${product_id} removed from cart.`
|
|
65
|
+
: `Product ${product_id} quantity set to ${quantity}.`);
|
|
66
|
+
});
|
|
67
|
+
addAuthedTool(server, ctx, {
|
|
68
|
+
name: "ah_remove_from_cart",
|
|
69
|
+
title: "Albert Heijn: Remove from Order",
|
|
70
|
+
kind: "destructive",
|
|
71
|
+
description: "Remove a single product from the active Albert Heijn delivery order. " +
|
|
72
|
+
"Use product_id from ah_get_cart. To remove products from the shopping list, use ah_remove_from_shopping_list.",
|
|
73
|
+
input: { product_id: z.number().int().describe("Numeric product ID to remove") },
|
|
74
|
+
}, async (c, { product_id }) => {
|
|
75
|
+
if (product_id <= 0)
|
|
76
|
+
throw new Error("product_id is required");
|
|
77
|
+
const order = await activeOrder(c);
|
|
78
|
+
if (!order)
|
|
79
|
+
return text("Your cart is empty: there is no active order.");
|
|
80
|
+
await setOrderItems(c, order.id, new Map([[product_id, 0]]));
|
|
81
|
+
return text(`Product ${product_id} removed from cart.`);
|
|
82
|
+
});
|
|
83
|
+
addAuthedTool(server, ctx, {
|
|
84
|
+
name: "ah_clear_cart",
|
|
85
|
+
title: "Albert Heijn: Clear Order",
|
|
86
|
+
kind: "destructive",
|
|
87
|
+
description: "Remove ALL items from the active Albert Heijn delivery order (not the shopping list). " +
|
|
88
|
+
'Irreversible — requires confirm="yes" to prevent accidental use.',
|
|
89
|
+
input: confirmInput,
|
|
90
|
+
}, async (c, { confirm }) => {
|
|
91
|
+
if (confirm !== "yes")
|
|
92
|
+
throw new Error('confirm must be "yes" to clear the cart');
|
|
93
|
+
const order = await activeOrder(c);
|
|
94
|
+
if (!order || order.items.length === 0)
|
|
95
|
+
return text("Your cart is already empty.");
|
|
96
|
+
await setOrderItems(c, order.id, new Map(order.items.map((it) => [it.productId, 0])));
|
|
97
|
+
return text("Shopping cart cleared.");
|
|
98
|
+
});
|
|
99
|
+
addAuthedTool(server, ctx, {
|
|
100
|
+
name: "ah_get_delivery_slots",
|
|
101
|
+
title: "Albert Heijn: Delivery Slots",
|
|
102
|
+
kind: "readOnly",
|
|
103
|
+
output: {
|
|
104
|
+
postal_code: z.string(),
|
|
105
|
+
days: z.array(z.object({ date: z.string(), windows: z.array(z.string()) })),
|
|
106
|
+
},
|
|
107
|
+
description: "List the delivery time windows Albert Heijn offers at the member's address, per day (Dutch time). " +
|
|
108
|
+
"Booking a slot is only possible in the AH app or on ah.nl; this shows when delivery is possible. " +
|
|
109
|
+
"Returns date and windows like '15:00-17:00'.",
|
|
110
|
+
input: {
|
|
111
|
+
days: z.number().int().optional().describe("Number of days with slots to return (default 7)"),
|
|
112
|
+
},
|
|
113
|
+
}, async (c, args) => {
|
|
114
|
+
const { address } = await cachedMember(ctx, c);
|
|
115
|
+
if (!address)
|
|
116
|
+
throw new Error("the member profile has no delivery address; add one in the AH app");
|
|
117
|
+
const days = (await getDeliverySlots(c, address)).filter((d) => d.slots.length > 0);
|
|
118
|
+
return structured({
|
|
119
|
+
postal_code: address.postalCode,
|
|
120
|
+
days: days.slice(0, orDefault(args.days, 7)).map((d) => ({
|
|
121
|
+
date: formatDate(d.date, false),
|
|
122
|
+
windows: d.slots.map((s) => `${formatTime(s.start)}-${formatTime(s.end)}`),
|
|
123
|
+
})),
|
|
124
|
+
}, days.length ? undefined : `AH offers no delivery slots for ${address.postalCode} right now.`);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=cart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart.js","sourceRoot":"","sources":["../../src/tools/cart.ts"],"names":[],"mappings":"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"}
|