auchan-mcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,21 @@
1
+ # auchan-mcp: variáveis de ambiente
2
+ # Com o plugin supermercados-mcp, o ficheiro é criado e aberto sozinho na primeira vez:
3
+ # Windows: %USERPROFILE%\.supermercados-mcp\credentials.env
4
+ # macOS/Linux: ~/.supermercados-mcp/credentials.env
5
+ # Sem o plugin, podes pôr as duas linhas abaixo nesse ficheiro ou em ~/.auchan-mcp/credentials.env.
6
+ # Esse ficheiro nunca vai para o git. A pesquisa funciona sem ele.
7
+
8
+ # Necessárias para o carrinho (get_cart, add_to_cart, update_cart_item).
9
+ # AUCHAN_EMAIL=tu@exemplo.pt
10
+ # AUCHAN_PASSWORD=a-tua-palavra-passe
11
+
12
+ # As seguintes definem-se no "env" do servidor no cliente MCP, não no ficheiro acima.
13
+
14
+ # Outro ficheiro de credenciais (por omissão: o do plugin e depois ~/.auchan-mcp/credentials.env).
15
+ # AUCHAN_ENV_PATH=C:\caminho\para\credentials.env
16
+
17
+ # Outra pasta para a sessão guardada e a cache (por omissão: ~/.auchan-mcp).
18
+ # AUCHAN_STATE_DIR=C:\caminho\para\pasta
19
+
20
+ # Põe false só para ver o browser durante o login, ao procurar um problema.
21
+ # AUCHAN_LOGIN_HEADLESS=true
package/README.md CHANGED
@@ -10,6 +10,7 @@ Feito à imagem do [continente-mcp](https://github.com/davidmarqu3s/continente-m
10
10
 
11
11
  - **Pesquisa sem conta**: nome, marca, preço, preço e preço por litro, quilo ou unidade.
12
12
  - **Carrinho**: ver, juntar e mudar quantidades; cada alteração é confirmada no carrinho a seguir.
13
+ - **Entra sozinho**: usa o email e a palavra-passe de um ficheiro teu, em segundo plano e sem janela, e volta a entrar sempre que o Auchan termina a sessão.
13
14
  - **Respeita o site**: no máximo um pedido por segundo, resultados guardados durante 6 horas e um user-agent que diz quem é.
14
15
  - **Tu decides**: a finalização da encomenda e o pagamento fazem-se sempre no Auchan.pt. O servidor não tem nenhuma ferramenta para isso.
15
16
 
@@ -31,7 +32,16 @@ Precisas do Node.js 20.18.1 ou mais recente e de um cliente MCP, por exemplo o C
31
32
  }
