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.
- package/.env.example +19 -0
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/check-change.js +157 -0
- package/continente-backup.js +166 -0
- package/continente-cookie-reader.py +164 -0
- package/continente-keepalive.py +135 -0
- package/package.json +55 -0
- package/skills/groceries/SKILL.md +61 -0
- package/src/auth.js +180 -0
- package/src/changedetector.js +158 -0
- package/src/index.js +613 -0
- package/src/utils.js +15 -0
|
@@ -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
|
+
}
|