@vierratale/ai 0.1.0-beta.2 → 0.1.0-beta.4

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
@@ -16,7 +16,7 @@ npx @vierratale/ai
16
16
 
17
17
  ```bash
18
18
  vierrataleai # Start chat (auto-detect provider)
19
- vierrataleai --provider ollama # Use local models
19
+ vierrataleai --provider cortex # Use local models
20
20
  vierrataleai --provider openai # Use cloud models
21
21
  vierrataleai --model vierratale-pro # Use specific model
22
22
  ```
@@ -42,8 +42,26 @@ vierrataleai --model vierratale-pro # Use specific model
42
42
  - `/models` - List available models
43
43
  - `/search <query>` - Search the web and summarize with AI
44
44
  - `/clear` - Clear screen
45
+ - `/new` - Start a new conversation (clear memory)
45
46
  - `/quit` - Exit
46
47
 
48
+ ## Smart Search
49
+
50
+ Asking a question (Who/What/When/Where/Why/How...?) automatically searches the
51
+ web and summarizes the results with AI. For example:
52
+
53
+ ```
54
+ > Who is Cristiano Ronaldo?
55
+ ```
56
+
57
+ triggers a live web search and answers from the results. Use `/search <query>`
58
+ to force a search explicitly.
59
+
60
+ ## Conversation Memory
61
+
62
+ Your conversation is saved automatically and restored on the next launch, so
63
+ you can continue where you left off. Use `/new` to start fresh.
64
+
47
65
  ## Configuration
48
66
 
49
67
  Config file: `~/.config/vierrataleai/config.json`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vierratale/ai",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "VierrataleAI - Intelligent terminal assistant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -7,6 +7,8 @@ import { Branding } from './ui/branding.js';
7
7
  import { Terminal } from './ui/terminal.js';
8
8
  import { loadSystemPrompt, showBanner } from './ui/banner.js';
9
9
  import { WebSearch } from './utils/websearch.js';
10
+ import { detectIntent } from './utils/intents.js';
11
+ import { Session } from './session.js';
10
12
 
