@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vierratale/ai",
3
- "version": "0.1.0-beta.9",
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
@@ -18,7 +18,7 @@ const DEFAULTS = {
18
18
  temperature: 0.7,
19
19
  maxTokens: 4096,
20
20
  keepAlive: '5m',
21
- numThreads: 6,
21
+ numThreads: 8,
22
22
  commandTimeoutMs: 60000,
23
23
  planTimeoutMs: 45000,
24
24
  };
@@ -1,6 +1,6 @@
1
1
  export const Branding = {
2
2
  APP_NAME: 'VierrataleAI',
3
- VERSION: '0.1.0-beta.9',
3
+ VERSION: '0.1.0-fixfetch-beta9.5',
4
4
 
5
5
  colors: {
6
6
  primary: '\x1b[38;2;124;58;237m',
@@ -1,4 +1,6 @@
1
- const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36';
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: { 'User-Agent': UA, Accept: 'text/html,application/xhtml+xml' },
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: resp.url || url,
68
- title: this._extractTitle(html),
69
- text: this._extractText(html).slice(0, maxText),
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
+ }