32
33
  ```
33
34
 
34
- A pesquisa funciona logo, sem conta. Para o carrinho não crias nada: quando o assistente precisar dele, abre-se uma janela do Edge ou do Chrome na página de login do Auchan. Entras tu, a janela fecha e ficas ligado. A palavra-passe nunca passa pelo servidor nem fica guardada: só a sessão, no teu computador (`%USERPROFILE%\.auchan-mcp\cookies.json`). Se aparecer um CAPTCHA, resolves tu na janela.
35
+ A pesquisa funciona logo, sem conta. Para o carrinho, cria o ficheiro `%USERPROFILE%\.auchan-mcp\credentials.env` (no macOS e Linux, `~/.auchan-mcp/credentials.env`) com duas linhas, sem aspas e sem espaços à volta do `=`:
36
+
37
+ ```
38
+ AUCHAN_EMAIL=o-teu-email
39
+ AUCHAN_PASSWORD=a-tua-palavra-passe
40
+ ```
41
+
42
+ Também lê `~/.supermercados-mcp/credentials.env`, o ficheiro que o plugin [supermercados-mcp](https://github.com/vasco-branco06/supermercados-mcp) cria e abre sozinho na primeira vez, e o que `AUCHAN_ENV_PATH` indicar. O Auchan só aceita produtos no carrinho depois de escolheres uma vez, no site, entrega em casa ou levantamento (com o código postal); essa escolha fica guardada na tua conta.
43
+
44
+ A palavra-passe fica em texto simples nesse ficheiro, na tua pasta de utilizador. Não o partilhes. Só é lida para preencher o formulário de login e nunca aparece nas respostas nem nos registos. A sessão (cookies e armazenamento local) fica em `%USERPROFILE%\.auchan-mcp\cookies.json`.
35
45
 
36
46
  ## Usar
37
47
 
@@ -42,12 +52,16 @@ Pede coisas como "procura leite meio-gordo no Auchan" ou "junta 2 pacotes ao car
42
52
  | `search_products` | Pesquisar o catálogo (devolve JSON) |
43
53
  | `get_cart` | Ver o carrinho |
44
54
  | `add_to_cart` / `update_cart_item` | Juntar um produto / mudar a quantidade (0 tira-o) |
45
- | `login` | Ligar a tua conta: abre a janela de login (as ferramentas do carrinho fazem-no sozinhas se for preciso) |
46
- | `logout` | Desligar a conta: apaga a sessão guardada neste computador |
55
+ | `login` | Verificar ou renovar a sessão com o ficheiro de credenciais (as ferramentas do carrinho fazem-no sozinhas) |
56
+ | `logout` | Desligar a conta: apaga a sessão guardada neste computador (o ficheiro de credenciais fica) |
47
57
  | `close_session` | Libertar recursos |
48
58
 
59
+ Com o servidor a correr, faz um pedido leve de 10 em 10 minutos para o Auchan não fechar a sessão por inatividade, e guarda o que o site renova.
60
+
49
61
  ## Mais
50
62
 
63
+ - **Sem ficheiro de credenciais:** existe o ramo [`login-por-janela`](https://github.com/vasco-branco06/supermercados-mcp/tree/login-por-janela), em que a pessoa entra numa janela do browser e a palavra-passe nunca é guardada.
64
+ - **Definições:** variáveis em [`.env.example`](.env.example).
51
65
  - **Problemas:** [docs/troubleshooting.md](docs/troubleshooting.md).
52
66
  - **Testes:** `npm test` corre sem internet. `LIVE=1` liga um teste que faz um pedido real ao Auchan.pt (no PowerShell: `$env:LIVE=1; npm test`).
53
67
 
package/dist/index.js CHANGED
@@ -176,21 +176,47 @@ async function fetchPage(url2, { fetchImpl = fetch, gate = throttle } = {}) {
176
176
  }
177
177
 
178
178
  // src/paths.js
179
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
179
180
  import { homedir } from "node:os";
180
181
  import { join } from "node:path";
181
182
  function resolveStatePaths(env = process.env) {
182
- const stateDir = env.AUCHAN_STATE_DIR || join(env.HOME || env.USERPROFILE || homedir(), ".auchan-mcp");
183
+ const home = env.HOME || env.USERPROFILE || homedir();
184
+ const stateDir = env.AUCHAN_STATE_DIR || join(home, ".auchan-mcp");
183
185
  return {
184
186
  stateDir,
185
187
  cacheDir: join(stateDir, "cache"),
188
+ // Where the plugin's first run creates the template, shared by every supermarket.
189
+ sharedCredentialFile: env.SUPERMERCADOS_MCP_CREDENTIALS || join(env.SUPERMERCADOS_MCP_DIR || join(home, ".supermercados-mcp"), "credentials.env"),
190
+ credentialFile: join(stateDir, "credentials.env"),
186
191
  cookieFile: join(stateDir, "cookies.json")
187
192
  };
188
193
  }
194
+ function parseEnvFile(contents) {
195
+ const parsed = {};
196
+ for (const line of contents.split(/\r?\n/)) {
197
+ const raw = line.trim().replace(/^export\s+/, "");
198
+ const index = raw.indexOf("=");
199
+ if (!raw || raw.startsWith("#") || index === -1) continue;
200
+ const key = raw.slice(0, index).trim();
201
+ const value = raw.slice(index + 1).trim().replace(/^(["'])(.*)\1$/, "$2");
202
+ if (/^[A-Z_][A-Z0-9_]*$/.test(key)) parsed[key] = value;
203
+ }
204
+ return parsed;
205
+ }
206
+ function loadCredentials(env = process.env) {
207
+ const { credentialFile, sharedCredentialFile } = resolveStatePaths(env);
208
+ for (const file2 of [env.AUCHAN_ENV_PATH, sharedCredentialFile, credentialFile]) {
209
+ if (!file2 || !existsSync(file2)) continue;
210
+ const { AUCHAN_EMAIL: email3, AUCHAN_PASSWORD: password } = parseEnvFile(readFileSync2(file2, "utf8"));
211
+ if (email3 && password) return { email: email3, password };
212
+ }
213
+ return null;
214
+ }
189
215
 
190
216
  // src/search.js
191
217
  import * as cheerio2 from "cheerio";
192
218
  import { createHash } from "node:crypto";
193
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
219
+ import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
194
220
  import { join as join2 } from "node:path";
195
221
 
196
222
  // ../../node_modules/zod/v4/classic/external.js
@@ -20010,7 +20036,7 @@ var searchUrl = (query) => `${AUCHAN_BASE}/pt/pesquisa?q=${encodeURIComponent(qu
20010
20036
  var cacheFile = (cacheDir, query) => join2(cacheDir, `${createHash("sha256").update(query.trim().toLowerCase()).digest("hex").slice(0, 32)}.json`);
20011
20037
  function readCache(file2, now) {
20012
20038
  try {
20013
- const entry = JSON.parse(readFileSync2(file2, "utf8"));
20039
+ const entry = JSON.parse(readFileSync3(file2, "utf8"));
20014
20040
  if (now - Date.parse(entry.fetched_at) < CACHE_TTL_MS && Array.isArray(entry.raw)) return entry;
20015
20041
  } catch {
20016
20042
  }
@@ -20035,40 +20061,54 @@ async function searchProducts(query, limit = 10, { cacheDir, fetchImpl, now = Da
20035
20061
  }
20036
20062
 
20037
20063
  // src/session.js
20038
- import { chmodSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
20064
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
20039
20065
  import { dirname } from "node:path";
20040
20066
  var LOGIN_URL = `${AUCHAN_BASE}/on/demandware.store/Sites-AuchanPT-Site/pt_PT/Login-OAuthLogin?oauthProvider=Salesforce&oauthLoginTargetEndPoint=1`;
20041
20067
  var DELIVERY_URL = `${AUCHAN_BASE}/on/demandware.store/Sites-AuchanPT-Site/pt_PT/Delivery-Header`;
20042
20068
  var CART_URL = `${AUCHAN_BASE}/on/demandware.store/Sites-AuchanPT-Site/pt_PT/Cart-Show`;
20069
+ var EMAIL_FIELD = 'input[name="uname1"], input[type="email"]';
20070
+ var PASSWORD_FIELD = 'input[name="passwordLogin"], input[type="password"]';
20071
+ var SUBMIT_BUTTON = '#btnSubmit_login, input[name="btnSubmit_login"], button[type="submit"]:visible';
20072
+ var CHALLENGE = 'iframe[src*="captcha"], iframe[title*="challenge"], .g-recaptcha, .h-captcha, #px-captcha';
20043
20073
  var BROWSER_CHANNELS = ["msedge", "chrome", void 0];
20044
20074
  var NO_PLAYWRIGHT = "falta o Playwright; instala o pacote de novo (npm install auchan-mcp)";
20045
- var NO_BROWSER = 'n\xE3o encontrei o Microsoft Edge nem o Google Chrome para abrir a janela de login; instala um deles ou corre "npx playwright install chromium"';
20046
- function writeCookies(path, cookies) {
20075
+ var NO_BROWSER = 'n\xE3o encontrei o Microsoft Edge nem o Google Chrome para fazer o login; instala um deles ou corre "npx playwright install chromium"';
20076
+ var FORM_CHANGED = "o formul\xE1rio de login do Auchan n\xE3o \xE9 o esperado (o site pode ter mudado); \xE9 preciso atualizar o servidor";
20077
+ var LOGIN_FAILED = "o login n\xE3o terminou; confirma o email e a palavra-passe no ficheiro de credenciais";
20078
+ var NO_DELIVERY = "o Auchan ainda n\xE3o tem a tua entrega escolhida; escolhe-a uma vez no auchan.pt (entrega em casa ou levantamento, com o c\xF3digo postal) e tenta de novo";
20079
+ function writeState(path, state) {
20047
20080
  mkdirSync2(dirname(path), { recursive: true, mode: 448 });
20048
- writeFileSync2(path, JSON.stringify(cookies, null, 2), { mode: 384 });
20081
+ writeFileSync2(path, JSON.stringify(state, null, 2), { mode: 384 });
20049
20082
  chmodSync(path, 384);
20050
20083
  }
20051
- function readCookies(path) {
20052
- if (!existsSync(path)) return null;
20084
+ function readState(path) {
20085
+ if (!existsSync2(path)) return null;
20053
20086
  try {
20054
- const cookies = JSON.parse(readFileSync3(path, "utf8"));
20055
- return Array.isArray(cookies) && cookies.length ? cookies : null;
20087
+ const saved = JSON.parse(readFileSync4(path, "utf8"));
20088
+ const state = Array.isArray(saved) ? { cookies: saved, origins: [] } : { cookies: saved?.cookies, origins: saved?.origins ?? [] };
20089
+ return Array.isArray(state.cookies) && state.cookies.length ? state : null;
20056
20090
  } catch {
20057
20091
  return null;
20058
20092
  }
20059
20093
  }
20060
20094
  function createSession({
20095
+ credentials,
20096
+ credentialFile = "credentials.env",
20061
20097
  cookieFile,
20062
20098
  log = console.error,
20063
20099
  loadPlaywright = () => import("playwright"),
20064
- loginTimeoutMs = 5 * 60 * 1e3,
20065
- pollMs = 3e3,
20066
- sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
20100
+ headless = true,
20101
+ loginTimeoutMs = 3e4,
20102
+ keepAliveMs = 10 * 60 * 1e3
20067
20103
  }) {
20068
20104
  let playwright = null;
20069
20105
  let api = null;
20106
+ let state = null;
20070
20107
  let loggedIn = false;
20071
20108
  let pending = null;
20109
+ let inFlight = 0;
20110
+ let timer = null;
20111
+ let beating = false;
20072
20112
  async function pw() {
20073
20113
  if (!playwright) {
20074
20114
  try {
@@ -20079,13 +20119,14 @@ function createSession({
20079
20119
  }
20080
20120
  return playwright;
20081
20121
  }
20082
- async function openApi(cookies) {
20122
+ async function openApi(next) {
20123
+ state = next;
20083
20124
  await api?.dispose().catch(() => {
20084
20125
  });
20085
20126
  api = await (await pw()).request.newContext({
20086
20127
  userAgent: USER_AGENT,
20087
20128
  locale: "pt-PT",
20088
- storageState: { cookies, origins: [] }
20129
+ storageState: { cookies: next.cookies, origins: [] }
20089
20130
  });
20090
20131
  }
20091
20132
  async function isLoggedIn(requester) {
@@ -20098,65 +20139,113 @@ function createSession({
20098
20139
  assertNotBlocked(res.status(), "");
20099
20140
  return res.ok() && /data-has-context="true"/.test(await res.text());
20100
20141
  }
20101
- async function launchWindow(chromium) {
20142
+ async function launch(chromium) {
20102
20143
  for (const channel of BROWSER_CHANNELS) {
20103
20144
  try {
20104
- return await chromium.launch({ headless: false, ...channel && { channel } });
20145
+ return await chromium.launch({ headless, ...channel && { channel } });
20105
20146
  } catch {
20106
20147
  }
20107
20148
  }
20108
- throw new Error(NO_BROWSER);
20149
+ return null;
20150
+ }
20151
+ function scrubbed(error62) {
20152
+ for (const secret of [credentials?.email, credentials?.password]) {
20153
+ if (secret) error62.message = String(error62.message).split(secret).join("***");
20154
+ }
20155
+ return error62;
20109
20156
  }
20110
- async function interactiveLogin(savedCookies = null) {
20157
+ async function autoLogin() {
20158
+ if (!credentials) {
20159
+ throw new Error(`falta o ficheiro ${credentialFile} com as linhas AUCHAN_EMAIL=... e AUCHAN_PASSWORD=... (ver o README)`);
20160
+ }
20111
20161
  const { chromium } = await pw();
20112
- const browser = await launchWindow(chromium);
20113
- const closed = new Promise((resolve) => browser.on("disconnected", () => resolve("closed")));
20162
+ const browser = await launch(chromium);
20163
+ if (!browser) throw new Error(NO_BROWSER);
20114
20164
  try {
20115
- const context = await browser.newContext({ locale: "pt-PT" });
20116
- if (savedCookies) await context.addCookies(savedCookies);
20165
+ const context = await browser.newContext({ locale: "pt-PT", userAgent: USER_AGENT });
20117
20166
  const page = await context.newPage();
20118
- log(savedCookies ? "[auth] Abri uma janela do browser: escolhe a entrega ou o levantamento no Auchan." : "[auth] Abri uma janela do browser: faz o login no Auchan e escolhe a entrega ou o levantamento.");
20119
- await throttle(() => page.goto(savedCookies ? `${AUCHAN_BASE}/pt/` : LOGIN_URL, { waitUntil: "domcontentloaded", timeout: 3e4 }));
20120
- const deadline = Date.now() + loginTimeoutMs;
20121
- while (Date.now() < deadline) {
20122
- if (await Promise.race([closed, sleep(pollMs).then(() => "tick")]) === "closed") {
20123
- throw new Error("a janela de login foi fechada antes de o login terminar; tenta de novo");
20124
- }
20125
- const url2 = new URL(page.url());
20126
- if (url2.hostname !== "www.auchan.pt" || /login/i.test(url2.pathname)) continue;
20127
- if (await isLoggedIn(context.request) && await hasDeliveryContext(context.request)) {
20128
- const cookies = await context.cookies();
20129
- writeCookies(cookieFile, cookies);
20130
- log("[auth] Sess\xE3o iniciada.");
20131
- return cookies;
20167
+ log("[auth] A iniciar sess\xE3o no Auchan...");
20168
+ await throttle(() => page.goto(LOGIN_URL, { waitUntil: "domcontentloaded", timeout: 3e4 }));
20169
+ await page.locator("#onetrust-accept-btn-handler").click({ timeout: 2e3 }).catch(() => {
20170
+ });
20171
+ try {
20172
+ await page.locator(EMAIL_FIELD).first().fill(credentials.email, { timeout: 2e4 });
20173
+ await page.locator(PASSWORD_FIELD).first().fill(credentials.password, { timeout: 1e4 });
20174
+ await throttle(() => page.locator(SUBMIT_BUTTON).first().click({ timeout: 1e4 }));
20175
+ } catch {
20176
+ throw new Error(FORM_CHANGED);
20177
+ }
20178
+ try {
20179
+ await page.waitForURL((url2) => url2.hostname === "www.auchan.pt" && !/login/i.test(url2.pathname), { timeout: loginTimeoutMs });
20180
+ } catch {
20181
+ if (await page.locator(CHALLENGE).count()) {
20182
+ throw new BlockedError("o site pediu uma verifica\xE7\xE3o anti-rob\xF4 (CAPTCHA) no login; entra uma vez no auchan.pt pelo browser e tenta mais tarde");
20132
20183
  }
20184
+ throw new Error(LOGIN_FAILED);
20133
20185
  }
20134
- throw new Error("o login n\xE3o terminou em 5 minutos; tenta de novo");
20186
+ if (!await isLoggedIn(context.request)) throw new Error(LOGIN_FAILED);
20187
+ const next = await context.storageState();
20188
+ writeState(cookieFile, next);
20189
+ log("[auth] Sess\xE3o iniciada.");
20190
+ return next;
20191
+ } catch (error62) {
20192
+ if (error62 instanceof BlockedError || error62.message === LOGIN_FAILED || error62.message === FORM_CHANGED || error62.message.startsWith("falta ")) throw scrubbed(error62);
20193
+ throw new Error(LOGIN_FAILED);
20135
20194
  } finally {
20136
20195
  await browser.close().catch(() => {
20137
20196
  });
20138
20197
  }
20139
20198
  }
20199
+ let lastPersist = 0;
20200
+ async function persist(force = false) {
20201
+ if (!api || !state || !force && Date.now() - lastPersist < 5 * 60 * 1e3) return;
20202
+ lastPersist = Date.now();
20203
+ try {
20204
+ const { cookies } = await api.storageState();
20205
+ if (cookies.length) {
20206
+ state = { cookies, origins: state.origins };
20207
+ writeState(cookieFile, state);
20208
+ }
20209
+ } catch {
20210
+ }
20211
+ }
20212
+ function startKeepAlive() {
20213
+ if (timer || !keepAliveMs) return;
20214
+ timer = setInterval(async () => {
20215
+ if (!api || !loggedIn || pending || inFlight || beating) return;
20216
+ beating = true;
20217
+ try {
20218
+ if (await isLoggedIn(api)) {
20219
+ await persist(true);
20220
+ return;
20221
+ }
20222
+ if (credentials) await openApi(await autoLogin());
20223
+ else loggedIn = false;
20224
+ } catch {
20225
+ loggedIn = false;
20226
+ } finally {
20227
+ beating = false;
20228
+ }
20229
+ }, keepAliveMs);
20230
+ timer.unref?.();
20231
+ }
20140
20232
  function ensureSession() {
20141
20233
  if (api && loggedIn) return Promise.resolve("saved");
20142
20234
  pending ??= (async () => {
20143
20235
  try {
20144
- const saved = readCookies(cookieFile);
20236
+ let how = "login";
20237
+ const saved = readState(cookieFile);
20145
20238
  if (saved) {
20146
20239
  await openApi(saved);
20147
- const logged = await isLoggedIn(api);
20148
- if (logged && await hasDeliveryContext(api)) {
20149
- loggedIn = true;
20150
- return "saved";
20151
- }
20152
- log(logged ? "[auth] Falta escolher a entrega." : "[auth] A sess\xE3o guardada expirou.");
20153
- await openApi(await interactiveLogin(logged ? saved : null));
20154
- loggedIn = true;
20155
- return "window";
20240
+ if (await isLoggedIn(api)) how = "saved";
20241
+ else log("[auth] A sess\xE3o guardada expirou; vou entrar outra vez.");
20156
20242
  }
20157
- await openApi(await interactiveLogin());
20243
+ if (how === "login") await openApi(await autoLogin());
20244
+ if (!await hasDeliveryContext(api)) throw new Error(NO_DELIVERY);
20158
20245
  loggedIn = true;
20159
- return "window";
20246
+ await persist(true);
20247
+ startKeepAlive();
20248
+ return how;
20160
20249
  } finally {
20161
20250
  pending = null;
20162
20251
  }
@@ -20171,15 +20260,21 @@ function createSession({
20171
20260
  return { status: res.status(), text: text2, url: res.url() };
20172
20261
  });
20173
20262
  async function request(method, path, form) {
20174
- await ensureSession();
20175
- let result = await call(method, path, form);
20176
- if (/login/i.test(new URL(result.url).pathname)) {
20177
- loggedIn = false;
20263
+ inFlight++;
20264
+ try {
20178
20265
  await ensureSession();
20179
- result = await call(method, path, form);
20180
- if (/login/i.test(new URL(result.url).pathname)) throw new Error("o site n\xE3o aceitou a sess\xE3o; tenta usar a ferramenta login de novo");
20266
+ let result = await call(method, path, form);
20267
+ if (/login/i.test(new URL(result.url).pathname)) {
20268
+ loggedIn = false;
20269
+ await ensureSession();
20270
+ result = await call(method, path, form);
20271
+ if (/login/i.test(new URL(result.url).pathname)) throw new Error("o site n\xE3o aceitou a sess\xE3o; tenta usar a ferramenta login de novo");
20272
+ }
20273
+ await persist();
20274
+ return { status: result.status, text: result.text };
20275
+ } finally {
20276
+ inFlight--;
20181
20277
  }
20182
- return { status: result.status, text: result.text };
20183
20278
  }
20184
20279
  const site = {
20185
20280
  get: (path) => request("get", path),
@@ -20189,6 +20284,11 @@ function createSession({
20189
20284
  return ensureSession();
20190
20285
  }
20191
20286
  async function close() {
20287
+ if (timer) {
20288
+ clearInterval(timer);
20289
+ timer = null;
20290
+ }
20291
+ await persist(true);
20192
20292
  const current = api;
20193
20293
  api = null;
20194
20294
  loggedIn = false;
@@ -20249,12 +20349,12 @@ var CART_TOOLS = [
20249
20349
  var LOGIN_TOOLS = [
20250
20350
  {
20251
20351
  name: "login",
20252
- description: "Connect your Auchan account. Opens a browser window on the Auchan.pt login page; the person logs in there and chooses home delivery or pickup with a postal code (Auchan needs this before any cart change), then the window closes. Tell the person this before calling. The password is never seen or stored. Cart tools call this automatically when there is no session.",
20352
+ description: "Connect your Auchan account with the email and password in credentials.env (no window). Logs in again by itself whenever the session ends. Cart tools call this automatically.",
20253
20353
  inputSchema: { type: "object", properties: {} }
20254
20354
  },
20255
20355
  {
20256
20356
  name: "logout",
20257
- description: "Disconnect your Auchan account: deletes the saved session from this computer.",
20357
+ description: "Disconnect your Auchan account: deletes the saved session from this computer (credentials.env is kept).",
20258
20358
  inputSchema: { type: "object", properties: {} }
20259
20359
  }
20260
20360
  ];
@@ -20287,7 +20387,13 @@ var AuchanServer = class {
20287
20387
  const paths = resolveStatePaths(env);
20288
20388
  this.cacheDir = paths.cacheDir;
20289
20389
  this.fetchImpl = fetchImpl;
20290
- this.session = session ?? createSession({ cookieFile: paths.cookieFile });
20390
+ this.credentialsFound = Boolean(loadCredentials(env));
20391
+ this.session = session ?? createSession({
20392
+ credentials: loadCredentials(env),
20393
+ credentialFile: paths.sharedCredentialFile,
20394
+ cookieFile: paths.cookieFile,
20395
+ headless: env.AUCHAN_LOGIN_HEADLESS !== "false"
20396
+ });
20291
20397
  this.tools = [SEARCH_TOOL, ...CART_TOOLS, ...LOGIN_TOOLS, CLOSE_TOOL];
20292
20398
  this.server = new Server({ name: "auchan-mcp", version: VERSION }, { capabilities: { tools: {} } });
20293
20399
  this.server.onclose = () => {
@@ -20331,7 +20437,7 @@ var AuchanServer = class {
20331
20437
  );
20332
20438
  }
20333
20439
  case "login":
20334
- return text(await this.session.login() === "saved" ? "J\xE1 tens a sess\xE3o do Auchan iniciada." : "Sess\xE3o do Auchan iniciada. J\xE1 posso usar o teu carrinho.");
20440
+ return text(await this.session.login() === "saved" ? "J\xE1 tens a sess\xE3o do Auchan iniciada." : "Sess\xE3o do Auchan iniciada com o ficheiro de credenciais. J\xE1 posso usar o teu carrinho.");
20335
20441
  case "logout":
20336
20442
  await this.session.logout();
20337
20443
  return text("Sess\xE3o do Auchan terminada: apaguei a sess\xE3o guardada neste computador.");
@@ -20345,7 +20451,7 @@ var AuchanServer = class {
20345
20451
  }
20346
20452
  async start() {
20347
20453
  await this.server.connect(new StdioServerTransport());
20348
- console.error(`Auchan MCP v${VERSION} started`);
20454
+ console.error(`Auchan MCP v${VERSION} started${this.credentialsFound ? "" : " (search only until credentials.env exists)"}`);
20349
20455
  }
20350
20456
  };
20351
20457
  if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auchan-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Servidor MCP para o Auchan.pt: pesquisa de produtos e gestão do carrinho.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,6 +28,7 @@
28
28
  "license": "MIT",
29
29
  "files": [
30
30
  "dist/",
31
+ ".env.example",
31
32
  "LICENSE",
32
33
  "README.md"
33
34
  ],