11
13
  function parseArgs(args) {
12
14
  const parsed = { provider: null, model: null, clear: false, version: false, help: false };
@@ -27,7 +29,7 @@ ${Branding.APP_NAME} ${Branding.VERSION}
27
29
  Usage: vierrataleai [options]
28
30
 
29
31
  Options:
30
- --provider, -p <name> Provider: ollama, openai (default: auto-detect)
32
+ --provider, -p <name> Provider: cortex, openai (default: auto-detect)
31
33
  --model, -m <name> Model: vierratale-lite/fast/balanced/pro/cloud
32
34
  --clear Clear screen on start
33
35
  --version, -v Show version
@@ -40,14 +42,59 @@ Commands (in chat):
40
42
  /models List available models
41
43
  /search <query> Search the web and summarize with AI
42
44
  /clear Clear screen
45
+ /new Start a new conversation (clear memory)
43
46
  /quit Exit
44
47
  `);
45
48
  }
46
49
 
50
+ async function answerWithSearch(messages, provider, query, systemPrompt) {
51
+ Terminal.printInfo('Searching the web...');
52
+ const results = await WebSearch.search(query);
53
+
54
+ if (results.length === 0) {
55
+ Terminal.printError('No results found for the query.');
56
+ return;
57
+ }
58
+
59
+ Terminal.printSearchResults(results);
60
+
61
+ const searchContext = results.map((r, i) =>
62
+ `${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.snippet}`
63
+ ).join('\n\n');
64
+
65
+ messages.push({
66
+ role: 'user',
67
+ content: `Web search results for "${query}":\n\n${searchContext}\n\nPlease summarize these results and answer based on them. Be concise.`,
68
+ });
69
+ Session.save(messages);
70
+
71
+ Terminal.printAIStart();
72
+ let response = '';
73
+ try {
74
+ for await (const chunk of provider.stream(messages, {
75
+ model: Config.get('model'),
76
+ systemPrompt,
77
+ })) {
78
+ Terminal.printAIChunk(chunk);
79
+ response += chunk;
80
+ }
81
+ } catch (err) {
82
+ Terminal.printError(err.message || 'Stream error');
83
+ }
84
+ Terminal.printAIEnd();
85
+ if (response) {
86
+ messages.push({ role: 'assistant', content: response });
87
+ }
88
+ }
89
+
47
90
  async function chat(provider, systemPrompt) {
48
- const messages = [];
91
+ const messages = Session.load();
49
92
  let exited = false;
50
93
 
94
+ if (messages.length > 0) {
95
+ Terminal.printInfo(`Resumed ${messages.filter((m) => m.role === 'user').length} previous message(s).`);
96
+ }
97
+
51
98
  const rl = createInterface({
52
99
  input: process.stdin,
53
100
  output: process.stdout,
@@ -101,8 +148,15 @@ async function chat(provider, systemPrompt) {
101
148
  continue;
102
149
  }
103
150
 
151
+ if (cmd === '/new') {
152
+ Session.clear();
153
+ messages.length = 0;
154
+ Terminal.printSuccess('Started a new conversation.');
155
+ continue;
156
+ }
157
+
104
158
  if (cmd === '/models') {
105
- const models = provider.name === 'ollama'
159
+ const models = provider.name === 'cortex'
106
160
  ? Catalog.getLocalModels()
107
161
  : Catalog.getCloudModels();
108
162
  Terminal.printInfo('Available models:');
@@ -134,7 +188,7 @@ async function chat(provider, systemPrompt) {
134
188
 
135
189
  if (cmd.startsWith('/provider ')) {
136
190
  const prov = cmd.split(' ')[1];
137
- if (['ollama', 'openai'].includes(prov)) {
191
+ if (['cortex', 'openai'].includes(prov)) {
138
192
  Config.save({ provider: prov });
139
193
  Terminal.printSuccess(`Provider: ${prov}`);
140
194
  } else {
@@ -148,53 +202,25 @@ async function chat(provider, systemPrompt) {
148
202
  continue;
149
203
  }
150
204
 
151
- if (cmd.startsWith('/search ') || cmd === '/search') {
152
- const query = cmd === '/search' ? '' : trimmed.slice(8).trim();
205
+ if (cmd.startsWith('/search ')) {
206
+ const query = trimmed.slice(8).trim();
153
207
  if (!query) {
154
208
  Terminal.printWarning('Usage: /search <query>');
155
209
  continue;
156
210
  }
211
+ await answerWithSearch(messages, provider, query, systemPrompt);
212
+ continue;
213
+ }
157
214
 
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
- }
215
+ const intent = detectIntent(trimmed);
216
+ if (intent.type === 'knowledge') {
217
+ Terminal.printSuccess(`Auto-search: "${trimmed}"`);
218
+ await answerWithSearch(messages, provider, trimmed, systemPrompt);
194
219
  continue;
195
220
  }
196
221
 
197
222
  messages.push({ role: 'user', content: trimmed });
223
+ Session.save(messages);
198
224
  Terminal.printAIStart();
199
225
 
200
226
  let response = '';
@@ -214,6 +240,7 @@ async function chat(provider, systemPrompt) {
214
240
  if (response) {
215
241
  messages.push({ role: 'assistant', content: response });
216
242
  }
243
+ Session.save(messages);
217
244
  }
218
245
  }
219
246
 
package/src/config.js CHANGED
@@ -8,7 +8,7 @@ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
8
8
  const DEFAULTS = {
9
9
  provider: 'auto',
10
10
  model: 'vierratale-fast',
11
- ollamaHost: 'http://127.0.0.1:11434',
11
+ engineHost: 'http://127.0.0.1:11434',
12
12
  numCtx: 8192,
13
13
  temperature: 0.7,
14
14
  maxTokens: 4096,
@@ -33,10 +33,12 @@ export const Config = {
33
33
 
34
34
  load() {
35
35
  const file = loadConfigFile();
36
+ // Migrate legacy 'cortexHost' key to 'engineHost' if present.
37
+ const engineHost = file.engineHost || file.cortexHost || DEFAULTS.engineHost;
36
38
  this._config = {
37
39
  provider: getEnv('VIERRATALE_PROVIDER', file.provider || DEFAULTS.provider),
38
40
  model: getEnv('VIERRATALE_MODEL', file.model || DEFAULTS.model),
39
- ollamaHost: getEnv('VIERRATALE_OLLAMA_HOST', file.ollamaHost || DEFAULTS.ollamaHost),
41
+ engineHost: getEnv('VIERRATALE_ENGINE_HOST', engineHost),
40
42
  numCtx: parseInt(getEnv('VIERRATALE_NUM_CTX', String(file.numCtx || DEFAULTS.numCtx))),
41
43
  temperature: parseFloat(getEnv('VIERRATALE_TEMPERATURE', String(file.temperature || DEFAULTS.temperature))),
42
44
  maxTokens: parseInt(getEnv('VIERRATALE_MAX_TOKENS', String(file.maxTokens || DEFAULTS.maxTokens))),
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { Catalog } from './catalog.js';
2
2
  export { Config } from './config.js';
3
3
  export { Installer } from './installer.js';
4
- export { OllamaProvider } from './providers/ollama.js';
4
+ export { CortexProvider } from './providers/cortex.js';
5
5
  export { OpenAIProvider } from './providers/openai.js';
6
6
  export { Branding } from './ui/branding.js';
package/src/installer.js CHANGED
@@ -9,23 +9,23 @@ function runSilent(cmd) {
9
9
  });
10
10
  }
11
11
 
12
- function isOllamaRunning(host) {
12
+ function isEngineRunning(host) {
13
13
  return fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) })
14
14
  .then((r) => r.ok)
15
15
  .catch(() => false);
16
16
  }
17
17
 
18
- function isOllamaInstalled() {
18
+ function isEngineInstalled() {
19
19
  try {
20
- execSync('which ollama 2>/dev/null', { stdio: 'ignore' });
20
+ execSync('which cortex 2>/dev/null', { stdio: 'ignore' });
21
21
  return true;
22
22
  } catch {}
23
- const paths = ['/usr/local/bin/ollama', '/usr/bin/ollama'];
23
+ const paths = ['/usr/local/bin/cortex', '/usr/bin/cortex'];
24
24
  return paths.some((p) => existsSync(p));
25
25
  }
26
26
 
27
- async function installOllama() {
28
- await runSilent('curl -fsSL https://ollama.com/install.sh | sh');
27
+ async function installEngine() {
28
+ await runSilent('curl -fsSL https://cortex.com/install.sh | sh');
29
29
  }
30
30
 
31
31
  async function pullModel(host, model) {
@@ -55,16 +55,16 @@ async function getInstalledModels(host) {
55
55
 
56
56
  export const Installer = {
57
57
  async ensure() {
58
- const host = Config.get('ollamaHost');
58
+ const host = Config.get('engineHost');
59
59
 
60
- if (!isOllamaInstalled()) {
61
- await installOllama();
60
+ if (!isEngineInstalled()) {
61
+ await installEngine();
62
62
  }
63
63
 
64
- const running = await isOllamaRunning(host);
64
+ const running = await isEngineRunning(host);
65
65
  if (!running) {
66
66
  try {
67
- execSync('ollama serve &', { stdio: 'ignore' });
67
+ execSync('cortex serve &', { stdio: 'ignore' });
68
68
  await new Promise((r) => setTimeout(r, 3000));
69
69
  } catch {}
70
70
  }
@@ -83,7 +83,7 @@ export const Installer = {
83
83
  },
84
84
 
85
85
  async isReady() {
86
- const host = Config.get('ollamaHost');
87
- return isOllamaRunning(host);
86
+ const host = Config.get('engineHost');
87
+ return isEngineRunning(host);
88
88
  },
89
89
  };
@@ -2,10 +2,10 @@ import { BaseProvider } from './base.js';
2
2
  import { Catalog } from '../catalog.js';
3
3
  import { Config } from '../config.js';
4
4
 
5
- export class OllamaProvider extends BaseProvider {
5
+ export class CortexProvider extends BaseProvider {
6
6
  constructor() {
7
- super('ollama');
8
- this.host = Config.get('ollamaHost');
7
+ super('cortex');
8
+ this.host = Config.get('engineHost');
9
9
  }
10
10
 
11
11
  get displayName() {
@@ -40,12 +40,12 @@ export class OllamaProvider extends BaseProvider {
40
40
  const model = Catalog.getRealModel(options.model || Config.get('model'));
41
41
  const systemPrompt = options.systemPrompt || '';
42
42
 
43
- const ollamaMessages = [];
43
+ const engineMessages = [];
44
44
  if (systemPrompt) {
45
- ollamaMessages.push({ role: 'system', content: systemPrompt });
45
+ engineMessages.push({ role: 'system', content: systemPrompt });
46
46
  }
47
47
  for (const msg of messages) {
48
- ollamaMessages.push({ role: msg.role, content: msg.content });
48
+ engineMessages.push({ role: msg.role, content: msg.content });
49
49
  }
50
50
 
51
51
  const resp = await fetch(`${this.host}/api/chat`, {
@@ -53,7 +53,7 @@ export class OllamaProvider extends BaseProvider {
53
53
  headers: { 'Content-Type': 'application/json' },
54
54
  body: JSON.stringify({
55
55
  model,
56
- messages: ollamaMessages,
56
+ messages: engineMessages,
57
57
  stream: true,
58
58
  options: {
59
59
  num_ctx: Config.get('numCtx'),
@@ -1,4 +1,4 @@
1
- import { OllamaProvider } from './ollama.js';
1
+ import { CortexProvider } from './cortex.js';
2
2
  import { OpenAIProvider } from './openai.js';
3
3
  import { Config } from '../config.js';
4
4
 
@@ -7,7 +7,7 @@ let providers = {};
7
7
  function getProviders() {
8
8
  if (Object.keys(providers).length === 0) {
9
9
  providers = {
10
- ollama: new OllamaProvider(),
10
+ cortex: new CortexProvider(),
11
11
  openai: new OpenAIProvider(),
12
12
  };
13
13
  }
@@ -23,7 +23,7 @@ export const ProviderFactory = {
23
23
  if (await ps[requested].isAvailable()) return ps[requested];
24
24
  }
25
25
 
26
- for (const name of ['ollama', 'openai']) {
26
+ for (const name of ['cortex', 'openai']) {
27
27
  if (await ps[name].isAvailable()) return ps[name];
28
28
  }
29
29
 
package/src/session.js ADDED
@@ -0,0 +1,41 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { Config } from './config.js';
4
+
5
+ const SESSION_FILE = join(Config.getConfigDir(), 'history.json');
6
+ const MAX_MESSAGES = 20;
7
+
8
+ export const Session = {
9
+ load() {
10
+ try {
11
+ if (existsSync(SESSION_FILE)) {
12
+ const raw = readFileSync(SESSION_FILE, 'utf-8');
13
+ const data = JSON.parse(raw || '[]');
14
+ if (Array.isArray(data)) return data.slice(-MAX_MESSAGES);
15
+ }
16
+ } catch {}
17
+ return [];
18
+ },
19
+
20
+ save(messages) {
21
+ try {
22
+ const dir = Config.getConfigDir();
23
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
24
+ writeFileSync(SESSION_FILE, JSON.stringify(messages.slice(-MAX_MESSAGES), null, 2));
25
+ } catch {}
26
+ },
27
+
28
+ clear() {
29
+ try {
30
+ if (existsSync(SESSION_FILE)) writeFileSync(SESSION_FILE, '[]');
31
+ } catch {}
32
+ },
33
+
34
+ hasHistory() {
35
+ try {
36
+ return existsSync(SESSION_FILE) && (JSON.parse(readFileSync(SESSION_FILE, 'utf-8') || '[]').length > 0);
37
+ } catch {
38
+ return false;
39
+ }
40
+ },
41
+ };
@@ -1,6 +1,6 @@
1
1
  export const Branding = {
2
2
  APP_NAME: 'VierrataleAI',
3
- VERSION: '0.1.0-beta.2',
3
+ VERSION: '0.1.0-beta.4',
4
4
 
5
5
  colors: {
6
6
  primary: '\x1b[38;2;124;58;237m',
@@ -0,0 +1,30 @@
1
+ const QUESTION_PREFIXES = [
2
+ 'who', 'what', 'when', 'where', 'why', 'how',
3
+ 'which', 'whose', 'whom', 'is', 'are', 'was', 'were',
4
+ 'can', 'could', 'does', 'do', 'did', 'would', 'should', 'will',
5
+ ];
6
+
7
+ const KNOWLEDGE_PREFIXES = [
8
+ 'tell me about', "what is", "what's", "who is", "who's",
9
+ 'what are', 'who was', 'when did', 'when was', 'where is',
10
+ 'where was', 'how did', 'how does', 'how to', 'why did', 'why is',
11
+ 'latest', 'news', 'current',
12
+ ];
13
+
14
+ export function detectIntent(text) {
15
+ const lower = text.toLowerCase().trim();
16
+ if (lower.startsWith('/')) return { type: 'command' };
17
+
18
+ const isQuestion = /\?$/.test(lower);
19
+ const firstWord = lower.split(/\s+/)[0] || '';
20
+
21
+ let needsWeb = false;
22
+ if (isQuestion && QUESTION_PREFIXES.includes(firstWord)) {
23
+ needsWeb = true;
24
+ }
25
+ if (KNOWLEDGE_PREFIXES.some((p) => lower.startsWith(p + ' ') || lower === p)) {
26
+ needsWeb = true;
27
+ }
28
+
29
+ return { type: needsWeb ? 'knowledge' : 'chat' };
30
+ }
@@ -79,15 +79,15 @@ export class WebSearch {
79
79
  }
80
80
 
81
81
  static _stripTags(text) {
82
+ const map = {
83
+ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"',
84
+ '&#039;': "'", '&#39;': "'", '&apos;': "'", '&nbsp;': ' ',
85
+ '&#x27;': "'", '&hellip;': '...',
86
+ };
82
87
  return String(text)
83
88
  .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, ' ')
89
+ .replace(/&[a-zA-Z0-9#]+;/g, (m) => map[m] ?? '')
90
+ .replace(/\s{2,}/g, ' ')
91
91
  .trim();
92
92
  }
93
93
  }