continente-mcp 3.1.0 → 4.1.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.
@@ -22,11 +22,11 @@ The goal is to pick the product the user actually buys, not just any product wit
22
22
 
23
23
  **1. Search with favourites context**
24
24
 
25
- Call `search_products` with the item name. Results are automatically ranked by the favourites list — the top result is usually correct. Note the top 3.
25
+ Call `search_products` with the item name. Favourites are ranked first and marked ⭐ — the top result is usually correct. Note the top 3.
26
26
 
27
- **2. Cross-check favourites**
27
+ **2. Prefer favourites**
28
28
 
29
- Call `get_favorites` once (reuse for all items in this request). If any favourite matches the item name, prefer it — even if not the top search result. A favourite is a stronger signal than search rank.
29
+ If any result is marked ⭐, prefer it — even if it is not the top result. A favourite is a stronger signal than search rank. If nothing is marked and the user seems to have favourites, call `refresh_favorites` once and search again.
30
30
 
31
31
  **3. Fallback: order history**
32
32
 
@@ -40,6 +40,11 @@ If confidence is high (clear favourite or obvious top result), add silently and
40
40
 
41
41
  Call `add_to_cart` with `product_id`. Default quantity is 1 unless specified (e.g. "2 pacotes de leite").
42
42
 
43
+ `add_to_cart` and `update_cart_item` both check the basket afterwards and only report success when it holds the requested quantity. Products sold by weight (e.g. bananas) are counted in units, like on the website.
44
+
45
+ - `below_minimum_quantity` — the product has a minimum, shown as ⚖️ in search results and in the error. Nothing was added; ask whether to add the minimum instead.
46
+ - `cart_add_not_confirmed` or `cart_quantity_not_confirmed` — the change may still have happened. Call `get_cart`, never repeat `add_to_cart`, and correct with `update_cart_item` if needed.
47
+
43
48
  ## Output format
44
49
 
45
50
  After all items are processed, print a compact summary:
@@ -47,15 +52,16 @@ After all items are processed, print a compact summary:
47
52
  ```
48
53
  ✓ Queijo Flamengo Mil Vacas 400g — added (from favourites)
49
54
  ✓ Leite Mimosa Meio-Gordo 1L — added
55
+ ✓ Banana Continente — quantity set to 12
50
56
  ✗ Pão de Forma — unclear match, please check manually
51
57
  ```
52
58
 
53
59
  ## Error handling
54
60
 
55
61
  - No search results → report as not found, skip, continue with others
56
- - `add_to_cart` fails → report the error, move on
62
+ - `add_to_cart` fails → handle the errors above, otherwise report the error and move on
57
63
  - Never stop the whole run because one item failed
58
64
 
59
65
  ## Performance
60
66
 
