@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 +32 -13
- package/package.json +4 -2
- package/scripts/postinstall.js +35 -0
- package/src/catalog.js +20 -0
- package/src/cli.js +33 -9
- package/src/config.js +19 -0
- package/src/installer.js +5 -4
- package/src/providers/anthropic.js +99 -0
- package/src/providers/base.js +4 -0
- package/src/providers/cortex.js +4 -0
- package/src/providers/gemini.js +97 -0
- package/src/providers/index.js +5 -1
- package/src/providers/openai.js +11 -1
- package/src/ui/banner.js +10 -5
- package/src/ui/branding.js +8 -9
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
|
29
|
-
|
|
30
|
-
| vierratale-
|
|
31
|
-
| vierratale-
|
|
32
|
-
| vierratale-
|
|
33
|
-
| vierratale-
|
|
34
|
-
| vierratale-
|
|
35
|
-
| vierratale-
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
20
|
+
execSync('which ollama 2>/dev/null', { stdio: 'ignore' });
|
|
21
21
|
return true;
|
|
22
22
|
} catch {}
|
|
23
|
-
const paths = ['/usr/local/bin/
|
|
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://
|
|
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('
|
|
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
|
+
}
|
package/src/providers/base.js
CHANGED
package/src/providers/cortex.js
CHANGED
|
@@ -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
|
+
}
|
package/src/providers/index.js
CHANGED
|
@@ -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
|
|
package/src/providers/openai.js
CHANGED
|
@@ -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 =
|
|
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(`
|
|
30
|
-
console.log(`
|
|
31
|
-
console.log(`
|
|
32
|
-
console.log(`
|
|
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}`);
|
package/src/ui/branding.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const Branding = {
|
|
2
2
|
APP_NAME: 'VierrataleAI',
|
|
3
|
-
VERSION: '0.1.0-beta.
|
|
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',
|