@vierratale/ai 0.1.0-beta.1 → 0.1.0-beta.2
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/README.md +1 -0
- package/package.json +1 -1
- package/src/cli.js +51 -3
- package/src/providers/base.js +4 -0
- package/src/providers/ollama.js +4 -0
- package/src/providers/openai.js +4 -0
- package/src/ui/banner.js +18 -12
- package/src/ui/branding.js +1 -1
- package/src/ui/terminal.js +50 -7
- package/src/utils/websearch.js +93 -0
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ vierrataleai --model vierratale-pro # Use specific model
|
|
|
40
40
|
- `/model [name]` - Switch model
|
|
41
41
|
- `/provider [name]` - Switch provider
|
|
42
42
|
- `/models` - List available models
|
|
43
|
+
- `/search <query>` - Search the web and summarize with AI
|
|
43
44
|
- `/clear` - Clear screen
|
|
44
45
|
- `/quit` - Exit
|
|
45
46
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ProviderFactory } from './providers/index.js';
|
|
|
6
6
|
import { Branding } from './ui/branding.js';
|
|
7
7
|
import { Terminal } from './ui/terminal.js';
|
|
8
8
|
import { loadSystemPrompt, showBanner } from './ui/banner.js';
|
|
9
|
+
import { WebSearch } from './utils/websearch.js';
|
|
9
10
|
|
|
10
11
|
function parseArgs(args) {
|
|
11
12
|
const parsed = { provider: null, model: null, clear: false, version: false, help: false };
|
|
@@ -37,6 +38,7 @@ Commands (in chat):
|
|
|
37
38
|
/model [name] Switch model
|
|
38
39
|
/provider [name] Switch provider
|
|
39
40
|
/models List available models
|
|
41
|
+
/search <query> Search the web and summarize with AI
|
|
40
42
|
/clear Clear screen
|
|
41
43
|
/quit Exit
|
|
42
44
|
`);
|
|
@@ -61,7 +63,7 @@ async function chat(provider, systemPrompt) {
|
|
|
61
63
|
|
|
62
64
|
const prompt = () => new Promise((resolve) => {
|
|
63
65
|
rl.question(
|
|
64
|
-
|
|
66
|
+
`\n${Branding.colors.bold}${Branding.colors.primary}┌ ${Branding.USER_PROMPT}${Branding.colors.reset} `,
|
|
65
67
|
resolve
|
|
66
68
|
);
|
|
67
69
|
});
|
|
@@ -90,7 +92,7 @@ async function chat(provider, systemPrompt) {
|
|
|
90
92
|
|
|
91
93
|
if (cmd === '/clear') {
|
|
92
94
|
Terminal.clear();
|
|
93
|
-
showBanner(provider.name, Config.get('model'));
|
|
95
|
+
showBanner(provider.name, Config.get('model'), provider.displayName);
|
|
94
96
|
continue;
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -146,6 +148,52 @@ async function chat(provider, systemPrompt) {
|
|
|
146
148
|
continue;
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
if (cmd.startsWith('/search ') || cmd === '/search') {
|
|
152
|
+
const query = cmd === '/search' ? '' : trimmed.slice(8).trim();
|
|
153
|
+
if (!query) {
|
|
154
|
+
Terminal.printWarning('Usage: /search <query>');
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
Terminal.printInfo('Searching the web...');
|
|
159
|
+
const results = await WebSearch.search(query);
|
|
160
|
+
|
|
161
|
+
if (results.length === 0) {
|
|
162
|
+
Terminal.printError('No results found for the query.');
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Terminal.printSearchResults(results);
|
|
167
|
+
|
|
168
|
+
const searchContext = results.map((r, i) =>
|
|
169
|
+
`${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.snippet}`
|
|
170
|
+
).join('\n\n');
|
|
171
|
+
|
|
172
|
+
messages.push({
|
|
173
|
+
role: 'user',
|
|
174
|
+
content: `Web search results for "${query}":\n\n${searchContext}\n\nPlease summarize these results and answer based on them. Be concise.`,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
Terminal.printAIStart();
|
|
178
|
+
let response = '';
|
|
179
|
+
try {
|
|
180
|
+
for await (const chunk of provider.stream(messages, {
|
|
181
|
+
model: Config.get('model'),
|
|
182
|
+
systemPrompt,
|
|
183
|
+
})) {
|
|
184
|
+
Terminal.printAIChunk(chunk);
|
|
185
|
+
response += chunk;
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
Terminal.printError(err.message || 'Stream error');
|
|
189
|
+
}
|
|
190
|
+
Terminal.printAIEnd();
|
|
191
|
+
if (response) {
|
|
192
|
+
messages.push({ role: 'assistant', content: response });
|
|
193
|
+
}
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
149
197
|
messages.push({ role: 'user', content: trimmed });
|
|
150
198
|
Terminal.printAIStart();
|
|
151
199
|
|
|
@@ -198,7 +246,7 @@ export async function run() {
|
|
|
198
246
|
Config.save({ provider: provider.name });
|
|
199
247
|
|
|
200
248
|
if (args.clear) Terminal.clear();
|
|
201
|
-
showBanner(provider.name, Config.get('model'));
|
|
249
|
+
showBanner(provider.name, Config.get('model'), provider.displayName);
|
|
202
250
|
|
|
203
251
|
const systemPrompt = loadSystemPrompt();
|
|
204
252
|
await chat(provider, systemPrompt);
|
package/src/providers/base.js
CHANGED
package/src/providers/ollama.js
CHANGED
|
@@ -8,6 +8,10 @@ export class OllamaProvider extends BaseProvider {
|
|
|
8
8
|
this.host = Config.get('ollamaHost');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
get displayName() {
|
|
12
|
+
return 'Cortex';
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
async isAvailable() {
|
|
12
16
|
try {
|
|
13
17
|
const resp = await fetch(`${this.host}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
package/src/providers/openai.js
CHANGED
package/src/ui/banner.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
|
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { Branding } from './branding.js';
|
|
5
|
+
import { Terminal } from './terminal.js';
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
|
|
@@ -15,17 +16,22 @@ export function loadSystemPrompt() {
|
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export function showBanner(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
console.log(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
);
|
|
27
|
-
console.log(
|
|
28
|
-
|
|
29
|
-
);
|
|
19
|
+
export function showBanner(providerName, model, providerDisplayName) {
|
|
20
|
+
const C = Branding.colors;
|
|
21
|
+
const width = 44;
|
|
22
|
+
const border = ` ╔${'═'.repeat(width)}╗`;
|
|
23
|
+
|
|
24
|
+
console.log();
|
|
25
|
+
console.log(`${C.primary}${Branding.BANNER}${C.reset}`);
|
|
26
|
+
console.log(` ${C.bold}${C.accent}▸ Vierratale AI ▸ Cortex Engine${C.reset}`);
|
|
27
|
+
console.log();
|
|
28
|
+
console.log(border);
|
|
29
|
+
console.log(` ║ ${C.bold}${C.primary}Model${C.reset}${' '.repeat(width - 12)}║`);
|
|
30
|
+
console.log(` ║ ${C.accent}${model}${C.reset}${' '.repeat(Math.max(1, width - 5 - model.length))}║`);
|
|
31
|
+
console.log(` ║ ${C.bold}${C.primary}Engine${C.reset}${' '.repeat(width - 13)}║`);
|
|
32
|
+
console.log(` ║ ${C.accent}${providerDisplayName || 'Cortex'}${C.reset}${' '.repeat(Math.max(1, width - 7 - (providerDisplayName || 'Cortex').length))}║`);
|
|
33
|
+
console.log(` ╚${'═'.repeat(width)}╝`);
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(` ${C.dim}Type /help for commands · /quit to exit${C.reset}`);
|
|
30
36
|
console.log();
|
|
31
37
|
}
|
package/src/ui/branding.js
CHANGED
package/src/ui/terminal.js
CHANGED
|
@@ -3,25 +3,56 @@ import { Branding } from './branding.js';
|
|
|
3
3
|
const C = Branding.colors;
|
|
4
4
|
|
|
5
5
|
export const Terminal = {
|
|
6
|
+
divider(char = '─', length = 50) {
|
|
7
|
+
return `${C.dim}${char.repeat(length)}${C.reset}`;
|
|
8
|
+
},
|
|
9
|
+
|
|
6
10
|
printUser(text) {
|
|
7
|
-
|
|
11
|
+
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
12
|
+
console.log();
|
|
13
|
+
console.log(`${this.divider('─', 4)} ${C.bold}${C.primary}YOU${C.reset} ${C.dim}${time}${C.reset} ${this.divider('─', 34)}`);
|
|
14
|
+
console.log(`${C.primary}┃${C.reset} ${text}`);
|
|
8
15
|
},
|
|
9
16
|
|
|
10
17
|
printAIStart() {
|
|
11
|
-
|
|
18
|
+
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
19
|
+
this.spinner = { frame: 0, timer: null };
|
|
20
|
+
console.log();
|
|
21
|
+
console.log(`${this.divider('─', 4)} ${C.bold}${C.accent}${Branding.AI_PROMPT}${C.reset} ${C.dim}${time}${C.reset} ${this.divider('─', 34)}`);
|
|
22
|
+
this.curLine = '';
|
|
23
|
+
this._printPrefix();
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
_printPrefix() {
|
|
27
|
+
process.stdout.write(`${C.accent}┃ ${C.reset}`);
|
|
12
28
|
},
|
|
13
29
|
|
|
14
30
|
printAIChunk(text) {
|
|
31
|
+
this.curLine = (this.curLine || '') + text;
|
|
15
32
|
process.stdout.write(text);
|
|
33
|
+
this._maybeWrap();
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
_maybeWrap() {
|
|
37
|
+
if (this.curLine && this.curLine.length > 100) {
|
|
38
|
+
const words = this.curLine.split(' ');
|
|
39
|
+
if (words.length > 1) {
|
|
40
|
+
const lastWord = words[words.length - 1];
|
|
41
|
+
const rest = this.curLine.slice(0, this.curLine.length - lastWord.length).trimEnd();
|
|
42
|
+
process.stdout.write(`\n${C.accent}┃ ${C.reset}`);
|
|
43
|
+
this.curLine = lastWord;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
16
46
|
},
|
|
17
47
|
|
|
18
48
|
printAIEnd() {
|
|
19
|
-
|
|
49
|
+
process.stdout.write('\n');
|
|
50
|
+
console.log(`${C.accent}┗${C.reset}${this.divider('─', 44)}`);
|
|
20
51
|
console.log();
|
|
21
52
|
},
|
|
22
53
|
|
|
23
54
|
printError(msg) {
|
|
24
|
-
console.
|
|
55
|
+
console.log(`${C.error}✖ ${msg}${C.reset}`);
|
|
25
56
|
},
|
|
26
57
|
|
|
27
58
|
printInfo(msg) {
|
|
@@ -29,11 +60,23 @@ export const Terminal = {
|
|
|
29
60
|
},
|
|
30
61
|
|
|
31
62
|
printSuccess(msg) {
|
|
32
|
-
console.log(`${C.success}${msg}${C.reset}`);
|
|
63
|
+
console.log(`${C.success}✔ ${msg}${C.reset}`);
|
|
33
64
|
},
|
|
34
65
|
|
|
35
66
|
printWarning(msg) {
|
|
36
|
-
console.log(`${C.warning}${msg}${C.reset}`);
|
|
67
|
+
console.log(`${C.warning}⚠ ${msg}${C.reset}`);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
printSearchResults(results) {
|
|
71
|
+
console.log(`${C.warning}┌── Web Search Results ──${C.reset}`);
|
|
72
|
+
results.forEach((r, i) => {
|
|
73
|
+
console.log(
|
|
74
|
+
` ${C.warning}${i + 1}.${C.reset} ${C.bold}${r.title}${C.reset}`
|
|
75
|
+
);
|
|
76
|
+
console.log(` ${C.dim}${r.url}${C.reset}`);
|
|
77
|
+
if (r.snippet) console.log(` ${r.snippet.slice(0, 120)}`);
|
|
78
|
+
});
|
|
79
|
+
console.log(`${C.warning}└──${C.reset}`);
|
|
37
80
|
},
|
|
38
81
|
|
|
39
82
|
clear() {
|
|
@@ -44,6 +87,6 @@ export const Terminal = {
|
|
|
44
87
|
return text
|
|
45
88
|
.replace(/\*\*(.*?)\*\*/g, `${C.bold}$1${C.reset}`)
|
|
46
89
|
.replace(/`([^`]+)`/g, `${C.accent}\`$1\`${C.reset}`)
|
|
47
|
-
.replace(/```(\w*)\n([\s\S]*?)```/g, `${C.dim}┌─$
|
|
90
|
+
.replace(/```(\w*)\n([\s\S]*?)```/g, `${C.dim}┌─${C.reset}\n$2${C.dim}└─────${C.reset}`);
|
|
48
91
|
},
|
|
49
92
|
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const DDG_SEARCH = 'https://html.duckduckgo.com/html/';
|
|
2
|
+
const WIKI_SEARCH = 'https://en.wikipedia.org/w/api.php';
|
|
3
|
+
const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36';
|
|
4
|
+
|
|
5
|
+
export class WebSearch {
|
|
6
|
+
static async search(query, maxResults = 5) {
|
|
7
|
+
let results = await this._searchDuckDuckGo(query, maxResults);
|
|
8
|
+
if (results.length === 0) {
|
|
9
|
+
results = await this._searchWikipedia(query, maxResults);
|
|
10
|
+
}
|
|
11
|
+
return results;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static async _searchDuckDuckGo(query, maxResults) {
|
|
15
|
+
const url = `${DDG_SEARCH}?q=${encodeURIComponent(query)}`;
|
|
16
|
+
try {
|
|
17
|
+
const resp = await fetch(url, {
|
|
18
|
+
headers: { 'User-Agent': UA },
|
|
19
|
+
signal: AbortSignal.timeout(10000),
|
|
20
|
+
});
|
|
21
|
+
if (!resp.ok) return [];
|
|
22
|
+
const html = await resp.text();
|
|
23
|
+
return this._parseDuckDuckGo(html, maxResults);
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static async _searchWikipedia(query, maxResults) {
|
|
30
|
+
const params = new URLSearchParams({
|
|
31
|
+
action: 'query',
|
|
32
|
+
list: 'search',
|
|
33
|
+
srsearch: query,
|
|
34
|
+
srlimit: String(maxResults),
|
|
35
|
+
format: 'json',
|
|
36
|
+
utf8: '1',
|
|
37
|
+
});
|
|
38
|
+
const url = `${WIKI_SEARCH}?${params.toString()}`;
|
|
39
|
+
try {
|
|
40
|
+
const resp = await fetch(url, {
|
|
41
|
+
headers: { 'User-Agent': UA },
|
|
42
|
+
signal: AbortSignal.timeout(10000),
|
|
43
|
+
});
|
|
44
|
+
if (!resp.ok) return [];
|
|
45
|
+
const data = await resp.json();
|
|
46
|
+
const search = data?.query?.search || [];
|
|
47
|
+
return search.map((r) => ({
|
|
48
|
+
title: r.title,
|
|
49
|
+
url: `https://en.wikipedia.org/wiki/${encodeURIComponent(r.title.replace(/ /g, '_'))}`,
|
|
50
|
+
snippet: this._stripTags(r.snippet || ''),
|
|
51
|
+
source: 'wikipedia',
|
|
52
|
+
}));
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static _parseDuckDuckGo(html, maxResults) {
|
|
59
|
+
const results = [];
|
|
60
|
+
const resultRegex = /<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>[\s\S]*?<a[^>]*class="result__snippet"[^>]*>(.*?)<\/a>/gi;
|
|
61
|
+
let match;
|
|
62
|
+
let count = 0;
|
|
63
|
+
while ((match = resultRegex.exec(html)) !== null && count < maxResults) {
|
|
64
|
+
let url = match[1];
|
|
65
|
+
const snippet = this._stripTags(match[3] || '');
|
|
66
|
+
const title = this._stripTags(match[2] || '');
|
|
67
|
+
url = this._decodeDdgUrl(url);
|
|
68
|
+
if (url && !url.includes('duckduckgo.com/y.js')) {
|
|
69
|
+
results.push({ title, url, snippet, source: 'duckduckgo' });
|
|
70
|
+
count++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static _decodeDdgUrl(url) {
|
|
77
|
+
const match = url.match(/uddg=([^&]+)/);
|
|
78
|
+
return match ? decodeURIComponent(match[1]) : url;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
static _stripTags(text) {
|
|
82
|
+
return String(text)
|
|
83
|
+
.replace(/<[^>]*>/g, '')
|
|
84
|
+
.replace(/&/g, '&')
|
|
85
|
+
.replace(/</g, '<')
|
|
86
|
+
.replace(/>/g, '>')
|
|
87
|
+
.replace(/"/g, '"')
|
|
88
|
+
.replace(/'/g, "'")
|
|
89
|
+
.replace(/'/g, "'")
|
|
90
|
+
.replace(/ /g, ' ')
|
|
91
|
+
.trim();
|
|
92
|
+
}
|
|
93
|
+
}
|