61
- `get_favorites` is slow. Call it once and reuse across all items. Only call `get_order_history` when favourites give no clear signal.
67
+ `get_order_history` opens each order and is slow. Only call it when search and favourites give no clear signal.
@@ -0,0 +1,37 @@
1
+ import { existsSync } from 'fs';
2
+
3
+ import { autoLogin, getCredentialStatus, loadCredentialEnv, resolveStatePaths } from '../continente-auto-login.js';
4
+
5
+ export { resolveStatePaths };
6
+
7
+ export function canAutoLogin(env = process.env) {
8
+ return getCredentialStatus(env).ready;
9
+ }
10
+
11
+ export async function refreshAuthCookies({
12
+ stateDir,
13
+ closeBrowser,
14
+ log = console.error,
15
+ env = process.env,
16
+ } = {}) {
17
+ if (!canAutoLogin(env)) {
18
+ log('Automatic login skipped: CONTINENTE_EMAIL and CONTINENTE_PASSWORD are not both set.');
19
+ return false;
20
+ }
21
+
22
+ const loginEnv = loadCredentialEnv(env);
23
+ if (stateDir) loginEnv.CONTINENTE_STATE_DIR = stateDir;
24
+ const { cookieFile } = resolveStatePaths(loginEnv);
25
+ loginEnv.CONTINENTE_COOKIES_PATH = cookieFile;
26
+
27
+ if (closeBrowser) {
28
+ await closeBrowser();
29
+ }
30
+
31
+ const result = await autoLogin({
32
+ env: loginEnv,
33
+ log: (message) => log(`[auth] ${message}`),
34
+ });
35
+
36
+ return Boolean(result.success && existsSync(cookieFile));
37
+ }
@@ -0,0 +1,113 @@
1
+ export function normalizeCartProductId(productId) {
2
+ const value = String(productId || '').trim();
3
+ if (!value) return value;
4
+
5
+ const withoutHtml = value.replace(/\.html$/, '');
6
+ const masterMatch = withoutHtml.match(/^(\d+)-master$/);
7
+ if (masterMatch) return masterMatch[1];
8
+
9
+ const numericTail = withoutHtml.match(/(\d+)$/);
10
+ return numericTail ? numericTail[1] : withoutHtml;
11
+ }
12
+
13
+ export function quantityForCartUpdate(displayQuantity, measureOptions = {}) {
14
+ const quantity = Number(displayQuantity);
15
+ if (!Number.isFinite(quantity) || quantity < 0) {
16
+ throw new Error('Quantity must be a non-negative number');
17
+ }
18
+
19
+ const conversion = Number(measureOptions.primaryToSecondary ?? measureOptions.unitConversionRate);
20
+ const hasAlternativeUnit = measureOptions.hasAlternativeSaleUnit || measureOptions.hasConversionRate;
21
+ if ((hasAlternativeUnit && (!Number.isFinite(conversion) || conversion <= 0)) ||
22
+ // Without an alternative sale unit, quantities are counted in the primary unit;
23
+ // secondaryunit is only the price reference (e.g. €/kg for a unit product).
24
+ (!hasAlternativeUnit && ((measureOptions.primaryunit && measureOptions.primaryunit !== 'un') ||
25
+ (measureOptions.selectedunit && measureOptions.selectedunit !== 'primary')))) {
26
+ throw new Error('quantity_semantics_unknown');
27
+ }
28
+ const cartQuantity = hasAlternativeUnit ? quantity * conversion : quantity;
29
+ if (!Number.isFinite(cartQuantity)) throw new Error('quantity_semantics_unknown');
30
+ const minimum = Number(measureOptions.minOrderQuantity);
31
+ if (quantity > 0 && minimum > 0 && cartQuantity < minimum - 0.0005) {
32
+ const displayMinimum = hasAlternativeUnit ? minimum / conversion : minimum;
33
+ throw new Error(`below_minimum_quantity: the minimum is ${Number(displayMinimum.toFixed(3))}`);
34
+ }
35
+
36
+ return Number(cartQuantity.toFixed(3)).toString();
37
+ }
38
+
39
+ export function summarizeCartState(payload = {}) {
40
+ const customerAuthenticated = payload?.resources?.customerAuthenticated === true;
41
+ // The storefront uses {} when this session has not created a basket yet.
42
+ // Require the known response envelope; arbitrary/malformed objects still fail.
43
+ if (payload?.action === 'Cart-MiniCartShow' && payload.basket &&
44
+ typeof payload.basket === 'object' && !Array.isArray(payload.basket) &&
45
+ Object.keys(payload.basket).length === 0) {
46
+ return { customerAuthenticated, items: [], total: 0 };
47
+ }
48
+ const addItems = Array.isArray(payload?.cart?.items) ? payload.cart.items : null;
49
+ const miniCartGroups = Array.isArray(payload?.basket?.itemsSortedByBrand)
50
+ ? payload.basket.itemsSortedByBrand
51
+ : null;
52
+
53
+ const unknown = { customerAuthenticated, error: 'cart_shape_unknown' };
54
+ if (!addItems && !miniCartGroups) return unknown;
55
+ if (!addItems && miniCartGroups.some(group => !Array.isArray(group?.items))) return unknown;
56
+
57
+ const sourceItems = addItems || (miniCartGroups
58
+ ? miniCartGroups.flatMap(group => Array.isArray(group?.items) ? group.items : [])
59
+ : []);
60
+
61
+ if (sourceItems.some(item => !item?.id || !item.productName ||
62
+ !Number.isFinite(Number(item.secondaryQuantity ?? item.quantity)) ||
63
+ Number(item.secondaryQuantity ?? item.quantity) < 0)) return unknown;
64
+
65
+ const items = sourceItems.map(item => ({
66
+ id: normalizeCartProductId(item.id),
67
+ name: item.productName,
68
+ qty: Number(item.secondaryQuantity ?? item.quantity ?? 1),
69
+ price: moneyOrNull(item?.price?.sales?.value ?? item?.priceTotal?.basePriceValue)
70
+ }));
71
+
72
+ const total = moneyOrNull(
73
+ payload?.cart?.totalProductsValueNumber ??
74
+ payload?.basket?.totals?.productsTotalPriceOnly ??
75
+ payload?.basket?.totals?.productsTotalPriceOnlyWithSDR
76
+ );
77
+
78
+ return {
79
+ customerAuthenticated,
80
+ items,
81
+ total
82
+ };
83
+ }
84
+
85
+ function moneyOrNull(value) {
86
+ if (value == null || value === '') return null;
87
+ const amount = Number(value);
88
+ return Number.isFinite(amount) && amount >= 0 ? amount : null;
89
+ }
90
+
91
+ // Order lines show "12 un", or "1.2 kg" for products bought by weight.
92
+ export function parseOrderQuantity(text) {
93
+ const match = String(text ?? '').trim().match(/^(\d+(?:[.,]\d+)?)\s*([a-zA-Z]*)/);
94
+ if (!match) return { qty: 1, unit: 'un' };
95
+ return { qty: Number(match[1].replace(',', '.')), unit: match[2].toLowerCase() || 'un' };
96
+ }
97
+
98
+ // Units and weights cannot be added together, so totals are kept per unit.
99
+ // Sorted by how many orders include the product, then by units bought.
100
+ export function tallyMostBought(orders) {
101
+ const tally = new Map();
102
+ for (const products of orders) {
103
+ const seenInOrder = new Set();
104
+ for (const { name, qty, unit } of products) {
105
+ const entry = tally.get(name) ?? { name, quantities: {}, orders: 0 };
106
+ entry.quantities[unit] = Number(((entry.quantities[unit] ?? 0) + qty).toFixed(3));
107
+ if (!seenInOrder.has(name)) { entry.orders += 1; seenInOrder.add(name); }
108
+ tally.set(name, entry);
109
+ }
110
+ }
111
+ return Array.from(tally.values())
112
+ .sort((a, b) => b.orders - a.orders || (b.quantities.un ?? 0) - (a.quantities.un ?? 0));
113
+ }