continentept-mcp 0.3.1 → 0.5.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/.env.example +3 -0
- package/CHANGELOG.md +33 -1
- package/README.md +14 -2
- package/dist/cli.js +16 -9
- package/dist/clients/authentication.js +17 -5
- package/dist/clients/storefront.js +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -1
- package/dist/domain/models.d.ts +65 -17
- package/dist/domain/models.js +10 -6
- package/dist/domain/money.d.ts +19 -0
- package/dist/domain/money.js +25 -0
- package/dist/domain/session.d.ts +6 -0
- package/dist/domain/session.js +3 -0
- package/dist/mcp/contracts.d.ts +92 -21
- package/dist/mcp/contracts.js +3 -3
- package/dist/mcp/guide.d.ts +1 -1
- package/dist/mcp/guide.js +2 -2
- package/dist/mcp/shopping.d.ts +8 -2
- package/dist/mcp/shopping.js +52 -27
- package/dist/parsers/catalog.d.ts +13 -5
- package/dist/parsers/catalog.js +48 -45
- package/dist/parsers/product.js +7 -5
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +4 -2
- package/dist/services/catalog.d.ts +10 -4
- package/dist/services/catalog.js +3 -2
- package/dist/services/updates.d.ts +21 -0
- package/dist/services/updates.js +70 -0
- package/dist/session/browser.js +2 -1
- package/dist/session/login-errors.d.ts +12 -0
- package/dist/session/login-errors.js +25 -0
- package/docs/configuration.md +28 -5
- package/docs/tools.md +27 -11
- package/docs/troubleshooting.md +1 -1
- package/package.json +5 -2
package/.env.example
CHANGED
|
@@ -9,6 +9,9 @@ CONTINENTE_PASSWORD=replace-with-your-password
|
|
|
9
9
|
# Close the browser after this many idle seconds; 0 keeps it open. Default: 300
|
|
10
10
|
# CONTINENTE_BROWSER_IDLE_SECONDS=300
|
|
11
11
|
|
|
12
|
+
# Set to 0 to stop the daily check for a newer release on npm.
|
|
13
|
+
# CONTINENTE_UPDATE_CHECK=1
|
|
14
|
+
|
|
12
15
|
# These two only work as environment variables in the MCP client configuration:
|
|
13
16
|
# CONTINENTE_CREDENTIALS_FILE=/private/path/credentials.env
|
|
14
17
|
# CONTINENTE_DATA_DIR=/private/path/continentept-mcp
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,36 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-10-15
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Searching for produce, meat and other products priced per kilogram failed with "Invalid money amount". Prices now record what they are for: per item, per piece, per kg or per litre.
|
|
14
|
+
- One unreadable search result no longer fails the whole search; the other results are returned as a partial result, and the unreadable one is described.
|
|
15
|
+
- Search results keep the website's relevance order. Products whose names merely resembled a favorite no longer jump ahead of better matches; exact favorites still come first.
|
|
16
|
+
- Favorites priced per kilogram are read correctly, and an unrecognised favorite price is reported as unknown instead of failing the refresh.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Breaking:** catalogue prices (`price` in search, favorites and `get_product`) are now `{ amount, per }`. `unitPriceText` is replaced by `secondaryPrice`, and the search field `similarity` is removed. Saved favorites are fetched again once after updating.
|
|
21
|
+
- Search results include `packageText`, the pack size as shown on the website.
|
|
22
|
+
- Sign-in errors say whether no credentials are configured or Continente did not accept them, with the next step for each case. The instructions present the `basket-planner` prompt as optional, for clients that support prompts.
|
|
23
|
+
- Browser engine updated to Playwright 1.64 (Chrome 156). After updating, the new Chromium downloads automatically on first use (about 150 MB, once).
|
|
24
|
+
|
|
25
|
+
## [0.4.0] - 2026-10-15
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Update notice: once a day the server checks npm for a newer release and, if there is one, tells the assistant and reports it in `get_session_status`. Offline-safe; turn it off with `CONTINENTE_UPDATE_CHECK=0`.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Documentation: how the home folder is chosen on Windows, what "readable only by your user" covers, when the favorites snapshot is refetched, and what deleting the data directory removes.
|
|
34
|
+
|
|
35
|
+
### Security
|
|
36
|
+
|
|
37
|
+
- A failed sign-in could write the account password or session cookies to the server log, because browser automation errors include what was typed and the request headers. Sign-in errors are now reported as fixed messages naming only the failed step.
|
|
38
|
+
|
|
9
39
|
## [0.3.1] - 2026-10-15
|
|
10
40
|
|
|
11
41
|
### Added
|
|
@@ -41,7 +71,9 @@ All notable changes to this project are documented here. The format follows
|
|
|
41
71
|
- Automatic sign-in from environment variables or a private credentials file, and a `login` command.
|
|
42
72
|
- The `basket-planner` skill for planning a shopping list and updating the basket.
|
|
43
73
|
|
|
44
|
-
[Unreleased]: https://github.com/andredelgado/continente-mcp/compare/v0.
|
|
74
|
+
[Unreleased]: https://github.com/andredelgado/continente-mcp/compare/v0.5.0...HEAD
|
|
75
|
+
[0.5.0]: https://github.com/andredelgado/continente-mcp/compare/v0.4.0...v0.5.0
|
|
76
|
+
[0.4.0]: https://github.com/andredelgado/continente-mcp/compare/v0.3.1...v0.4.0
|
|
45
77
|
[0.3.1]: https://github.com/andredelgado/continente-mcp/compare/v0.3.0...v0.3.1
|
|
46
78
|
[0.3.0]: https://github.com/andredelgado/continente-mcp/compare/v0.2.0...v0.3.0
|
|
47
79
|
[0.2.0]: https://github.com/andredelgado/continente-mcp/compare/v0.1.0...v0.2.0
|
package/README.md
CHANGED
|
@@ -22,7 +22,8 @@ on the website.
|
|
|
22
22
|
cannot be confirmed, the result says so and tells the assistant to check the cart instead
|
|
23
23
|
of retrying.
|
|
24
24
|
- **Your account**: favorites, order history and a ranking of the products you buy most.
|
|
25
|
-
- **Runs locally** over stdio.
|
|
25
|
+
- **Runs locally** over stdio. Your credentials and session are stored on your computer and
|
|
26
|
+
sent only to Continente.pt.
|
|
26
27
|
- **Built-in guidance.** The server tells the assistant the cart rules as soon as it
|
|
27
28
|
connects, and offers a `basket-planner` prompt with the full shopping workflow. Nothing
|
|
28
29
|
extra to install; the same guide also ships as a [skill](skills/basket-planner/SKILL.md).
|
|
@@ -44,11 +45,22 @@ it needs it (about 150 MB, once).
|
|
|
44
45
|
3. Enter your Continente email and password when asked. Claude Desktop keeps the password
|
|
45
46
|
in its secure settings storage.
|
|
46
47
|
|
|
47
|
-
That's it. The
|
|
48
|
+
That's it. The server is also listed
|
|
48
49
|
in the [MCP Registry](https://registry.modelcontextprotocol.io) as
|
|
49
50
|
`io.github.andredelgado/continente-mcp`, so clients that browse the registry can find it. To use 1Password instead
|
|
50
51
|
of storing the password in Claude Desktop, use the manual setup below.
|
|
51
52
|
|
|
53
|
+
### Updates
|
|
54
|
+
|
|
55
|
+
The server checks npm once a day for a newer release. When there is one, it tells the
|
|
56
|
+
assistant, so Claude can mention it, and `get_session_status` shows it.
|
|
57
|
+
|
|
58
|
+
- **Extension:** download the new `continente-mcp.mcpb` and open it. Claude Desktop updates the
|
|
59
|
+
extension in place and keeps your email and password.
|
|
60
|
+
- **npx setups:** using `continentept-mcp@latest` picks up new versions when the client restarts.
|
|
61
|
+
- **Release emails:** on [GitHub](https://github.com/andredelgado/continente-mcp), choose
|
|
62
|
+
**Watch → Custom → Releases**.
|
|
63
|
+
|
|
52
64
|
## Manual setup (any MCP client)
|
|
53
65
|
|
|
54
66
|
**1. Save your credentials** in `~/.continentept-mcp/credentials.env` (see
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,9 @@ import { createShoppingApplication } from './runtime.js';
|
|
|
5
5
|
import { runAutoLogin } from './auto-login.js';
|
|
6
6
|
import { isMainModule } from './main-module.js';
|
|
7
7
|
import { runChromiumInstall } from './session/chromium.js';
|
|
8
|
+
import { safeLoginMessage } from './session/login-errors.js';
|
|
9
|
+
import { checkForUpdate } from './services/updates.js';
|
|
10
|
+
import { loadSettings } from './config.js';
|
|
8
11
|
const USAGE = `Usage: continentept-mcp [command]
|
|
9
12
|
|
|
10
13
|
Commands:
|
|
@@ -17,16 +20,20 @@ Options:
|
|
|
17
20
|
-h, --help Show this help
|
|
18
21
|
-v, --version Show the version
|
|
19
22
|
`;
|
|
20
|
-
function
|
|
23
|
+
function packageMetadata() {
|
|
21
24
|
const metadata = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
22
|
-
if (typeof metadata !== 'object' || metadata === null || !('version' in metadata) ||
|
|
23
|
-
typeof metadata.version !== 'string')
|
|
24
|
-
throw new Error('Missing package
|
|
25
|
-
return metadata.version;
|
|
25
|
+
if (typeof metadata !== 'object' || metadata === null || !('version' in metadata) || !('name' in metadata) ||
|
|
26
|
+
typeof metadata.version !== 'string' || typeof metadata.name !== 'string')
|
|
27
|
+
throw new Error('Missing package metadata');
|
|
28
|
+
return { name: metadata.name, version: metadata.version };
|
|
26
29
|
}
|
|
27
30
|
export async function startStdioServer() {
|
|
28
|
-
const version =
|
|
29
|
-
const
|
|
31
|
+
const { name, version } = packageMetadata();
|
|
32
|
+
const settings = loadSettings();
|
|
33
|
+
const update = await checkForUpdate({ currentVersion: version, packageName: name, dataDir: settings.dataDir, enabled: settings.updateCheck });
|
|
34
|
+
if (update.available)
|
|
35
|
+
console.error(`${name} ${update.latestVersion} is available (running ${version}).`);
|
|
36
|
+
const application = createShoppingApplication({ version, update });
|
|
30
37
|
await connectStdioApplication(application);
|
|
31
38
|
console.error(`continentept-mcp ${version} ready on stdio`);
|
|
32
39
|
return application;
|
|
@@ -67,14 +74,14 @@ export async function main(args) {
|
|
|
67
74
|
return (await runAutoLogin({ showBrowser: rest.includes('--show-browser') })) ? 0 : 1;
|
|
68
75
|
}
|
|
69
76
|
catch (error) {
|
|
70
|
-
console.error(
|
|
77
|
+
console.error(safeLoginMessage(error));
|
|
71
78
|
return 1;
|
|
72
79
|
}
|
|
73
80
|
case 'install-browser':
|
|
74
81
|
return runChromiumInstall(rest);
|
|
75
82
|
case '-v':
|
|
76
83
|
case '--version':
|
|
77
|
-
console.log(
|
|
84
|
+
console.log(packageMetadata().version);
|
|
78
85
|
return 0;
|
|
79
86
|
case '-h':
|
|
80
87
|
case '--help':
|
|
@@ -5,6 +5,7 @@ import { STOREFRONT_ORIGIN } from '../parsers/catalog.js';
|
|
|
5
5
|
import { isLoginUrl, responseStatus } from './policy.js';
|
|
6
6
|
import { CART_SUMMARY_URL } from './storefront.js';
|
|
7
7
|
import { requestIO, acquireResource, checkRequest } from '../session/request.js';
|
|
8
|
+
import { LoginFailed, loginFailure } from '../session/login-errors.js';
|
|
8
9
|
const LOGIN_ORIGIN = 'https://login.continente.pt';
|
|
9
10
|
const EMAIL_INPUT = '#userNameCC';
|
|
10
11
|
const PASSWORD_INPUT = '#password_input';
|
|
@@ -59,32 +60,43 @@ export class AuthenticationClient {
|
|
|
59
60
|
this.logger('Login skipped: CONTINENTE_EMAIL and CONTINENTE_PASSWORD are required.');
|
|
60
61
|
return false;
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
let step = 'starting the browser';
|
|
64
|
+
let browser;
|
|
63
65
|
let closing;
|
|
64
|
-
const close = () => closing ??= browser
|
|
66
|
+
const close = () => closing ??= browser?.close() ?? Promise.resolve();
|
|
65
67
|
try {
|
|
68
|
+
const launched = browser = await acquireResource(() => this.launch({ headless: !this.options.showBrowser }), opened => opened.close());
|
|
66
69
|
return await requestIO(async () => {
|
|
67
|
-
const context = await acquireResource(() =>
|
|
70
|
+
const context = await acquireResource(() => launched.newContext({ userAgent: browserUserAgent(launched.version()), locale: 'pt-PT' }), context => context.close());
|
|
68
71
|
const page = await acquireResource(() => context.newPage(), page => page.close());
|
|
69
72
|
this.logger('Signing in.');
|
|
73
|
+
step = 'opening the login page';
|
|
70
74
|
await page.goto(`${STOREFRONT_ORIGIN}/login/`, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
71
75
|
await page.getByRole('button', { name: CONSENT_BUTTON }).click({ timeout: 5000 }).catch(() => { });
|
|
72
76
|
const frame = await this.loginFrame(page);
|
|
77
|
+
step = 'entering the email';
|
|
73
78
|
await this.submit(frame, EMAIL_INPUT, email);
|
|
74
79
|
const leftLogin = page.waitForURL(url => url.origin === STOREFRONT_ORIGIN && !isLoginUrl(url.href), { timeout: 20000 }).catch(() => { });
|
|
80
|
+
step = 'entering the password';
|
|
75
81
|
await this.submit(frame, PASSWORD_INPUT, password);
|
|
76
82
|
await leftLogin;
|
|
77
83
|
checkRequest();
|
|
84
|
+
step = 'checking the new session';
|
|
78
85
|
if (!(await this.isSignedIn(context)))
|
|
79
|
-
throw new
|
|
86
|
+
throw new LoginFailed('Sign-in failed: the website did not accept the login.');
|
|
87
|
+
step = 'saving the session';
|
|
80
88
|
const cookies = SessionCookiesSchema.safeParse(await context.cookies());
|
|
81
89
|
if (!cookies.success)
|
|
82
|
-
throw new
|
|
90
|
+
throw new LoginFailed('Sign-in failed: the website returned an unexpected session.');
|
|
83
91
|
await this.cookies.save(cookies.data);
|
|
84
92
|
this.logger('Signed in; session saved.');
|
|
85
93
|
return true;
|
|
86
94
|
}, close);
|
|
87
95
|
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
// Playwright messages from these steps can contain the typed password or Cookie headers.
|
|
98
|
+
throw loginFailure(step, error);
|
|
99
|
+
}
|
|
88
100
|
finally {
|
|
89
101
|
await close();
|
|
90
102
|
}
|
|
@@ -93,7 +93,7 @@ export class CatalogClient {
|
|
|
93
93
|
break;
|
|
94
94
|
}
|
|
95
95
|
const previousCount = ids.size;
|
|
96
|
-
parsed.value.forEach(product => ids.add(product.productId));
|
|
96
|
+
parsed.value.products.forEach(product => ids.add(product.productId));
|
|
97
97
|
pages.push(html);
|
|
98
98
|
reportProgress(pages.length, 5, 'Reading catalogue pages');
|
|
99
99
|
const continuation = parseSearchPagination(html);
|
package/dist/config.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface Settings {
|
|
|
9
9
|
readonly favoritesFile: string;
|
|
10
10
|
/** Undefined keeps the session default. */
|
|
11
11
|
readonly browserIdleMs: number | undefined;
|
|
12
|
+
/** Check npm once a day for a newer release. */
|
|
13
|
+
readonly updateCheck: boolean;
|
|
12
14
|
}
|
|
13
15
|
/** Read `KEY=value` lines. Surrounding quotes are removed; nothing is expanded or unescaped. */
|
|
14
16
|
export declare function readDotEnv(text: string): Record<string, string>;
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
const FILE_KEYS = new Set(['CONTINENTE_EMAIL', 'CONTINENTE_PASSWORD', 'CONTINENTE_BROWSER_IDLE_SECONDS']);
|
|
5
|
+
const FILE_KEYS = new Set(['CONTINENTE_EMAIL', 'CONTINENTE_PASSWORD', 'CONTINENTE_BROWSER_IDLE_SECONDS', 'CONTINENTE_UPDATE_CHECK']);
|
|
6
6
|
const ASSIGNMENT = /^(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/;
|
|
7
7
|
/** Read `KEY=value` lines. Surrounding quotes are removed; nothing is expanded or unescaped. */
|
|
8
8
|
export function readDotEnv(text) {
|
|
@@ -52,6 +52,7 @@ export function loadSettings(env = process.env) {
|
|
|
52
52
|
cookieFile: join(dataDir, 'session.json'),
|
|
53
53
|
favoritesFile: join(dataDir, 'favorites.json'),
|
|
54
54
|
browserIdleMs: Number.isFinite(idleSeconds) && idleSeconds >= 0 ? idleSeconds * 1000 : undefined,
|
|
55
|
+
updateCheck: !['0', 'false', 'off', 'no'].includes(value('CONTINENTE_UPDATE_CHECK')?.trim().toLowerCase() ?? ''),
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
export function hasCredentials(settings) {
|
package/dist/domain/models.d.ts
CHANGED
|
@@ -4,8 +4,16 @@ export declare const FavoriteProductSchema: z.ZodObject<{
|
|
|
4
4
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
5
5
|
name: z.ZodString;
|
|
6
6
|
price: z.ZodNullable<z.ZodObject<{
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
amount: z.ZodObject<{
|
|
8
|
+
currency: z.ZodLiteral<"EUR">;
|
|
9
|
+
minorUnits: z.ZodNumber;
|
|
10
|
+
}, z.core.$strict>;
|
|
11
|
+
per: z.ZodEnum<{
|
|
12
|
+
item: "item";
|
|
13
|
+
kg: "kg";
|
|
14
|
+
l: "l";
|
|
15
|
+
piece: "piece";
|
|
16
|
+
}>;
|
|
9
17
|
}, z.core.$strict>>;
|
|
10
18
|
url: z.ZodURL;
|
|
11
19
|
}, z.core.$strict>;
|
|
@@ -15,11 +23,31 @@ export declare const ProductSchema: z.ZodObject<{
|
|
|
15
23
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
16
24
|
name: z.ZodString;
|
|
17
25
|
price: z.ZodNullable<z.ZodObject<{
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
amount: z.ZodObject<{
|
|
27
|
+
currency: z.ZodLiteral<"EUR">;
|
|
28
|
+
minorUnits: z.ZodNumber;
|
|
29
|
+
}, z.core.$strict>;
|
|
30
|
+
per: z.ZodEnum<{
|
|
31
|
+
item: "item";
|
|
32
|
+
kg: "kg";
|
|
33
|
+
l: "l";
|
|
34
|
+
piece: "piece";
|
|
35
|
+
}>;
|
|
20
36
|
}, z.core.$strict>>;
|
|
21
37
|
url: z.ZodURL;
|
|
22
|
-
|
|
38
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
39
|
+
amount: z.ZodObject<{
|
|
40
|
+
currency: z.ZodLiteral<"EUR">;
|
|
41
|
+
minorUnits: z.ZodNumber;
|
|
42
|
+
}, z.core.$strict>;
|
|
43
|
+
per: z.ZodEnum<{
|
|
44
|
+
item: "item";
|
|
45
|
+
kg: "kg";
|
|
46
|
+
l: "l";
|
|
47
|
+
piece: "piece";
|
|
48
|
+
}>;
|
|
49
|
+
}, z.core.$strict>>;
|
|
50
|
+
packageText: z.ZodNullable<z.ZodString>;
|
|
23
51
|
minimumText: z.ZodNullable<z.ZodString>;
|
|
24
52
|
}, z.core.$strict>;
|
|
25
53
|
export type Product = z.infer<typeof ProductSchema>;
|
|
@@ -227,7 +255,7 @@ export declare const PurchaseFrequencySchema: z.ZodObject<{
|
|
|
227
255
|
}, z.core.$strict>;
|
|
228
256
|
export type PurchaseFrequency = z.infer<typeof PurchaseFrequencySchema>;
|
|
229
257
|
export declare const FavoritesCacheSchema: z.ZodObject<{
|
|
230
|
-
schemaVersion: z.ZodLiteral<
|
|
258
|
+
schemaVersion: z.ZodLiteral<2>;
|
|
231
259
|
accountScope: z.ZodString;
|
|
232
260
|
fetchedAt: z.ZodISODateTime;
|
|
233
261
|
favorites: z.ZodArray<z.ZodObject<{
|
|
@@ -235,8 +263,16 @@ export declare const FavoritesCacheSchema: z.ZodObject<{
|
|
|
235
263
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
236
264
|
name: z.ZodString;
|
|
237
265
|
price: z.ZodNullable<z.ZodObject<{
|
|
238
|
-
|
|
239
|
-
|
|
266
|
+
amount: z.ZodObject<{
|
|
267
|
+
currency: z.ZodLiteral<"EUR">;
|
|
268
|
+
minorUnits: z.ZodNumber;
|
|
269
|
+
}, z.core.$strict>;
|
|
270
|
+
per: z.ZodEnum<{
|
|
271
|
+
item: "item";
|
|
272
|
+
kg: "kg";
|
|
273
|
+
l: "l";
|
|
274
|
+
piece: "piece";
|
|
275
|
+
}>;
|
|
240
276
|
}, z.core.$strict>>;
|
|
241
277
|
url: z.ZodURL;
|
|
242
278
|
}, z.core.$strict>>;
|
|
@@ -248,18 +284,30 @@ export declare const ProductDetailsSchema: z.ZodObject<{
|
|
|
248
284
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
249
285
|
name: z.ZodString;
|
|
250
286
|
url: z.ZodURL;
|
|
251
|
-
price: z.ZodObject<{
|
|
252
|
-
amount: z.
|
|
287
|
+
price: z.ZodNullable<z.ZodObject<{
|
|
288
|
+
amount: z.ZodObject<{
|
|
253
289
|
currency: z.ZodLiteral<"EUR">;
|
|
254
290
|
minorUnits: z.ZodNumber;
|
|
255
|
-
}, z.core.$strict
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
291
|
+
}, z.core.$strict>;
|
|
292
|
+
per: z.ZodEnum<{
|
|
293
|
+
item: "item";
|
|
294
|
+
kg: "kg";
|
|
295
|
+
l: "l";
|
|
296
|
+
piece: "piece";
|
|
260
297
|
}>;
|
|
261
|
-
}, z.core.$strict
|
|
262
|
-
|
|
298
|
+
}, z.core.$strict>>;
|
|
299
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
300
|
+
amount: z.ZodObject<{
|
|
301
|
+
currency: z.ZodLiteral<"EUR">;
|
|
302
|
+
minorUnits: z.ZodNumber;
|
|
303
|
+
}, z.core.$strict>;
|
|
304
|
+
per: z.ZodEnum<{
|
|
305
|
+
item: "item";
|
|
306
|
+
kg: "kg";
|
|
307
|
+
l: "l";
|
|
308
|
+
piece: "piece";
|
|
309
|
+
}>;
|
|
310
|
+
}, z.core.$strict>>;
|
|
263
311
|
availability: z.ZodEnum<{
|
|
264
312
|
available: "available";
|
|
265
313
|
unavailable: "unavailable";
|
package/dist/domain/models.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ToolErrorSchema } from './errors.js';
|
|
3
3
|
import { CartItemIdSchema, OrderIdSchema, OrderNumberSchema, ProductIdSchema, ProductReferenceSchema } from './identity.js';
|
|
4
|
-
import { MoneySchema, QuotedMoneySchema } from './money.js';
|
|
4
|
+
import { MoneySchema, PriceTagSchema, QuotedMoneySchema } from './money.js';
|
|
5
5
|
import { ObservedQuantitySchema, QuantitySchema, ProductSaleRulesSchema } from './quantity.js';
|
|
6
6
|
const productDetails = {
|
|
7
7
|
name: z.string().trim().min(1).max(250),
|
|
8
|
-
price
|
|
8
|
+
/** The main price as displayed; null when absent or unrecognised. */
|
|
9
|
+
price: PriceTagSchema.nullable(),
|
|
9
10
|
url: z.url(),
|
|
10
11
|
};
|
|
11
12
|
export const FavoriteProductSchema = ProductReferenceSchema.safeExtend(productDetails);
|
|
12
13
|
export const ProductSchema = FavoriteProductSchema.safeExtend({
|
|
13
|
-
|
|
14
|
+
/** The comparison price shown next to the main one, such as per kg or per piece. */
|
|
15
|
+
secondaryPrice: PriceTagSchema.nullable(),
|
|
16
|
+
/** Pack size as displayed, such as `940 gr`, `2 x 52 gr` or `1,2 kg (aprox.)`. */
|
|
17
|
+
packageText: z.string().nullable(),
|
|
14
18
|
minimumText: z.string().nullable(),
|
|
15
19
|
});
|
|
16
20
|
export const CartItemSchema = z.strictObject({
|
|
@@ -51,15 +55,15 @@ export const PurchaseFrequencySchema = z.strictObject({
|
|
|
51
55
|
}).refine(frequency => (frequency.identityBasis === 'product') === (frequency.productId !== null), { message: 'Identity basis must agree with the available product ID' })
|
|
52
56
|
.refine(frequency => new Set(frequency.quantities.map(quantity => quantity.unit)).size === frequency.quantities.length, { message: 'Purchase totals must contain each unit only once' });
|
|
53
57
|
export const FavoritesCacheSchema = z.strictObject({
|
|
54
|
-
schemaVersion: z.literal(
|
|
58
|
+
schemaVersion: z.literal(2),
|
|
55
59
|
accountScope: z.string().trim().min(1).max(128),
|
|
56
60
|
fetchedAt: z.iso.datetime({ offset: true }),
|
|
57
61
|
favorites: z.array(FavoriteProductSchema),
|
|
58
62
|
completeness: z.literal('complete').optional(),
|
|
59
63
|
});
|
|
60
64
|
export const ProductDetailsSchema = ProductReferenceSchema.safeExtend({
|
|
61
|
-
name: productDetails.name, url: productDetails.url, price:
|
|
62
|
-
|
|
65
|
+
name: productDetails.name, url: productDetails.url, price: PriceTagSchema.nullable(),
|
|
66
|
+
secondaryPrice: PriceTagSchema.nullable(), availability: z.enum(['available', 'unavailable', 'unknown']),
|
|
63
67
|
saleRules: z.discriminatedUnion('status', [
|
|
64
68
|
z.strictObject({ status: z.literal('available'), rules: ProductSaleRulesSchema }),
|
|
65
69
|
z.strictObject({ status: z.literal('unavailable'), error: ToolErrorSchema }),
|
package/dist/domain/money.d.ts
CHANGED
|
@@ -21,3 +21,22 @@ export declare const QuotedMoneySchema: z.ZodObject<{
|
|
|
21
21
|
export type QuotedMoney = z.infer<typeof QuotedMoneySchema>;
|
|
22
22
|
/** Convert decimal text through integer arithmetic; never round an unrecognized precision. */
|
|
23
23
|
export declare function parseMoneyAmount(input: unknown): Result<Money | null, ToolError>;
|
|
24
|
+
/**
|
|
25
|
+
* What a displayed price is for. The storefront shows packaged goods per `item` (one pack or
|
|
26
|
+
* article), produce and meat per `kg`, drinks per `l`, and an approximate price per `piece`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const PriceTagSchema: z.ZodObject<{
|
|
29
|
+
amount: z.ZodObject<{
|
|
30
|
+
currency: z.ZodLiteral<"EUR">;
|
|
31
|
+
minorUnits: z.ZodNumber;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
per: z.ZodEnum<{
|
|
34
|
+
item: "item";
|
|
35
|
+
kg: "kg";
|
|
36
|
+
l: "l";
|
|
37
|
+
piece: "piece";
|
|
38
|
+
}>;
|
|
39
|
+
}, z.core.$strict>;
|
|
40
|
+
export type PriceTag = z.infer<typeof PriceTagSchema>;
|
|
41
|
+
/** Parse labels such as `1,29€`, `1,19€/kg` or `0,24€/un`. An empty label is an unknown price. */
|
|
42
|
+
export declare function parsePriceTag(input: unknown): Result<PriceTag | null, ToolError>;
|
package/dist/domain/money.js
CHANGED
|
@@ -28,3 +28,28 @@ export function parseMoneyAmount(input) {
|
|
|
28
28
|
}
|
|
29
29
|
return { ok: true, value: { currency: 'EUR', minorUnits: Number(minorUnits) } };
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* What a displayed price is for. The storefront shows packaged goods per `item` (one pack or
|
|
33
|
+
* article), produce and meat per `kg`, drinks per `l`, and an approximate price per `piece`.
|
|
34
|
+
*/
|
|
35
|
+
export const PriceTagSchema = z.strictObject({
|
|
36
|
+
amount: MoneySchema,
|
|
37
|
+
per: z.enum(['item', 'piece', 'kg', 'l']),
|
|
38
|
+
});
|
|
39
|
+
const PRICE_UNITS = { kg: 'kg', l: 'l', lt: 'l', un: 'piece' };
|
|
40
|
+
/** Parse labels such as `1,29€`, `1,19€/kg` or `0,24€/un`. An empty label is an unknown price. */
|
|
41
|
+
export function parsePriceTag(input) {
|
|
42
|
+
if (input === null || input === undefined)
|
|
43
|
+
return { ok: true, value: null };
|
|
44
|
+
if (typeof input !== 'string')
|
|
45
|
+
return { ok: false, error: invalidData('price') };
|
|
46
|
+
const text = input.replace(/\s+/g, '');
|
|
47
|
+
if (!text)
|
|
48
|
+
return { ok: true, value: null };
|
|
49
|
+
const match = /^(\d+(?:[.,]\d{1,2})?)€(?:\/([a-z]+))?$/i.exec(text);
|
|
50
|
+
const per = match ? PRICE_UNITS[(match[2] ?? '').toLowerCase()] ?? (match[2] === undefined ? 'item' : undefined) : undefined;
|
|
51
|
+
const amount = match && per ? parseMoneyAmount(match[1]) : null;
|
|
52
|
+
if (!amount?.ok || !amount.value || !per)
|
|
53
|
+
return { ok: false, error: { ...invalidData('price'), message: 'Unrecognised price format' } };
|
|
54
|
+
return { ok: true, value: { amount: amount.value, per } };
|
|
55
|
+
}
|
package/dist/domain/session.d.ts
CHANGED
|
@@ -38,3 +38,9 @@ export declare const FavoritesStatusSchema: z.ZodObject<{
|
|
|
38
38
|
empty: z.ZodNullable<z.ZodBoolean>;
|
|
39
39
|
}, z.core.$strict>;
|
|
40
40
|
export type FavoritesStatus = z.infer<typeof FavoritesStatusSchema>;
|
|
41
|
+
export declare const UpdateStatusSchema: z.ZodObject<{
|
|
42
|
+
currentVersion: z.ZodNullable<z.ZodString>;
|
|
43
|
+
latestVersion: z.ZodNullable<z.ZodString>;
|
|
44
|
+
available: z.ZodBoolean;
|
|
45
|
+
}, z.core.$strict>;
|
|
46
|
+
export type UpdateStatus = z.infer<typeof UpdateStatusSchema>;
|
package/dist/domain/session.js
CHANGED
|
@@ -9,3 +9,6 @@ export const FavoritesStatusSchema = z.strictObject({
|
|
|
9
9
|
state: z.enum(['missing', 'fresh', 'stale', 'unavailable']), fetchedAt: z.iso.datetime({ offset: true }).nullable(),
|
|
10
10
|
expiresAt: z.iso.datetime({ offset: true }).nullable(), empty: z.boolean().nullable(),
|
|
11
11
|
});
|
|
12
|
+
export const UpdateStatusSchema = z.strictObject({
|
|
13
|
+
currentVersion: z.string().nullable(), latestVersion: z.string().nullable(), available: z.boolean(),
|
|
14
|
+
});
|
package/dist/mcp/contracts.d.ts
CHANGED
|
@@ -205,14 +205,33 @@ export declare const SearchData: z.ZodObject<{
|
|
|
205
205
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
206
206
|
name: z.ZodString;
|
|
207
207
|
price: z.ZodNullable<z.ZodObject<{
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
amount: z.ZodObject<{
|
|
209
|
+
currency: z.ZodLiteral<"EUR">;
|
|
210
|
+
minorUnits: z.ZodNumber;
|
|
211
|
+
}, z.core.$strict>;
|
|
212
|
+
per: z.ZodEnum<{
|
|
213
|
+
item: "item";
|
|
214
|
+
kg: "kg";
|
|
215
|
+
l: "l";
|
|
216
|
+
piece: "piece";
|
|
217
|
+
}>;
|
|
210
218
|
}, z.core.$strict>>;
|
|
211
219
|
url: z.ZodURL;
|
|
212
|
-
|
|
220
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
221
|
+
amount: z.ZodObject<{
|
|
222
|
+
currency: z.ZodLiteral<"EUR">;
|
|
223
|
+
minorUnits: z.ZodNumber;
|
|
224
|
+
}, z.core.$strict>;
|
|
225
|
+
per: z.ZodEnum<{
|
|
226
|
+
item: "item";
|
|
227
|
+
kg: "kg";
|
|
228
|
+
l: "l";
|
|
229
|
+
piece: "piece";
|
|
230
|
+
}>;
|
|
231
|
+
}, z.core.$strict>>;
|
|
232
|
+
packageText: z.ZodNullable<z.ZodString>;
|
|
213
233
|
minimumText: z.ZodNullable<z.ZodString>;
|
|
214
234
|
favorite: z.ZodBoolean;
|
|
215
|
-
similarity: z.ZodNumber;
|
|
216
235
|
}, z.core.$strict>>;
|
|
217
236
|
scanned: z.ZodNumber;
|
|
218
237
|
returned: z.ZodNumber;
|
|
@@ -256,8 +275,16 @@ export declare const FavoritesData: z.ZodObject<{
|
|
|
256
275
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
257
276
|
name: z.ZodString;
|
|
258
277
|
price: z.ZodNullable<z.ZodObject<{
|
|
259
|
-
|
|
260
|
-
|
|
278
|
+
amount: z.ZodObject<{
|
|
279
|
+
currency: z.ZodLiteral<"EUR">;
|
|
280
|
+
minorUnits: z.ZodNumber;
|
|
281
|
+
}, z.core.$strict>;
|
|
282
|
+
per: z.ZodEnum<{
|
|
283
|
+
item: "item";
|
|
284
|
+
kg: "kg";
|
|
285
|
+
l: "l";
|
|
286
|
+
piece: "piece";
|
|
287
|
+
}>;
|
|
261
288
|
}, z.core.$strict>>;
|
|
262
289
|
url: z.ZodURL;
|
|
263
290
|
}, z.core.$strict>>;
|
|
@@ -498,14 +525,33 @@ export declare const SearchOutput: z.ZodObject<{
|
|
|
498
525
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
499
526
|
name: z.ZodString;
|
|
500
527
|
price: z.ZodNullable<z.ZodObject<{
|
|
501
|
-
|
|
502
|
-
|
|
528
|
+
amount: z.ZodObject<{
|
|
529
|
+
currency: z.ZodLiteral<"EUR">;
|
|
530
|
+
minorUnits: z.ZodNumber;
|
|
531
|
+
}, z.core.$strict>;
|
|
532
|
+
per: z.ZodEnum<{
|
|
533
|
+
item: "item";
|
|
534
|
+
kg: "kg";
|
|
535
|
+
l: "l";
|
|
536
|
+
piece: "piece";
|
|
537
|
+
}>;
|
|
503
538
|
}, z.core.$strict>>;
|
|
504
539
|
url: z.ZodURL;
|
|
505
|
-
|
|
540
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
541
|
+
amount: z.ZodObject<{
|
|
542
|
+
currency: z.ZodLiteral<"EUR">;
|
|
543
|
+
minorUnits: z.ZodNumber;
|
|
544
|
+
}, z.core.$strict>;
|
|
545
|
+
per: z.ZodEnum<{
|
|
546
|
+
item: "item";
|
|
547
|
+
kg: "kg";
|
|
548
|
+
l: "l";
|
|
549
|
+
piece: "piece";
|
|
550
|
+
}>;
|
|
551
|
+
}, z.core.$strict>>;
|
|
552
|
+
packageText: z.ZodNullable<z.ZodString>;
|
|
506
553
|
minimumText: z.ZodNullable<z.ZodString>;
|
|
507
554
|
favorite: z.ZodBoolean;
|
|
508
|
-
similarity: z.ZodNumber;
|
|
509
555
|
}, z.core.$strict>>;
|
|
510
556
|
scanned: z.ZodNumber;
|
|
511
557
|
returned: z.ZodNumber;
|
|
@@ -598,8 +644,16 @@ export declare const FavoritesOutput: z.ZodObject<{
|
|
|
598
644
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
599
645
|
name: z.ZodString;
|
|
600
646
|
price: z.ZodNullable<z.ZodObject<{
|
|
601
|
-
|
|
602
|
-
|
|
647
|
+
amount: z.ZodObject<{
|
|
648
|
+
currency: z.ZodLiteral<"EUR">;
|
|
649
|
+
minorUnits: z.ZodNumber;
|
|
650
|
+
}, z.core.$strict>;
|
|
651
|
+
per: z.ZodEnum<{
|
|
652
|
+
item: "item";
|
|
653
|
+
kg: "kg";
|
|
654
|
+
l: "l";
|
|
655
|
+
piece: "piece";
|
|
656
|
+
}>;
|
|
603
657
|
}, z.core.$strict>>;
|
|
604
658
|
url: z.ZodURL;
|
|
605
659
|
}, z.core.$strict>>;
|
|
@@ -1301,6 +1355,11 @@ export declare const SessionStatusOutput: z.ZodObject<{
|
|
|
1301
1355
|
expiresAt: z.ZodNullable<z.ZodISODateTime>;
|
|
1302
1356
|
empty: z.ZodNullable<z.ZodBoolean>;
|
|
1303
1357
|
}, z.core.$strict>;
|
|
1358
|
+
update: z.ZodObject<{
|
|
1359
|
+
currentVersion: z.ZodNullable<z.ZodString>;
|
|
1360
|
+
latestVersion: z.ZodNullable<z.ZodString>;
|
|
1361
|
+
available: z.ZodBoolean;
|
|
1362
|
+
}, z.core.$strict>;
|
|
1304
1363
|
}, z.core.$strict>;
|
|
1305
1364
|
}, z.core.$strict>, z.ZodObject<{
|
|
1306
1365
|
status: z.ZodLiteral<"error">;
|
|
@@ -1359,18 +1418,30 @@ export declare const ProductDetailsOutput: z.ZodObject<{
|
|
|
1359
1418
|
productSlug: z.ZodNullable<z.core.$ZodBranded<z.ZodString, "ProductSlug", "out">>;
|
|
1360
1419
|
name: z.ZodString;
|
|
1361
1420
|
url: z.ZodURL;
|
|
1362
|
-
price: z.ZodObject<{
|
|
1363
|
-
amount: z.
|
|
1421
|
+
price: z.ZodNullable<z.ZodObject<{
|
|
1422
|
+
amount: z.ZodObject<{
|
|
1364
1423
|
currency: z.ZodLiteral<"EUR">;
|
|
1365
1424
|
minorUnits: z.ZodNumber;
|
|
1366
|
-
}, z.core.$strict
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1425
|
+
}, z.core.$strict>;
|
|
1426
|
+
per: z.ZodEnum<{
|
|
1427
|
+
item: "item";
|
|
1428
|
+
kg: "kg";
|
|
1429
|
+
l: "l";
|
|
1430
|
+
piece: "piece";
|
|
1371
1431
|
}>;
|
|
1372
|
-
}, z.core.$strict
|
|
1373
|
-
|
|
1432
|
+
}, z.core.$strict>>;
|
|
1433
|
+
secondaryPrice: z.ZodNullable<z.ZodObject<{
|
|
1434
|
+
amount: z.ZodObject<{
|
|
1435
|
+
currency: z.ZodLiteral<"EUR">;
|
|
1436
|
+
minorUnits: z.ZodNumber;
|
|
1437
|
+
}, z.core.$strict>;
|
|
1438
|
+
per: z.ZodEnum<{
|
|
1439
|
+
item: "item";
|
|
1440
|
+
kg: "kg";
|
|
1441
|
+
l: "l";
|
|
1442
|
+
piece: "piece";
|
|
1443
|
+
}>;
|
|
1444
|
+
}, z.core.$strict>>;
|
|
1374
1445
|
availability: z.ZodEnum<{
|
|
1375
1446
|
available: "available";
|
|
1376
1447
|
unavailable: "unavailable";
|