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

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
@@ -12,27 +12,43 @@ npm install -g @vierratale/ai
12
12
  npx @vierratale/ai
13
13
  ```
14
14
 
15
+ The first launch detects and, if needed, installs and starts the local engine
16
+ (**Cortex** - "secretly" Ollama under the hood), then pulls the model you use.
17
+
15
18
  ## Usage
16
19
 
17
20
  ```bash
18
21
  vierrataleai # Start chat (auto-detect provider)
19
22
  vierrataleai --provider cortex # Use local models
20
- vierrataleai --provider openai # Use cloud models
23
+ vierrataleai --provider openai # Use cloud models (OpenAI)
24
+ vierrataleai --provider anthropic # Use cloud models (Claude)
25
+ vierrataleai --provider gemini # Use cloud models (Gemini)
21
26
  vierrataleai --model vierratale-pro # Use specific model
22
27
  ```
23
28
 
24
29
  ## Models
25
30
 
26
- | Model | Description |
27
- |-------|-------------|
28
- | vierratale-lite | Minimal, fast responses |
29
- | vierratale-fast | Fast, balanced quality |
30
- | vierratale-small | Small, efficient |
31
- | vierratale-balanced | Balanced performance |
32
- | vierratale-plus | Enhanced capability |
33
- | vierratale-pro | Professional grade |
34
- | vierratale-cloud | Cloud-powered |
35
- | vierratale-cloud-pro | Cloud professional |
31
+ Local models run through the **Cortex** engine:
32
+
33
+ | Model | Backend |
34
+ |-------|---------|
35
+ | vierratale-lite | qwen2.5-coder:0.5b |
36
+ | vierratale-fast | qwen2.5-coder:1.5b |
37
+ | vierratale-small | qwen2.5-coder:3b |
38
+ | vierratale-balanced | qwen2.5-coder:7b |
39
+ | vierratale-plus | qwen2.5-coder:14b |
40
+ | vierratale-pro | qwen2.5-coder:32b |
41
+
42
+ Cloud models are available when the matching provider + API key is configured:
43
+
44
+ | Model | Backend |
45
+ |-------|---------|
46
+ | vierratale-cloud-mini | gpt-4o-mini (OpenAI) |
47
+ | vierratale-cloud | gpt-4o (OpenAI) |
48
+ | vierratale-cloud-fast | claude-3-5-haiku-20241022 (Anthropic) |
49
+ | vierratale-cloud-pro | claude-sonnet-4-20250514 (Anthropic) |
50
+ | vierratale-cloud-lite | gemini-2.0-flash (Gemini) |
51
+ | vierratale-cloud-plus | gemini-1.5-pro (Gemini) |
36
52
 
37
53
  ## Commands
38
54
 
@@ -40,6 +56,7 @@ vierrataleai --model vierratale-pro # Use specific model
40
56
  - `/model [name]` - Switch model
41
57
  - `/provider [name]` - Switch provider
42
58
  - `/models` - List available models
59
+ - `/system [text]` - Set a custom system prompt / persona
43
60
  - `/search <query>` - Search the web and summarize with AI
44
61
  - `/clear` - Clear screen
45
62
  - `/new` - Start a new conversation (clear memory)
@@ -67,9 +84,11 @@ you can continue where you left off. Use `/new` to start fresh.
67
84
  Config file: `~/.config/vierrataleai/config.json`
68
85
 
69
86
  Environment variables:
70
- - `VIERRATALE_PROVIDER` - Default provider
87
+ - `VIERRATALE_PROVIDER` - Default provider (auto/cortex/openai/anthropic/gemini)
71
88
  - `VIERRATALE_MODEL` - Default model
72
- - `OPENAI_API_KEY` - OpenAI API key (for cloud models)
89
+ - `OPENAI_API_KEY` - OpenAI API key (openai provider)
90
+ - `ANTHROPIC_API_KEY` - Anthropic API key (anthropic provider)
91
+ - `GEMINI_API_KEY` - Google AI API key (gemini provider)
73
92
 
74
93
  ## License
75
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vierratale/ai",
3
- "version": "0.1.0-beta.4",
3
+ "version": "0.1.0-beta.6",
4
4
  "description": "VierrataleAI - Intelligent terminal assistant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,9 @@
11
11
  ".": "./src/index.js"
12
12
  },
13
13
  "scripts": {
14
- "start": "node bin/vierrataleai.js"
14
+ "start": "node bin/vierrataleai.js",
15
+ "test": "node --test test/",
16
+ "postinstall": "node scripts/postinstall.js"
15
17
  },
16
18
  "keywords": ["ai", "chatbot", "terminal", "assistant", "llm"],
17
19
  "author": "Vierratale",
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // Optional engine setup after install. Never fails the package install.
3
+ import { execSync } from 'child_process';
4
+ import { existsSync } from 'fs';
5
+
6
+ const isInstalled = () => {
7
+ try {
8
+ execSync('which ollama 2>/dev/null', { stdio: 'ignore' });
9
+ return true;
10
+ } catch {}
11
+ return existsSync('/usr/local/bin/ollama') || existsSync('/usr/bin/ollama');
12
+ };
13
+
14
+ try {
15
+ if (isInstalled()) {
16
+ process.exit(0);
17
+ }
18
+
19
+ const nonInteractive =
20
+ !process.stdout.isTTY || process.env.CI || process.env.NODE_ENV === 'test';
21
+
22
+ // Only auto-install when explicitly requested. Otherwise just inform the user
23
+ // that the engine will be installed on first launch.
24
+ if (process.env.VIERRATALE_INSTALL_ENGINE === '1' && !nonInteractive) {
25
+ execSync('curl -fsSL https://ollama.com/install.sh | sh', { stdio: 'inherit' });
26
+ } else {
27
+ console.log(
28
+ '\n[vierrataleai] The local engine (ollama) was not detected.\n' +
29
+ ' - Run the CLI: it will attempt to install and start the engine automatically.\n' +
30
+ ' - Or install it yourself: curl -fsSL https://ollama.com/install.sh | sh\n'
31
+ );
32
+ }
33
+ } catch {
34
+ // Never fail the install because of engine setup.
35
+ }
package/src/catalog.js CHANGED
@@ -60,6 +60,26 @@ export const Catalog = {
60
60
  return 'vierratale-fast';
61
61
  },
62
62
 
63
+ getDefaultCloudModel() {
64
+ return 'vierratale-cloud-mini';
65
+ },
66
+
67
+ isLocalModel(displayName) {
68
+ return LOCAL_MODELS.includes(displayName);
69
+ },
70
+
71
+ isCloudModel(displayName) {
72
+ return CLOUD_MODELS.includes(displayName);
73
+ },
74
+
75
+ getVendorForCloudModel(displayName) {
76
+ const real = REVERSE[displayName];
77
+ if (real && real.startsWith('claude-')) return 'anthropic';
78
+ if (real && real.startsWith('gemini-')) return 'gemini';
79
+ if (real && real.startsWith('gpt-')) return 'openai';
80
+ return 'openai';
81
+ },
82
+
63
83
  getModelInfo(displayName) {
64
84
  const real = REVERSE[displayName];
65
85
  if (!real) return null;
package/src/cli.js CHANGED
@@ -72,7 +72,7 @@ async function answerWithSearch(messages, provider, query, systemPrompt) {
72
72
  let response = '';
73
73
  try {
74
74
  for await (const chunk of provider.stream(messages, {
75
- model: Config.get('model'),
75
+ model: Config.getEffectiveModel(provider.name),
76
76
  systemPrompt,
77
77
  })) {
78
78
  Terminal.printAIChunk(chunk);
@@ -85,6 +85,7 @@ async function answerWithSearch(messages, provider, query, systemPrompt) {
85
85
  if (response) {
86
86
  messages.push({ role: 'assistant', content: response });
87
87
  }
88
+ Session.save(messages);
88
89
  }
89
90
 
90
91
  async function chat(provider, systemPrompt) {
@@ -139,7 +140,7 @@ async function chat(provider, systemPrompt) {
139
140
 
140
141
  if (cmd === '/clear') {
141
142
  Terminal.clear();
142
- showBanner(provider.name, Config.get('model'), provider.displayName);
143
+ showBanner(provider.name, Config.getEffectiveModel(provider.name), provider.displayName);
143
144
  continue;
144
145
  }
145
146
 
@@ -156,13 +157,13 @@ async function chat(provider, systemPrompt) {
156
157
  }
157
158
 
158
159
  if (cmd === '/models') {
159
- const models = provider.name === 'cortex'
160
+ const models = provider.isLocal
160
161
  ? Catalog.getLocalModels()
161
162
  : Catalog.getCloudModels();
162
163
  Terminal.printInfo('Available models:');
163
164
  for (const m of models) {
164
165
  const info = Catalog.getModelInfo(m);
165
- const current = m === Config.get('model') ? ' (active)' : '';
166
+ const current = m === Config.getEffectiveModel(provider.name) ? ' (active)' : '';
166
167
  console.log(` ${Branding.colors.accent}${m}${Branding.colors.dim}${current} - ${info.tier}${Branding.colors.reset}`);
167
168
  }
168
169
  continue;
@@ -173,6 +174,7 @@ async function chat(provider, systemPrompt) {
173
174
  if (modelName) {
174
175
  if (Catalog.getModelInfo(modelName)) {
175
176
  Config.save({ model: modelName });
177
+ Config.setProviderModel(provider.name, modelName);
176
178
  Terminal.printSuccess(`Model: ${modelName}`);
177
179
  } else {
178
180
  Terminal.printError(`Unknown model: ${modelName}`);
@@ -182,15 +184,21 @@ async function chat(provider, systemPrompt) {
182
184
  }
183
185
 
184
186
  if (cmd === '/model') {
185
- Terminal.printInfo(`Current model: ${Config.get('model')}`);
187
+ Terminal.printInfo(`Current model: ${Config.getEffectiveModel(provider.name)}`);
186
188
  continue;
187
189
  }
188
190
 
189
191
  if (cmd.startsWith('/provider ')) {
190
192
  const prov = cmd.split(' ')[1];
191
- if (['cortex', 'openai'].includes(prov)) {
193
+ if (['cortex', 'openai', 'anthropic', 'gemini'].includes(prov)) {
192
194
  Config.save({ provider: prov });
193
- Terminal.printSuccess(`Provider: ${prov}`);
195
+ const next = ProviderFactory.create(prov);
196
+ if (next && await next.isAvailable()) {
197
+ provider = next;
198
+ Terminal.printSuccess(`Provider: ${prov}`);
199
+ } else {
200
+ Terminal.printWarning(`Provider saved, but ${prov} is not available right now. Will apply on restart.`);
201
+ }
194
202
  } else {
195
203
  Terminal.printError(`Unknown provider: ${prov}`);
196
204
  }
@@ -202,6 +210,22 @@ async function chat(provider, systemPrompt) {
202
210
  continue;
203
211
  }
204
212
 
213
+ if (cmd.startsWith('/system')) {
214
+ const prompt = trimmed.slice(7).trim();
215
+ if (prompt.toLowerCase() === 'reset') {
216
+ Config.save({ systemPrompt: '' });
217
+ systemPrompt = loadSystemPrompt();
218
+ Terminal.printSuccess('System prompt reset to default.');
219
+ } else if (prompt) {
220
+ Config.save({ systemPrompt: prompt });
221
+ systemPrompt = prompt;
222
+ Terminal.printSuccess('System prompt updated.');
223
+ } else {
224
+ Terminal.printInfo(`Current system prompt: ${systemPrompt}`);
225
+ }
226
+ continue;
227
+ }
228
+
205
229
  if (cmd.startsWith('/search ')) {
206
230
  const query = trimmed.slice(8).trim();
207
231
  if (!query) {
@@ -226,7 +250,7 @@ async function chat(provider, systemPrompt) {
226
250
  let response = '';
227
251
  try {
228
252
  for await (const chunk of provider.stream(messages, {
229
- model: Config.get('model'),
253
+ model: Config.getEffectiveModel(provider.name),
230
254
  systemPrompt,
231
255
  })) {
232
256
  Terminal.printAIChunk(chunk);
@@ -273,7 +297,7 @@ export async function run() {
273
297
  Config.save({ provider: provider.name });
274
298
 
275
299
  if (args.clear) Terminal.clear();
276
- showBanner(provider.name, Config.get('model'), provider.displayName);
300
+ showBanner(provider.name, Config.getEffectiveModel(provider.name), provider.displayName);
277
301
 
278
302
  const systemPrompt = loadSystemPrompt();
279
303
  await chat(provider, systemPrompt);
package/src/config.js CHANGED
@@ -44,6 +44,9 @@ export const Config = {
44
44
  maxTokens: parseInt(getEnv('VIERRATALE_MAX_TOKENS', String(file.maxTokens || DEFAULTS.maxTokens))),
45
45
  openaiApiKey: getEnv('OPENAI_API_KEY', file.openaiApiKey || ''),
46
46
  anthropicApiKey: getEnv('ANTHROPIC_API_KEY', file.anthropicApiKey || ''),
47
+ geminiApiKey: getEnv('GEMINI_API_KEY', file.geminiApiKey || ''),
48
+ systemPrompt: file.systemPrompt || '',
49
+ providerModels: file.providerModels || {},
47
50
  };
48
51
  return this._config;
49
52
  },
@@ -58,6 +61,22 @@ export const Config = {
58
61
  return { ...this._config };
59
62
  },
60
63
 
64
+ getProviderModel(provider) {
65
+ if (!this._config) this.load();
66
+ return this._config.providerModels?.[provider] || null;
67
+ },
68
+
69
+ setProviderModel(provider, model) {
70
+ if (!this._config) this.load();
71
+ this._config.providerModels = this._config.providerModels || {};
72
+ this._config.providerModels[provider] = model;
73
+ this.save();
74
+ },
75
+
76
+ getEffectiveModel(provider) {
77
+ return this.getProviderModel(provider) || this.get('model');
78
+ },
79
+
61
80
  save(overrides = {}) {
62
81
  if (!existsSync(CONFIG_DIR)) {
63
82
  mkdirSync(CONFIG_DIR, { recursive: true });
package/src/installer.js CHANGED
@@ -17,15 +17,16 @@ function isEngineRunning(host) {
17
17
 
18
18
  function isEngineInstalled() {
19
19
  try {
20
- execSync('which cortex 2>/dev/null', { stdio: 'ignore' });
20
+ execSync('which ollama 2>/dev/null', { stdio: 'ignore' });
21
21
  return true;
22
22
  } catch {}
23
- const paths = ['/usr/local/bin/cortex', '/usr/bin/cortex'];
23
+ const paths = ['/usr/local/bin/ollama', '/usr/bin/ollama'];
24
24
  return paths.some((p) => existsSync(p));
25
25
  }
26
26
 
27
27
  async function installEngine() {
28
- await runSilent('curl -fsSL https://cortex.com/install.sh | sh');
28
+ await runSilent('curl -fsSL https://ollama.com/install.sh | sh');
29
+ return isEngineInstalled();
29
30
  }
30
31
 
31
32
  async function pullModel(host, model) {
@@ -64,7 +65,7 @@ export const Installer = {
64
65
  const running = await isEngineRunning(host);
65
66
  if (!running) {
66
67
  try {
67
- execSync('cortex serve &', { stdio: 'ignore' });
68
+ execSync('ollama serve &', { stdio: 'ignore' });
68
69
  await new Promise((r) => setTimeout(r, 3000));
69
70
  } catch {}
70
71
  }
@@ -0,0 +1,99 @@
1
+ import { BaseProvider } from './base.js';
2
+ import { Catalog } from '../catalog.js';
3
+ import { Config } from '../config.js';
4
+
5
+ const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
6
+
7
+ export class AnthropicProvider extends BaseProvider {
8
+ constructor() {
9
+ super('anthropic');
10
+ }
11
+
12
+ get displayName() {
13
+ return 'Nebula';
14
+ }
15
+
16
+ _resolveModel(options) {
17
+ const requested = options.model || Config.get('model');
18
+ if (Catalog.isLocalModel(requested) || !Catalog.isCloudModel(requested)) {
19
+ return Catalog.getRealModel(Catalog.getDefaultCloudModel());
20
+ }
21
+ const real = Catalog.getRealModel(requested);
22
+ return real.startsWith('claude-') ? real : 'claude-3-5-haiku-20241022';
23
+ }
24
+
25
+ async isAvailable() {
26
+ const key = Config.get('anthropicApiKey');
27
+ return Boolean(key);
28
+ }
29
+
30
+ async listModels() {
31
+ const key = Config.get('anthropicApiKey');
32
+ if (!key) return [];
33
+ const models = Object.keys(Catalog.getAllModels()).filter((m) => m.startsWith('claude-'));
34
+ return models.map((real) => ({
35
+ realName: real,
36
+ displayName: Catalog.getDisplayName(real),
37
+ }));
38
+ }
39
+
40
+ async *stream(messages, options = {}) {
41
+ const key = Config.get('anthropicApiKey');
42
+ if (!key) throw new Error('Anthropic API key not configured');
43
+
44
+ const model = this._resolveModel(options);
45
+ const systemPrompt = options.systemPrompt || '';
46
+
47
+ const system = [];
48
+ if (systemPrompt) system.push({ type: 'text', text: systemPrompt });
49
+ const apiMessages = messages.map((m) => ({
50
+ role: m.role === 'assistant' ? 'assistant' : 'user',
51
+ content: m.content,
52
+ }));
53
+
54
+ const resp = await fetch(ANTHROPIC_URL, {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ 'x-api-key': key,
59
+ 'anthropic-version': '2023-06-01',
60
+ },
61
+ body: JSON.stringify({
62
+ model,
63
+ system: system.length ? system : undefined,
64
+ messages: apiMessages,
65
+ max_tokens: options.maxTokens || Config.get('maxTokens'),
66
+ stream: true,
67
+ temperature: options.temperature || Config.get('temperature'),
68
+ }),
69
+ });
70
+
71
+ if (!resp.ok) {
72
+ throw new Error('Failed to connect to cloud engine');
73
+ }
74
+
75
+ const reader = resp.body.getReader();
76
+ const decoder = new TextDecoder();
77
+ let buffer = '';
78
+
79
+ while (true) {
80
+ const { done, value } = await reader.read();
81
+ if (done) break;
82
+
83
+ buffer += decoder.decode(value, { stream: true });
84
+ const lines = buffer.split('\n');
85
+ buffer = lines.pop() || '';
86
+
87
+ for (const line of lines) {
88
+ if (!line.startsWith('data: ')) continue;
89
+ const data = line.slice(6);
90
+ try {
91
+ const json = JSON.parse(data);
92
+ if (json.type === 'content_block_delta' && json.delta?.text) {
93
+ yield json.delta.text;
94
+ }
95
+ } catch {}
96
+ }
97
+ }
98
+ }
99
+ }
@@ -7,6 +7,10 @@ export class BaseProvider {
7
7
  return 'Cortex';
8
8
  }
9
9
 
10
+ get isLocal() {
11
+ return false;
12
+ }
13
+
10
14
  async *stream(messages, options = {}) {
11
15
  throw new Error('stream() must be implemented');
12
16
  }
@@ -12,6 +12,10 @@ export class CortexProvider extends BaseProvider {
12
12
  return 'Cortex';
13
13
  }
14
14
 
15
+ get isLocal() {
16
+ return true;
17
+ }
18
+
15
19
  async isAvailable() {
16
20
  try {
17
21
  const resp = await fetch(`${this.host}/api/tags`, { signal: AbortSignal.timeout(3000) });
@@ -0,0 +1,97 @@
1
+ import { BaseProvider } from './base.js';
2
+ import { Catalog } from '../catalog.js';
3
+ import { Config } from '../config.js';
4
+
5
+ const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models';
6
+
7
+ export class GeminiProvider extends BaseProvider {
8
+ constructor() {
9
+ super('gemini');
10
+ }
11
+
12
+ get displayName() {
13
+ return 'Nebula';
14
+ }
15
+
16
+ _resolveModel(options) {
17
+ const requested = options.model || Config.get('model');
18
+ if (Catalog.isLocalModel(requested) || !Catalog.isCloudModel(requested)) {
19
+ return Catalog.getRealModel(Catalog.getDefaultCloudModel());
20
+ }
21
+ const real = Catalog.getRealModel(requested);
22
+ return real.startsWith('gemini-') ? real : 'gemini-2.0-flash';
23
+ }
24
+
25
+ async isAvailable() {
26
+ const key = Config.get('geminiApiKey');
27
+ return Boolean(key);
28
+ }
29
+
30
+ async listModels() {
31
+ const key = Config.get('geminiApiKey');
32
+ if (!key) return [];
33
+ const models = Object.keys(Catalog.getAllModels()).filter((m) => m.startsWith('gemini-'));
34
+ return models.map((real) => ({
35
+ realName: real,
36
+ displayName: Catalog.getDisplayName(real),
37
+ }));
38
+ }
39
+
40
+ async *stream(messages, options = {}) {
41
+ const key = Config.get('geminiApiKey');
42
+ if (!key) throw new Error('Gemini API key not configured');
43
+
44
+ const model = this._resolveModel(options);
45
+ const systemPrompt = options.systemPrompt || '';
46
+
47
+ const contents = messages.map((m) => ({
48
+ role: m.role === 'assistant' ? 'model' : 'user',
49
+ parts: [{ text: m.content }],
50
+ }));
51
+ if (systemPrompt) {
52
+ contents.unshift({ role: 'user', parts: [{ text: `System: ${systemPrompt}` }] });
53
+ }
54
+
55
+ const resp = await fetch(
56
+ `${GEMINI_URL}/${model}:streamGenerateContent?key=${key}&alt=sse`,
57
+ {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({
61
+ contents,
62
+ generationConfig: {
63
+ temperature: options.temperature || Config.get('temperature'),
64
+ maxOutputTokens: options.maxTokens || Config.get('maxTokens'),
65
+ },
66
+ }),
67
+ }
68
+ );
69
+
70
+ if (!resp.ok) {
71
+ throw new Error('Failed to connect to cloud engine');
72
+ }
73
+
74
+ const reader = resp.body.getReader();
75
+ const decoder = new TextDecoder();
76
+ let buffer = '';
77
+
78
+ while (true) {
79
+ const { done, value } = await reader.read();
80
+ if (done) break;
81
+
82
+ buffer += decoder.decode(value, { stream: true });
83
+ const lines = buffer.split('\n');
84
+ buffer = lines.pop() || '';
85
+
86
+ for (const line of lines) {
87
+ if (!line.startsWith('data: ')) continue;
88
+ const data = line.slice(6);
89
+ try {
90
+ const json = JSON.parse(data);
91
+ const text = json.candidates?.[0]?.content?.parts?.[0]?.text;
92
+ if (text) yield text;
93
+ } catch {}
94
+ }
95
+ }
96
+ }
97
+ }
@@ -1,5 +1,7 @@
1
1
  import { CortexProvider } from './cortex.js';
2
2
  import { OpenAIProvider } from './openai.js';
3
+ import { AnthropicProvider } from './anthropic.js';
4
+ import { GeminiProvider } from './gemini.js';
3
5
  import { Config } from '../config.js';
4
6
 
5
7
  let providers = {};
@@ -9,6 +11,8 @@ function getProviders() {
9
11
  providers = {
10
12
  cortex: new CortexProvider(),
11
13
  openai: new OpenAIProvider(),
14
+ anthropic: new AnthropicProvider(),
15
+ gemini: new GeminiProvider(),
12
16
  };
13
17
  }
14
18
  return providers;
@@ -23,7 +27,7 @@ export const ProviderFactory = {
23
27
  if (await ps[requested].isAvailable()) return ps[requested];
24
28
  }
25
29
 
26
- for (const name of ['cortex', 'openai']) {
30
+ for (const name of ['cortex', 'openai', 'anthropic', 'gemini']) {
27
31
  if (await ps[name].isAvailable()) return ps[name];
28
32
  }
29
33
 
@@ -45,11 +45,21 @@ export class OpenAIProvider extends BaseProvider {
45
45
  }
46
46
  }
47
47
 
48
+ // If the configured model is a local (ollama) model, use a cloud default so
49
+ // we never send a qwen model name to the OpenAI API.
50
+ _resolveModel(options) {
51
+ const requested = options.model || Config.get('model');
52
+ if (Catalog.isLocalModel(requested) || !Catalog.isCloudModel(requested)) {
53
+ return Catalog.getRealModel(Catalog.getDefaultCloudModel());
54
+ }
55
+ return Catalog.getRealModel(requested);
56
+ }
57
+
48
58
  async *stream(messages, options = {}) {
49
59
  const key = Config.get('openaiApiKey');
50
60
  if (!key) throw new Error('Cloud API key not configured');
51
61
 
52
- const model = Catalog.getRealModel(options.model || Config.get('model'));
62
+ const model = this._resolveModel(options);
53
63
  const systemPrompt = options.systemPrompt || '';
54
64
 
55
65
  const apiMessages = [];
package/src/ui/banner.js CHANGED
@@ -3,10 +3,13 @@ import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { Branding } from './branding.js';
5
5
  import { Terminal } from './terminal.js';
6
+ import { Config } from '../config.js';
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
 
9
10
  export function loadSystemPrompt() {
11
+ const custom = Config.get('systemPrompt');
12
+ if (custom) return custom;
10
13
  try {
11
14
  const jsonPath = join(__dirname, '..', 'prompts', 'system.json');
12
15
  const data = JSON.parse(readFileSync(jsonPath, 'utf-8'));
@@ -20,16 +23,18 @@ export function showBanner(providerName, model, providerDisplayName) {
20
23
  const C = Branding.colors;
21
24
  const width = 44;
22
25
  const border = ` ╔${'═'.repeat(width)}╗`;
26
+ const visible = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
27
+ const pad = (s) => s + ' '.repeat(Math.max(1, width - visible(s).length));
23
28
 
24
29
  console.log();
25
30
  console.log(`${C.primary}${Branding.BANNER}${C.reset}`);
26
- console.log(` ${C.bold}${C.accent}▸ Vierratale AI ▸ Cortex Engine${C.reset}`);
31
+ console.log(` ${C.bold}${C.accent}▸ Vierratale AI ▸ ${providerDisplayName || 'Cortex'} Engine${C.reset}`);
27
32
  console.log();
28
33
  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))}║`);
