continente-mcp 3.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.
@@ -0,0 +1,158 @@
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
+ }