@vierratale/ai 0.1.0-beta.14 → 0.1.0-beta.16

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.
@@ -1,364 +0,0 @@
1
- import { createWriteStream, mkdirSync, rmSync, writeFileSync } from 'fs';
2
- import { join } from 'path';
3
- import { execFile } from 'child_process';
4
- import { promisify } from 'util';
5
- import { tmpdir } from 'os';
6
- import { browserHeaders, isBlockedBody } from './webfetch.js';
7
-
8
- const execFileAsync = promisify(execFile);
9
-
10
- const MAX_DOWNLOAD_BYTES = 500 * 1024 * 1024; // 500MB cap
11
-
12
- const ARCHIVE_EXT = /\.(zip|tar|tar\.gz|tgz|tar\.bz2|tbz2|gz|bz2|7z|rar|xz)$/i;
13
-
14
- const MIME_EXT = {
15
- 'application/zip': 'zip',
16
- 'application/x-zip-compressed': 'zip',
17
- 'application/x-tar': 'tar',
18
- 'application/gzip': 'gz',
19
- 'application/x-gzip': 'gz',
20
- 'image/png': 'png',
21
- 'image/jpeg': 'jpg',
22
- 'image/gif': 'gif',
23
- 'image/webp': 'webp',
24
- 'image/svg+xml': 'svg',
25
- 'application/pdf': 'pdf',
26
- 'application/json': 'json',
27
- 'text/html': 'html',
28
- 'text/plain': 'txt',
29
- 'text/markdown': 'md',
30
- 'text/csv': 'csv',
31
- 'application/msword': 'doc',
32
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
33
- 'application/vnd.ms-excel': 'xls',
34
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
35
- 'application/octet-stream': 'bin',
36
- };
37
-
38
- function stripInvalidFilenameChars(name) {
39
- // eslint-disable-next-line no-control-regex
40
- return String(name || '')
41
- .replace(/[\\/:*?"<>|\x00-\x1f]/g, '_')
42
- .replace(/^\.+/, '')
43
- .replace(/\.+$/, '')
44
- .trim()
45
- .slice(0, 200) || 'download';
46
- }
47
-
48
- function baseNameFromUrl(url) {
49
- try {
50
- const u = new URL(url);
51
- const name = decodeURIComponent(u.pathname.split('/').filter(Boolean).pop() || '');
52
- return name || (u.hostname || 'download').replace(/^www\./, '');
53
- } catch {
54
- return 'download';
55
- }
56
- }
57
-
58
- function fileNameFromHeaders(url, contentType) {
59
- const base = baseNameFromUrl(url);
60
- if (contentType) {
61
- const ct = contentType.split(';')[0].trim().toLowerCase();
62
- const ext = MIME_EXT[ct];
63
- if (ext && !base.toLowerCase().endsWith(`.${ext}`)) {
64
- return `${stripInvalidFilenameChars(base)}.${ext}`;
65
- }
66
- }
67
- return stripInvalidFilenameChars(base);
68
- }
69
-
70
- function isArchiveUrl(url) {
71
- return ARCHIVE_EXT.test(String(url).split('?')[0]);
72
- }
73
-
74
- function isDirectoryListing(html) {
75
- const s = String(html || '').slice(0, 20000).toLowerCase();
76
- const hasLinks = /<a href=/i.test(s);
77
- if (!hasLinks) return false;
78
- return (
79
- /<a[^>]+href=[^>]*>\s*\.\.[\s<]/.test(s) ||
80
- /(?:index of \/|directory listing for|parent directory)/.test(s) ||
81
- /<h1>.*listing.*<\/h1>/.test(s)
82
- );
83
- }
84
-
85
- function joinUrl(base, rel) {
86
- try {
87
- return new URL(rel, base).toString();
88
- } catch {
89
- return null;
90
- }
91
- }
92
-
93
- function sameDomain(a, b) {
94
- try {
95
- return new URL(a).hostname === new URL(b).hostname;
96
- } catch {
97
- return false;
98
- }
99
- }
100
-
101
- function isGithubRepoUrl(url) {
102
- return /^https?:\/\/github\.com\/[\w.-]+\/[\w.-]+/i.test(url);
103
- }
104
-
105
- export class Downloader {
106
- static async fetchStream(url, { timeout = 30000, headers = {} } = {}) {
107
- const resp = await fetch(url, {
108
- headers: { ...browserHeaders('*/*'), ...headers },
109
- redirect: 'follow',
110
- signal: AbortSignal.timeout(timeout),
111
- });
112
- if (!resp.ok) throw new Error(`HTTP ${resp.status} for ${url}`);
113
- return resp;
114
- }
115
-
116
- // Stream a raw body to a file. Returns metadata.
117
- static async downloadFile(url, { dir = process.cwd(), filename, timeout = 30000, onProgress } = {}) {
118
- mkdirSync(dir, { recursive: true });
119
- const resp = await this.fetchStream(url, { timeout });
120
- const contentType = resp.headers.get('content-type') || '';
121
- const finalUrl = resp.url || url;
122
- const name = filename || fileNameFromHeaders(finalUrl, contentType);
123
- const dest = join(dir, stripInvalidFilenameChars(name));
124
-
125
- const reader = resp.body.getReader();
126
- const stream = createWriteStream(dest);
127
- let received = 0;
128
-
129
- try {
130
- for (;;) {
131
- const { done, value } = await reader.read();
132
- if (done) break;
133
- received += value.length;
134
- if (received > MAX_DOWNLOAD_BYTES) {
135
- throw new Error(`Download exceeded ${MAX_DOWNLOAD_BYTES} byte cap`);
136
- }
137
- await new Promise((resolve, reject) => stream.write(value, (err) => (err ? reject(err) : resolve())));
138
- }
139
- } catch (err) {
140
- await new Promise((resolve) => stream.close(resolve));
141
- rmSync(dest, { force: true });
142
- throw err;
143
- }
144
- await new Promise((resolve) => stream.end(resolve));
145
- if (onProgress) onProgress(received, received);
146
- return { ok: true, path: dest, size: received, contentType, url: finalUrl, name };
147
- }
148
-
149
- static async extractArchive(archivePath, destDir) {
150
- const file = String(archivePath).toLowerCase();
151
- mkdirSync(destDir, { recursive: true });
152
- if (file.endsWith('.zip')) {
153
- await execFileAsync('unzip', ['-o', archivePath, '-d', destDir]);
154
- } else if (file.endsWith('.tar.gz') || file.endsWith('.tgz') || file.endsWith('.tar.bz2') ||
155
- file.endsWith('.tbz2') || file.endsWith('.tar.xz') || file.endsWith('.txz') || file.endsWith('.tar')) {
156
- await execFileAsync('tar', ['-xf', archivePath, '-C', destDir]);
157
- } else if (file.endsWith('.7z')) {
158
- await execFileAsync('7z', ['x', archivePath, `-o${destDir}`, '-y']);
159
- } else if (file.endsWith('.rar')) {
160
- await execFileAsync('unrar', ['x', archivePath, destDir]);
161
- } else {
162
- throw new Error(`Cannot extract that archive: ${archivePath}`);
163
- }
164
- }
165
-
166
- // Zip a folder. destZip must end in .zip or .tar.gz.
167
- static async zipFolder(srcDir, destZip) {
168
- mkdirSync(srcDir, { recursive: true });
169
- const lower = destZip.toLowerCase();
170
- const absZip = join(process.cwd(), destZip);
171
- if (lower.endsWith('.zip')) {
172
- await execFileAsync('zip', ['-r', absZip, '.'], { cwd: srcDir });
173
- } else if (lower.endsWith('.tar.gz') || lower.endsWith('.tgz')) {
174
- await execFileAsync('tar', ['-czf', absZip, '-C', srcDir, '.']);
175
- } else {
176
- throw new Error('Destination must end in .zip or .tar.gz');
177
- }
178
- const { statSync } = await import('fs');
179
- return { ok: true, path: absZip, size: statSync(absZip).size };
180
- }
181
-
182
- static async download(url, { dir = process.cwd(), filename, timeout = 30000, onProgress, extract = true } = {}) {
183
- const resp = await this.fetchStream(url, { timeout });
184
- const contentType = resp.headers.get('content-type') || '';
185
- const finalUrl = resp.url || url;
186
- const isHtml = /text\/html|application\/xhtml/i.test(contentType);
187
- const isPlain = /text\/plain|text\/markdown|application\/json|\+json|text\/csv/i.test(contentType);
188
-
189
- // We must peek the body once. For HTML we buffer it (capped); otherwise
190
- // stream it to disk directly.
191
- if (isHtml || isPlain) {
192
- const buf = Buffer.from(await resp.arrayBuffer());
193
- const html = buf.toString('utf8');
194
- if (typeof onProgress === 'function') onProgress(buf.length, buf.length);
195
-
196
- if (isHtml && isBlockedBody(html)) {
197
- throw new Error('Download blocked by anti-bot protection.');
198
- }
199
-
200
- // github repo URL -> grab the repo zip and extract it (its HTML page
201
- // would otherwise just be saved as a file).
202
- if (isGithubRepoUrl(finalUrl)) {
203
- return this._downloadGithub(finalUrl, { dir, filename, onProgress });
204
- }
205
-
206
- // HTML directory listing -> recursive folder download.
207
- if (isHtml && isDirectoryListing(html)) {
208
- return this._downloadListing(finalUrl, html, { dir, filename, onProgress });
209
- }
210
-
211
- // Otherwise save plain text / html as a file.
212
- const kind = 'file';
213
- const name = filename || fileNameFromHeaders(finalUrl, contentType);
214
- const dest = join(dir, stripInvalidFilenameChars(name));
215
- mkdirSync(dir, { recursive: true });
216
- writeFileSync(dest, html, 'utf8');
217
- return { ok: true, path: dest, size: buf.length, kind, url: finalUrl, name };
218
- }
219
-
220
- // Binary / archive: stream to disk.
221
- const result = await this.downloadFile(finalUrl, { dir, filename, timeout, onProgress });
222
- let kind = 'file';
223
- if (isArchiveUrl(result.name) || /application\/(zip|x-zip|x-tar|gzip)|multipart\/x-zip/i.test(result.contentType)) {
224
- kind = 'archive';
225
- if (extract) {
226
- const extractDir = result.path.replace(/\.(zip|tar\.gz|tgz|tar\.bz2|tbz2|tar\.xz|txz|tar|7z|rar)$/i, '');
227
- await this.extractArchive(result.path, extractDir);
228
- return { ...result, kind, extracted: extractDir };
229
- }
230
- }
231
- return { ...result, kind };
232
- }
233
-
234
- static async _downloadListing(url, html, { dir, filename, onProgress }) {
235
- const name = filename || stripInvalidFilenameChars(baseNameFromUrl(url));
236
- const folderDir = join(dir, name);
237
- mkdirSync(folderDir, { recursive: true });
238
- const links = this._listingLinks(url, html);
239
- let count = 0;
240
- for (const { href } of links) {
241
- const resolved = joinUrl(url, href);
242
- if (!resolved) continue;
243
- if (!sameDomain(url, resolved)) continue;
244
- const isSubdir = href.endsWith('/') || /^[^.?]+\/$/.test(href);
245
- try {
246
- if (isSubdir) {
247
- const subDir = join(folderDir, stripInvalidFilenameChars(baseNameFromUrl(resolved)));
248
- const subHtml = await this._peekHtml(resolved);
249
- if (subHtml && isDirectoryListing(subHtml)) {
250
- await this._downloadListingIntoSelf(resolved, subHtml, subDir, onProgress, count);
251
- count++;
252
- }
253
- } else {
254
- const fileName = stripInvalidFilenameChars(baseNameFromUrl(resolved));
255
- await this.downloadFile(resolved, { dir: folderDir, filename: fileName, onProgress });
256
- count++;
257
- }
258
- } catch (err) {
259
- // ignore individual link failures during a folder crawl
260
- }
261
- }
262
- if (typeof onProgress === 'function') onProgress(count, count);
263
- return { ok: true, path: folderDir, kind: 'folder', url, name };
264
- }
265
-
266
- static async _peekHtml(url) {
267
- try {
268
- const resp = await this.fetchStream(url, { timeout: 15000 });
269
- const contentType = resp.headers.get('content-type') || '';
270
- if (!/text\/html|application\/xhtml/i.test(contentType)) return null;
271
- const buf = Buffer.from(await resp.arrayBuffer());
272
- return buf.toString('utf8').slice(0, 20000);
273
- } catch {
274
- return null;
275
- }
276
- }
277
-
278
- static async _downloadListingIntoSelf(url, html, folderDir, onProgress, count) {
279
- mkdirSync(folderDir, { recursive: true });
280
- const links = this._listingLinks(url, html);
281
- for (const { href } of links) {
282
- const resolved = joinUrl(url, href);
283
- if (!resolved || !sameDomain(url, resolved)) continue;
284
- const isSubdir = href.endsWith('/') || /^[^.?]+\/$/.test(href);
285
- try {
286
- if (isSubdir) {
287
- const subDir = join(folderDir, stripInvalidFilenameChars(baseNameFromUrl(resolved)));
288
- const subHtml = await this._peekHtml(resolved);
289
- if (subHtml && isDirectoryListing(subHtml)) {
290
- await this._downloadListingIntoSelf(resolved, subHtml, subDir, onProgress, count);
291
- }
292
- } else {
293
- const fileName = stripInvalidFilenameChars(baseNameFromUrl(resolved));
294
- await this.downloadFile(resolved, { dir: folderDir, filename: fileName, onProgress });
295
- }
296
- } catch {
297
- // ignore individual link failures during a folder crawl
298
- }
299
- }
300
- }
301
-
302
- static _listingLinks(baseUrl, html) {
303
- const out = [];
304
- const re = /<a[^>]+href\s*=\s*"([^"]+)"[^>]*>(.*?)<\/a>/gi;
305
- let m;
306
- while ((m = re.exec(html)) !== null) {
307
- const href = m[1];
308
- if (!href) continue;
309
- if (href.startsWith('#') || href.startsWith('?') || href.startsWith('javascript:')) continue;
310
- if (href.startsWith('..') || href === '/' || href === '../') continue;
311
- out.push({ href });
312
- }
313
- return out;
314
- }
315
-
316
- static async _downloadGithub(url, { dir, filename, onProgress }) {
317
- const zipUrl = await this.githubZipUrl(url);
318
- const name = filename || `${baseNameFromUrl(url) || 'repo'}`;
319
- const destDir = join(dir, stripInvalidFilenameChars(name));
320
- mkdirSync(destDir, { recursive: true });
321
- const tmpArchive = join(tmpdir(), `github-${Date.now()}.zip`);
322
- try {
323
- await this.downloadFile(zipUrl, { dir: tmpdir(), filename: tmpArchive.split('/').pop(), timeout: 60000, onProgress });
324
- await this.extractArchive(tmpArchive, destDir);
325
- } finally {
326
- try { rmSync(tmpArchive, { force: true }); } catch {}
327
- }
328
- return { ok: true, path: destDir, kind: 'folder', url, name };
329
- }
330
-
331
- static async githubZipUrl(url) {
332
- try {
333
- const u = new URL(url);
334
- const path = u.pathname.replace(/\/+$/, '').split('/').filter(Boolean);
335
- const owner = path[0];
336
- const repo = path[1];
337
- if (path[2] === 'tree' && path[3]) {
338
- const branch = path.slice(3).join('/');
339
- return `https://codeload.github.com/${owner}/${repo}/zip/refs/heads/${branch}`;
340
- }
341
- if (path[2] === 'archive' && path[3] && path[4]) {
342
- return `https://codeload.github.com/${owner}/${repo}/zip/${path.slice(3).join('/')}`;
343
- }
344
- const branch = await this.defaultGitHubBranch(owner, repo);
345
- return `https://codeload.github.com/${owner}/${repo}/zip/refs/heads/${branch}`;
346
- } catch {
347
- return `${String(url).replace(/\/+$/, '')}.zip`;
348
- }
349
- }
350
-
351
- static async defaultGitHubBranch(owner, repo) {
352
- try {
353
- const resp = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
354
- headers: { 'User-Agent': 'vierrataleai-downloader', Accept: 'application/vnd.github+json' },
355
- signal: AbortSignal.timeout(10000),
356
- });
357
- if (!resp.ok) return 'main';
358
- const data = await resp.json();
359
- return data.default_branch || 'main';
360
- } catch {
361
- return 'main';
362
- }
363
- }
364
- }
@@ -1,44 +0,0 @@
1
- import { readdirSync } from 'fs';
2
- import { join } from 'path';
3
-
4
- // Directories that are never worth expanding in a workspace tree.
5
- const SKIP_DIRS = new Set(['.git', 'node_modules', '__pycache__', '.cache']);
6
-
7
- // Render a compact box-drawing tree of a directory. Returns an array of lines
8
- // starting with "." for the workspace root. Bounded so huge folders (node_modules,
9
- // caches, download dirs) can never flood a chat with thousands of lines.
10
- export function renderFileTree(rootDir, { maxDepth = 4, maxEntries = 150 } = {}) {
11
- const root = rootDir || process.cwd();
12
- const lines = ['.'];
13
- let entries = 0;
14
-
15
- const walk = (dir, depth, prefix) => {
16
- if (entries >= maxEntries) return;
17
- let names;
18
- try {
19
- names = readdirSync(dir, { withFileTypes: true });
20
- } catch {
21
- return;
22
- }
23
- names = names
24
- .filter((e) => !SKIP_DIRS.has(e.name))
25
- .sort((a, b) => {
26
- if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
27
- return a.name.localeCompare(b.name);
28
- });
29
- for (let i = 0; i < names.length; i++) {
30
- if (entries >= maxEntries) return;
31
- const e = names[i];
32
- const last = i === names.length - 1;
33
- lines.push(`${prefix}${last ? '└── ' : '├── '}${e.name}`);
34
- entries++;
35
- if (e.isDirectory() && depth < maxDepth) {
36
- walk(join(dir, e.name), depth + 1, `${prefix}${last ? ' ' : '│ '}`);
37
- }
38
- }
39
- };
40
-
41
- walk(root, 0, '');
42
- if (entries >= maxEntries) lines.push(`… showing first ${entries} entries`);
43
- return lines;
44
- }
@@ -1,97 +0,0 @@
1
- import { existsSync, mkdirSync, statSync, writeFileSync } from 'fs';
2
- import { dirname, resolve, sep } from 'path';
3
-
4
- const FILE_RE = /^FILE:\s*(.+?)\s*$/i;
5
- const FOLDER_RE = /^FOLDER:\s*(.+?)\s*$/i;
6
-
7
- export class FileWriter {
8
- static parse(text) {
9
- const files = [];
10
- const folders = [];
11
- const lines = String(text || '').split('\n');
12
- let i = 0;
13
- while (i < lines.length) {
14
- let m = FILE_RE.exec(lines[i]);
15
- if (m) {
16
- const path = m[1].trim();
17
- let j = i + 1;
18
- while (j < lines.length && lines[j].trim() === '') j++;
19
- if (j < lines.length && lines[j].startsWith('```')) {
20
- const start = j + 1;
21
- let end = start;
22
- while (end < lines.length && !lines[end].startsWith('```')) end++;
23
- if (end < lines.length) {
24
- files.push({
25
- path,
26
- language: lines[j].slice(3).trim(),
27
- content: lines.slice(start, end).join('\n'),
28
- });
29
- i = end + 1;
30
- continue;
31
- }
32
- }
33
- }
34
- m = FOLDER_RE.exec(lines[i]);
35
- if (m) {
36
- folders.push(m[1].trim());
37
- }
38
- i++;
39
- }
40
- return { files, folders };
41
- }
42
-
43
- static resolveTarget(path) {
44
- const base = resolve(process.cwd());
45
- const target = resolve(base, path);
46
- if (target !== base && !target.startsWith(base + sep)) {
47
- throw new Error(`Refusing to write outside the working directory: ${path}`);
48
- }
49
- return target;
50
- }
51
-
52
- static async write({ files = [], folders = [], overwrite = false, confirm } = {}) {
53
- const results = [];
54
-
55
- for (const p of folders) {
56
- try {
57
- mkdirSync(this.resolveTarget(p), { recursive: true });
58
- results.push({ kind: 'folder', path: p, target: this.resolveTarget(p), status: 'created' });
59
- } catch (err) {
60
- results.push({ kind: 'folder', path: p, status: 'error', error: err.message });
61
- }
62
- }
63
-
64
- for (const f of files) {
65
- let target;
66
- try {
67
- target = this.resolveTarget(f.path);
68
- } catch (err) {
69
- results.push({ kind: 'file', path: f.path, status: 'error', error: err.message });
70
- continue;
71
- }
72
- if (existsSync(target) && statSync(target).isDirectory()) {
73
- results.push({ kind: 'file', path: f.path, target, status: 'error', error: 'Target is a directory' });
74
- continue;
75
- }
76
- if (existsSync(target) && confirm) {
77
- const ok = await confirm(`Overwrite ${f.path}?`);
78
- if (!ok) {
79
- results.push({ kind: 'file', path: f.path, target, status: 'skipped', error: 'Declined' });
80
- continue;
81
- }
82
- } else if (existsSync(target) && !overwrite) {
83
- results.push({ kind: 'file', path: f.path, target, status: 'skipped', error: 'File already exists' });
84
- continue;
85
- }
86
- try {
87
- mkdirSync(dirname(target), { recursive: true });
88
- writeFileSync(target, f.content, 'utf-8');
89
- results.push({ kind: 'file', path: f.path, target, status: 'written' });
90
- } catch (err) {
91
- results.push({ kind: 'file', path: f.path, target, status: 'error', error: err.message });
92
- }
93
- }
94
-
95
- return results;
96
- }
97
- }
@@ -1,67 +0,0 @@
1
- import { readFileSync, existsSync, mkdirSync, appendFileSync } from 'fs';
2
- import { join } from 'path';
3
- import { Config } from '../config.js';
4
-
5
- const MAX_LINE = 2000;
6
-
7
- function pad(n) {
8
- return String(n).padStart(2, '0');
9
- }
10
-
11
- function dateStamp(d) {
12
- return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
13
- }
14
-
15
- function timeStamp(d) {
16
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
17
- }
18
-
19
- function cap(text) {
20
- const s = String(text ?? '');
21
- return s.length > MAX_LINE ? `${s.slice(0, MAX_LINE)}…` : s;
22
- }
23
-
24
- // Automatic activity logger. Writes one timestamped file per day into the
25
- // config directory (logs/). A no-op until init() is called, so tests and
26
- // library consumers never accidentally write into the real logs folder.
27
- class Logger {
28
- constructor() {
29
- this.dir = null;
30
- this.file = null;
31
- this._initialized = false;
32
- }
33
-
34
- init(opts = {}) {
35
- if (this._initialized) return this;
36
- this.dir = opts.baseDir ? join(opts.baseDir, 'logs') : join(Config.getConfigDir(), 'logs');
37
- mkdirSync(this.dir, { recursive: true });
38
- this.file = join(this.dir, `vierrataleai-${dateStamp(new Date())}.log`);
39
- this._initialized = true;
40
- return this;
41
- }
42
-
43
- log(event, message = '') {
44
- if (!this._initialized) return '';
45
- const line = `${timeStamp(new Date())} [${event}] ${cap(message)}`;
46
- try {
47
- appendFileSync(this.file, line + '\n');
48
- } catch {}
49
- return line;
50
- }
51
-
52
- getPath() {
53
- return this.file;
54
- }
55
-
56
- tail(count = 30) {
57
- if (!this._initialized || !existsSync(this.file)) return [];
58
- try {
59
- return readFileSync(this.file, 'utf-8').split('\n').filter(Boolean).slice(-count);
60
- } catch {
61
- return [];
62
- }
63
- }
64
- }
65
-
66
- export { Logger };
67
- export const logger = new Logger();