34
+ console.log(` ║${pad(' ' + C.bold + C.primary + 'Model' + C.reset)}║`);
35
+ console.log(` ║${pad(' ' + C.accent + model + C.reset)}║`);
36
+ console.log(` ║${pad(' ' + C.bold + C.primary + 'Engine' + C.reset)}║`);
37
+ console.log(` ║${pad(' ' + C.accent + (providerDisplayName || 'Cortex') + C.reset)}║`);
33
38
  console.log(` ╚${'═'.repeat(width)}╝`);
34
39
  console.log();
35
40
  console.log(` ${C.dim}Type /help for commands · /quit to exit${C.reset}`);
@@ -1,6 +1,6 @@
1
1
  export const Branding = {
2
2
  APP_NAME: 'VierrataleAI',
3
- VERSION: '0.1.0-beta.4',
3
+ VERSION: '0.1.0-beta.6',
4
4
 
5
5
  colors: {
6
6
  primary: '\x1b[38;2;124;58;237m',
@@ -14,14 +14,13 @@ export const Branding = {
14
14
  },
15
15
 
16
16
  BANNER: [
17
- '',
18
- ' ██╗ ██╗██╗██████╗ ███████╗',
19
- ' ██║ ██║██║██╔══██╗██╔════╝',
20
- ' ██║ ██║██║██████╔╝█████╗ ',
21
- ' ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ',
22
- ' ╚████╔╝ ██║██████╔╝███████╗',
23
- ' ╚═══╝ ╚═╝╚═════╝ ╚══════╝',
24
- '',
17
+ ' _',
18
+ ' _ _ _ / \\ _______',
19
+ ' | | | | / _ \\\\_ _ /',
20
+ ' | | | |_ ___ _ __ _ __ __ _/ /_\\ \\ | | ',
21
+ ' | | | | |/ _ \\ \'__| \'__/ _` | _ | | | ',
22
+ ' \\ \\_/ / | __/ | | | | (_| | | | |_| |_ ',
23
+ ' \\___/|_|\\___|_| |_| \\__,_|_| |_/\\___/ ',
25
24
  ].join('\n'),
26
25
 
27
26
  USER_PROMPT: 'You',