continente-mcp 3.1.0 → 4.0.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/src/auth.js DELETED
@@ -1,180 +0,0 @@
1
- import { chromium } from 'playwright';
2
-
3
- const CONTINENTE_LOGIN_URL = 'https://login.continente.pt/u/login?clientId=NLR6WHyO8Iba4eRS&lang=pt-PT';
4
- const CONTINENTE_MAIN = 'https://www.continente.pt';
5
- const SESSION_DIR = `${process.env.HOME}/.continente`;
6
-
7
- export class Authenticator {
8
- constructor() {
9
- this.browser = null;
10
- this.context = null;
11
- this.page = null;
12
- this.cookies = [];
13
- }
14
-
15
- async ensureBrowser() {
16
- if (!this.browser) {
17
- this.browser = await chromium.launch({ headless: true });
18
- }
19
- if (!this.context) {
20
- this.context = await this.browser.newContext({
21
- userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
22
- locale: 'pt-PT'
23
- });
24
- // Load existing cookies if available
25
- await this.loadCookies();
26
- }
27
- if (!this.page) {
28
- this.page = await this.context.newPage();
29
- }
30
- return this.page;
31
- }
32
-
33
- getCookiesPath() {
34
- return `${SESSION_DIR}/session.json`;
35
- }
36
-
37
- async saveCookies() {
38
- const fs = await import('fs');
39
- try {
40
- fs.mkdirSync(SESSION_DIR, { recursive: true });
41
- } catch (e) {}
42
- const cookies = await this.context.cookies();
43
- fs.writeFileSync(this.getCookiesPath(), JSON.stringify(cookies, null, 2));
44
- }
45
-
46
- async loadCookies() {
47
- const fs = await import('fs');
48
- try {
49
- const data = fs.readFileSync(this.getCookiesPath(), 'utf8');
50
- const cookies = JSON.parse(data);
51
- if (cookies.length > 0) {
52
- await this.context.addCookies(cookies);
53
- this.cookies = cookies;
54
- }
55
- } catch (e) {
56
- // No cookies yet
57
- }
58
- }
59
-
60
- async isLoggedIn() {
61
- try {
62
- const page = await this.ensureBrowser();
63
- await page.goto(CONTINENTE_MAIN, { waitUntil: 'domcontentloaded', timeout: 15000 });
64
- // Look for the account/logged-in state
65
- const loginLink = await page.$('a[href*="logout"], a[href*="sair"], [class*="user"], [class*="account"]');
66
- const pageContent = await page.content();
67
- // If we see a logged-in indicator (e.g., user name in header)
68
- const isLogged = pageContent.includes('data-testid="user"') ||
69
- pageContent.includes('class="logged-in') ||
70
- pageContent.includes('bjzIjUWquR'); // some session marker
71
- return false; // Conservative - always try to re-auth for now
72
- } catch (e) {
73
- return false;
74
- }
75
- }
76
-
77
- async login(email, password) {
78
- const fs = await import('fs');
79
- try { fs.mkdirSync(SESSION_DIR, { recursive: true }); } catch (e) {}
80
-
81
- const page = await this.ensureBrowser();
82
-
83
- console.error(`[Auth] Starting login flow for ${email}`);
84
-
85
- // Go to login page
86
- await page.goto(CONTINENTE_LOGIN_URL, { waitUntil: 'networkidle', timeout: 30000 });
87
- await page.waitForTimeout(2000);
88
-
89
- // Step 1: Enter email
90
- console.error('[Auth] Step 1: Entering email...');
91
- const emailInput = await page.waitForSelector('input[type="email"], input[type="text"]', { timeout: 10000 }).catch(() => null);
92
- if (!emailInput) {
93
- throw new Error('Could not find email input field');
94
- }
95
-
96
- await emailInput.fill(email);
97
- await page.waitForTimeout(500);
98
-
99
- // Click continue/next
100
- const continueBtn = await page.waitForSelector('button[type="submit"], button:has-text("Avançar"), button:has-text("Continuar")', { timeout: 5000 }).catch(() => null);
101
- if (continueBtn) {
102
- await continueBtn.click();
103
- } else {
104
- await page.keyboard.press('Enter');
105
- }
106
-
107
- await page.waitForTimeout(2000);
108
-
109
- // Step 2: Enter password
110
- console.error('[Auth] Step 2: Entering password...');
111
- const passwordInput = await page.waitForSelector('input[type="password"]', { timeout: 10000 }).catch(() => null);
112
- if (!passwordInput) {
113
- // Maybe already logged in or different flow
114
- const currentUrl = page.url();
115
- console.error(`[Auth] No password field found. Current URL: ${currentUrl}`);
116
- throw new Error('Password field not found - login flow may have changed');
117
- }
118
-
119
- await passwordInput.fill(password);
120
- await page.waitForTimeout(500);
121
-
122
- // Submit
123
- const submitBtn = await page.waitForSelector('button[type="submit"], button:has-text("Entrar"), button:has-text("Login")', { timeout: 5000 }).catch(() => null);
124
- if (submitBtn) {
125
- await submitBtn.click();
126
- } else {
127
- await page.keyboard.press('Enter');
128
- }
129
-
130
- // Wait for redirect to main site
131
- await page.waitForTimeout(3000);
132
- await page.waitForURL(/continente\.pt/, { timeout: 15000 }).catch(() => {});
133
-
134
- const finalUrl = page.url();
135
- console.error(`[Auth] Final URL after login: ${finalUrl}`);
136
-
137
- // Save session cookies
138
- await this.saveCookies();
139
- this.cookies = await this.context.cookies();
140
-
141
- console.error(`[Auth] Login successful, ${this.cookies.length} cookies saved`);
142
-
143
- return { success: true, url: finalUrl, cookies: this.cookies.length };
144
- }
145
-
146
- async logout() {
147
- const fs = await import('fs');
148
- if (this.page) {
149
- try {
150
- await this.page.goto(`${CONTINENTE_MAIN}/logout`, { timeout: 5000 }).catch(() => {});
151
- } catch (e) {}
152
- await this.page.close();
153
- this.page = null;
154
- }
155
- if (this.context) {
156
- await this.context.close();
157
- this.context = null;
158
- }
159
- if (this.browser) {
160
- await this.browser.close();
161
- this.browser = null;
162
- }
163
- // Remove saved cookies
164
- try { fs.unlinkSync(this.getCookiesPath()); } catch (e) {}
165
- console.error('[Auth] Logged out, session cleared');
166
- }
167
-
168
- async getSessionCookies() {
169
- if (!this.context && !this.browser) {
170
- await this.ensureBrowser();
171
- }
172
- return this.context ? await this.context.cookies() : [];
173
- }
174
-
175
- async close() {
176
- if (this.page) { try { await this.page.close(); } catch (e) {} this.page = null; }
177
- if (this.context) { try { await this.context.close(); } catch (e) {} this.context = null; }
178
- if (this.browser) { try { await this.browser.close(); } catch (e) {} this.browser = null; }
179
- }
180
- }
@@ -1,158 +0,0 @@
1
- import { createHash } from 'crypto';
2
- import { readFileSync, writeFileSync, existsSync } from 'fs';
3
- import { join } from 'path';
4
- import * as cheerio from 'cheerio';
5
-
6
- const STATE_DIR = `${process.env.HOME}/.continente`;
7
-
8
- export class ChangeDetector {
9
- constructor() {
10
- this.statePath = join(STATE_DIR, 'fingerprint.json');
11
- }
12
-
13
- getStatePath() {
14
- return this.statePath;
15
- }
16
-
17
- /**
18
- * Extract structural fingerprint from HTML — ignores prices/text that change
19
- * but captures the DOM structure (tags, class patterns, attribute presence)
20
- */
21
- extractFingerprint(html) {
22
- const $ = cheerio.load(html);
23
-
24
- // Remove dynamic content
25
- $('script, style, noscript, iframe, svg').remove();
26
- $('[class*="analytics"], [class*="tracking"], [data-datalayer]').remove();
27
-
28
- // Get structural signature: count of key element types + class patterns
29
- const structure = {
30
- forms: $('form').length,
31
- inputs: $('input').length,
32
- buttons: $('button').length,
33
- links: $('a[href*="/produto/"]').length,
34
- productCards: $('[class*="product"], [class*="card"]').filter((i, el) => {
35
- return $(el).find('a[href*="/produto/"]').length > 0 || $(el).text().includes('€');
36
- }).length,
37
- priceElements: $('[class*="price"], .product-price, [data-price]').length,
38
- searchInput: $('input[type="search"], input[name*="search"], input[placeholder*="Pesquisar"]').length,
39
- productGrid: $('[class*="grid"], [class*="listing"], [class*="results"]').length,
40
- // Get CSS class prefixes (ignoring specific names like "product-123")
41
- classPrefixes: [...new Set(
42
- $('[class]').map((i, el) => {
43
- const cls = $(el).attr('class') || '';
44
- return cls.split(' ').map(c => c.replace(/-\d+/g, '-X').replace(/[a-z]+\d+$/gi, c.match(/[a-z]+/i)?.[0] + 'X')).join(' ');
45
- }).get()
46
- )].filter(Boolean).slice(0, 50),
47
- };
48
-
49
- // Hash of structure
50
- const hash = createHash('sha256')
51
- .update(JSON.stringify(structure))
52
- .digest('hex')
53
- .substring(0, 16);
54
-
55
- return {
56
- structure,
57
- hash,
58
- timestamp: new Date().toISOString()
59
- };
60
- }
61
-
62
- /**
63
- * Check if a URL's content has changed structurally
64
- */
65
- async checkUrl(url, headers = {}) {
66
- const response = await fetch(url, {
67
- headers: {
68
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
69
- 'Accept': 'text/html',
70
- ...headers
71
- }
72
- });
73
- const html = await response.text();
74
- return this.extractFingerprint(html);
75
- }
76
-
77
- /**
78
- * Save current state as baseline
79
- */
80
- async saveBaseline(html, label = 'default') {
81
- const fingerprint = this.extractFingerprint(html);
82
- const state = {
83
- label,
84
- savedAt: new Date().toISOString(),
85
- fingerprint
86
- };
87
-
88
- try {
89
- const { mkdirSync } = await import('fs');
90
- mkdirSync(STATE_DIR, { recursive: true });
91
- } catch (e) {}
92
-
93
- writeFileSync(this.statePath, JSON.stringify(state, null, 2));
94
- return fingerprint;
95
- }
96
-
97
- /**
98
- * Compare current HTML against saved baseline
99
- */
100
- compare(html) {
101
- if (!existsSync(this.statePath)) {
102
- return { changed: null, reason: 'No baseline saved yet' };
103
- }
104
-
105
- try {
106
- const saved = JSON.parse(readFileSync(this.statePath, 'utf8'));
107
- const current = this.extractFingerprint(html);
108
-
109
- const savedHash = saved.fingerprint?.hash;
110
- const currentHash = current.hash;
111
-
112
- if (savedHash === currentHash) {
113
- return { changed: false, saved: saved.fingerprint, current };
114
- }
115
-
116
- // Report what changed
117
- const savedStr = saved.fingerprint?.structure || {};
118
- const currentStr = current.structure || {};
119
-
120
- const differences = {};
121
- for (const key of Object.keys(savedStr)) {
122
- if (JSON.stringify(savedStr[key]) !== JSON.stringify(currentStr[key])) {
123
- differences[key] = { before: savedStr[key], after: currentStr[key] };
124
- }
125
- }
126
-
127
- return {
128
- changed: true,
129
- saved: saved.fingerprint,
130
- current,
131
- differences,
132
- savedAt: saved.savedAt
133
- };
134
- } catch (e) {
135
- return { changed: null, reason: e.message };
136
- }
137
- }
138
-
139
- /**
140
- * Get current status
141
- */
142
- getStatus() {
143
- if (!existsSync(this.statePath)) {
144
- return { hasBaseline: false };
145
- }
146
- try {
147
- const saved = JSON.parse(readFileSync(this.statePath, 'utf8'));
148
- return {
149
- hasBaseline: true,
150
- savedAt: saved.savedAt,
151
- label: saved.label,
152
- hash: saved.fingerprint?.hash
153
- };
154
- } catch (e) {
155
- return { hasBaseline: false, error: e.message };
156
- }
157
- }
158
- }