@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vierratale/ai",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.2",
4
4
  "description": "VierrataleAI - Intelligent terminal assistant",
5
5
  "type": "module",
6
6
  "bin": {
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
- `${Branding.colors.bold}${Branding.colors.primary}${Branding.USER_PROMPT} >${Branding.colors.reset} `,
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);
@@ -3,6 +3,10 @@ export class BaseProvider {
3
3
  this.name = name;
4
4
  }
5
5
 
6
+ get displayName() {
7
+ return 'Cortex';
8
+ }
9
+
6
10
  async *stream(messages, options = {}) {
7
11
  throw new Error('stream() must be implemented');
8
12
  }
@@ -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) });
@@ -7,6 +7,10 @@ export class OpenAIProvider extends BaseProvider {
7
7
  super('openai');
8
8
  }
9
9
 
10
+ get displayName() {
11
+ return 'Nebula';
12
+ }
13
+
10
14
  async isAvailable() {
11
15
  const key = Config.get('openaiApiKey');
12
16
  if (!key) return false;
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(provider, model) {
19
- console.log(Branding.colors.primary + Branding.BANNER + Branding.colors.reset);
20
- console.log(
21
- ` ${Branding.colors.dim}Intelligent Terminal Assistant${Branding.colors.reset}`
22
- );
23
- console.log(
24
- ` ${Branding.colors.accent}Model:${Branding.colors.reset} ${model} ` +
25
- `${Branding.colors.accent}Engine:${Branding.colors.reset} ${provider}`
26
- );
27
- console.log(
28
- ` ${Branding.colors.dim}Type /help for commands, /quit to exit${Branding.colors.reset}`
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
  }
@@ -1,6 +1,6 @@
1
1
  export const Branding = {
2
2
  APP_NAME: 'VierrataleAI',
3
- VERSION: '0.1.0-beta.1',
3
+ VERSION: '0.1.0-beta.2',
4
4
 
5
5
  colors: {
6
6
  primary: '\x1b[38;2;124;58;237m',
@@ -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
- console.log(`${C.bold}${C.primary}${Branding.USER_PROMPT} >${C.reset} ${text}`);
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
- process.stdout.write(`${C.bold}${C.accent}${Branding.AI_PROMPT} >${C.reset} `);
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
- console.log();
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.error(`${C.error}Error: ${msg}${C.reset}`);
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}┌─$1─┐${C.reset}\n$2${C.dim}└─────┘${C.reset}`);
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(/&amp;/g, '&')
85
+ .replace(/&lt;/g, '<')
86
+ .replace(/&gt;/g, '>')
87
+ .replace(/&quot;/g, '"')
88
+ .replace(/&#x27;/g, "'")
89
+ .replace(/&#39;/g, "'")
90
+ .replace(/&nbsp;/g, ' ')
91
+ .trim();
92
+ }
93
+ }