@vierratale/ai 0.1.0-beta.9 → 0.1.0-fixfetch-beta9.5
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/package.json +2 -2
- package/src/config.js +1 -1
- package/src/ui/branding.js +1 -1
- package/src/utils/webfetch.js +159 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vierratale/ai",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-fixfetch-beta9.5",
|
|
4
4
|
"description": "VierrataleAI - Intelligent terminal assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"start": "node bin/vierrataleai.js",
|
|
21
|
-
"test": "node --test test
|
|
21
|
+
"test": "node --test \"test/*.test.js\"",
|
|
22
22
|
"postinstall": "node scripts/postinstall.js"
|
|
23
23
|
},
|
|
24
24
|
"keywords": ["ai", "chatbot", "terminal", "assistant", "llm"],
|
package/src/config.js
CHANGED
package/src/ui/branding.js
CHANGED
package/src/utils/webfetch.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
const UA =
|
|
1
|
+
const UA =
|
|
2
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
|
|
3
|
+
'(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
|
|
2
4
|
|
|
3
5
|
const MAX_BYTES = 200000; // 200KB cap
|
|
4
6
|
const MAX_TEXT = 8000; // ~8k chars of readable text
|
|
@@ -8,6 +10,40 @@ const BLOCK_TAGS = new Set([
|
|
|
8
10
|
'nav', 'footer', 'aside', 'iframe', 'form', 'button', 'noscript',
|
|
9
11
|
]);
|
|
10
12
|
|
|
13
|
+
// Body markers that indicate an anti-bot wall instead of real content.
|
|
14
|
+
const BLOCK_MARKERS = [
|
|
15
|
+
'just a moment',
|
|
16
|
+
'client challenge',
|
|
17
|
+
'checking your browser',
|
|
18
|
+
'enable javascript and cookies',
|
|
19
|
+
'cf-chl',
|
|
20
|
+
'challenge-platform',
|
|
21
|
+
'attention required!',
|
|
22
|
+
'powershell install',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Browser-like headers so header-based bot checks see a real browser.
|
|
26
|
+
function browserHeaders(accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8') {
|
|
27
|
+
return {
|
|
28
|
+
'User-Agent': UA,
|
|
29
|
+
Accept: accept,
|
|
30
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
31
|
+
Connection: 'keep-alive',
|
|
32
|
+
'Upgrade-Insecure-Requests': '1',
|
|
33
|
+
'Sec-Fetch-Dest': accept.startsWith('application/json') ? 'empty' : 'document',
|
|
34
|
+
'Sec-Fetch-Mode': accept.startsWith('application/json') ? 'cors' : 'navigate',
|
|
35
|
+
'Sec-Fetch-Site': 'none',
|
|
36
|
+
'Sec-Fetch-User': accept.startsWith('application/json') ? '?0' : '?1',
|
|
37
|
+
'Cache-Control': 'no-cache',
|
|
38
|
+
Pragma: 'no-cache',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isBlockedBody(html) {
|
|
43
|
+
const s = String(html || '').toLowerCase().slice(0, 4000);
|
|
44
|
+
return BLOCK_MARKERS.some((m) => s.includes(m));
|
|
45
|
+
}
|
|
46
|
+
|
|
11
47
|
export class WebFetch {
|
|
12
48
|
static normalizeUrl(input) {
|
|
13
49
|
const trimmed = (input || '').trim();
|
|
@@ -34,10 +70,45 @@ export class WebFetch {
|
|
|
34
70
|
throw new Error('Invalid URL. Provide a valid http(s) address.');
|
|
35
71
|
}
|
|
36
72
|
|
|
73
|
+
let finalUrl = url;
|
|
74
|
+
let html = '';
|
|
75
|
+
let blocked = false;
|
|
76
|
+
try {
|
|
77
|
+
const res = await this._fetchHtml(url);
|
|
78
|
+
finalUrl = res.url;
|
|
79
|
+
html = res.html;
|
|
80
|
+
const status = res.status;
|
|
81
|
+
blocked = status !== 200 || isBlockedBody(html);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
blocked = true;
|
|
84
|
+
// _fetchHtml errors are network/cert/PROTOCOL issues; a fallback may
|
|
85
|
+
// still succeed (e.g. registry API is reachable even when the HTML
|
|
86
|
+
// page's CDN is not).
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Bot-walled npm/pypi pages degrade to their open JSON API.
|
|
90
|
+
if (blocked) {
|
|
91
|
+
const registry = await this._registryFallback(finalUrl || url, maxText);
|
|
92
|
+
if (registry) return registry;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (blocked) {
|
|
96
|
+
const status = 'blocked';
|
|
97
|
+
throw new Error(`Request blocked (${status}) for ${finalUrl || url}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
url: finalUrl,
|
|
102
|
+
title: this._extractTitle(html),
|
|
103
|
+
text: this._extractText(html).slice(0, maxText),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static async _fetchHtml(url) {
|
|
37
108
|
let resp;
|
|
38
109
|
try {
|
|
39
110
|
resp = await fetch(url, {
|
|
40
|
-
headers:
|
|
111
|
+
headers: browserHeaders(),
|
|
41
112
|
redirect: 'follow',
|
|
42
113
|
signal: AbortSignal.timeout(15000),
|
|
43
114
|
});
|
|
@@ -45,11 +116,6 @@ export class WebFetch {
|
|
|
45
116
|
throw new Error(`Could not reach ${url}`);
|
|
46
117
|
}
|
|
47
118
|
|
|
48
|
-
if (!resp.ok) {
|
|
49
|
-
throw new Error(`Request failed (${resp.status}) for ${url}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Refuse to read the whole body past the cap.
|
|
53
119
|
const reader = resp.body.getReader();
|
|
54
120
|
const decoder = new TextDecoder('utf-8', { fatal: false });
|
|
55
121
|
let html = '';
|
|
@@ -63,13 +129,95 @@ export class WebFetch {
|
|
|
63
129
|
reader.cancel();
|
|
64
130
|
html += decoder.decode();
|
|
65
131
|
|
|
132
|
+
return { url: resp.url || url, html, status: resp.status };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Registry fallback (npm + PyPI) via their open JSON APIs ---------- //
|
|
136
|
+
|
|
137
|
+
static async _registryFallback(url, maxText) {
|
|
138
|
+
const npm = this._npmSpec(url);
|
|
139
|
+
if (npm) {
|
|
140
|
+
try {
|
|
141
|
+
return await this._npmFallback(npm, url, maxText);
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
const pypi = this._pypiSpec(url);
|
|
145
|
+
if (pypi) {
|
|
146
|
+
try {
|
|
147
|
+
return await this._pypiFallback(pypi, url, maxText);
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static _npmSpec(url) {
|
|
154
|
+
const m = /npmjs\.\w+\/package\/([@a-zA-Z0-9._~-]+(?:\/[@a-zA-Z0-9._~-]+)?)/i.exec(url);
|
|
155
|
+
return m ? m[1] : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static _pypiSpec(url) {
|
|
159
|
+
const m = /pypi\.org\/project\/([a-zA-Z0-9._-]+)/i.exec(url);
|
|
160
|
+
return m ? m[1] : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
static async _fetchJson(apiUrl) {
|
|
164
|
+
const resp = await fetch(apiUrl, {
|
|
165
|
+
headers: browserHeaders('application/json'),
|
|
166
|
+
redirect: 'follow',
|
|
167
|
+
signal: AbortSignal.timeout(15000),
|
|
168
|
+
});
|
|
169
|
+
if (!resp.ok) throw new Error(`API ${resp.status}`);
|
|
170
|
+
return resp.json();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static async _npmFallback(spec, pageUrl, maxText) {
|
|
174
|
+
const pkg = encodeURIComponent(spec); // '@vierratale/ai' -> %40vierratale%2Fai
|
|
175
|
+
const data = await this._fetchJson(`https://registry.npmjs.org/${pkg}`);
|
|
176
|
+
const version =
|
|
177
|
+
(data['dist-tags'] && (data['dist-tags'].latest || data['dist-tags'].beta)) ||
|
|
178
|
+
Object.keys(data.versions || {})[0];
|
|
179
|
+
const meta = (data.versions && data.versions[version]) || {};
|
|
180
|
+
const deps = meta.dependencies || data.dependencies || {};
|
|
181
|
+
const lines = [
|
|
182
|
+
`${data.name} (npm package)`,
|
|
183
|
+
`Latest version: ${version}`,
|
|
184
|
+
meta.description || data.description || '',
|
|
185
|
+
`License: ${meta.license || data.license || 'N/A'}`,
|
|
186
|
+
`Dependencies: ${Object.keys(deps).length}`,
|
|
187
|
+
'',
|
|
188
|
+
];
|
|
189
|
+
const readme = String(data.readme || '').trim();
|
|
190
|
+
if (readme) lines.push(readme.slice(0, maxText));
|
|
66
191
|
return {
|
|
67
|
-
url:
|
|
68
|
-
title:
|
|
69
|
-
text:
|
|
192
|
+
url: pageUrl,
|
|
193
|
+
title: `${data.name} · npm`,
|
|
194
|
+
text: lines.filter(Boolean).join('\n').slice(0, maxText),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static async _pypiFallback(spec, pageUrl, maxText) {
|
|
199
|
+
const data = await this._fetchJson(`https://pypi.org/pypi/${encodeURIComponent(spec)}/json`);
|
|
200
|
+
const info = data.info || {};
|
|
201
|
+
const lines = [
|
|
202
|
+
`${info.name} (PyPI package)`,
|
|
203
|
+
`Version: ${info.version}`,
|
|
204
|
+
info.summary || '',
|
|
205
|
+
`Requires Python: ${info.requires_python || 'N/A'}`,
|
|
206
|
+
`License: ${info.license || 'N/A'}`,
|
|
207
|
+
`Home: ${info.home_page || info.project_url || 'N/A'}`,
|
|
208
|
+
'',
|
|
209
|
+
];
|
|
210
|
+
const desc = String(info.description || '').trim();
|
|
211
|
+
if (desc) lines.push(desc.slice(0, maxText));
|
|
212
|
+
return {
|
|
213
|
+
url: pageUrl,
|
|
214
|
+
title: `${info.name} · PyPI`,
|
|
215
|
+
text: lines.filter(Boolean).join('\n').slice(0, maxText),
|
|
70
216
|
};
|
|
71
217
|
}
|
|
72
218
|
|
|
219
|
+
// --- extraction -------------------------------------------------------- //
|
|
220
|
+
|
|
73
221
|
static _extractTitle(html) {
|
|
74
222
|
const m = /<title[^>]*>([^<]*)<\/title>/i.exec(html);
|
|
75
223
|
return m ? this._stripEntities(m[1].trim()) : '';
|
|
@@ -112,4 +260,4 @@ export class WebFetch {
|
|
|
112
260
|
.replace(/\s+/g, ' ')
|
|
113
261
|
.trim();
|
|
114
262
|
}
|
|
115
|
-
}
|
|
263
|
+
}
|