@vierratale/ai 0.1.0-beta.1

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 ADDED
@@ -0,0 +1,57 @@
1
+ # VierrataleAI
2
+
3
+ Intelligent terminal assistant that runs AI models locally and in the cloud.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # From npm
9
+ npm install -g @vierratale/ai
10
+
11
+ # Or run directly
12
+ npx @vierratale/ai
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ vierrataleai # Start chat (auto-detect provider)
19
+ vierrataleai --provider ollama # Use local models
20
+ vierrataleai --provider openai # Use cloud models
21
+ vierrataleai --model vierratale-pro # Use specific model
22
+ ```
23
+
24
+ ## Models
25
+
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 |
36
+
37
+ ## Commands
38
+
39
+ - `/help` - Show commands
40
+ - `/model [name]` - Switch model
41
+ - `/provider [name]` - Switch provider
42
+ - `/models` - List available models
43
+ - `/clear` - Clear screen
44
+ - `/quit` - Exit
45
+
46
+ ## Configuration
47
+
48
+ Config file: `~/.config/vierrataleai/config.json`
49
+
50
+ Environment variables:
51
+ - `VIERRATALE_PROVIDER` - Default provider
52
+ - `VIERRATALE_MODEL` - Default model
53
+ - `OPENAI_API_KEY` - OpenAI API key (for cloud models)
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/cli.js';
3
+ run();
package/build.sh ADDED
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ # Build VierrataleAI Node.js version for packaging
3
+ set -e
4
+
5
+ cd "$(dirname "$0")"
6
+
7
+ echo "Installing dependencies..."
8
+ npm install
9
+
10
+ echo "Testing..."
11
+ node bin/vierrataleai.js --version
12
+
13
+ echo "Packaging..."
14
+ npm pack
15
+
16
+ echo "Build complete!"
17
+ echo "To publish: npm publish --tag beta"
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@vierratale/ai",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "VierrataleAI - Intelligent terminal assistant",
5
+ "type": "module",
6
+ "bin": {
7
+ "vierrataleai": "./bin/vierrataleai.js"
8
+ },
9
+ "main": "./src/index.js",
10
+ "exports": {
11
+ ".": "./src/index.js"
12
+ },
13
+ "scripts": {
14
+ "start": "node bin/vierrataleai.js"
15
+ },
16
+ "keywords": ["ai", "chatbot", "terminal", "assistant", "llm"],
17
+ "author": "Vierratale",
18
+ "license": "MIT",
19
+ "engines": {
20
+ "node": ">=18.0.0"
21
+ },
22
+ "dependencies": {}
23
+ }
package/src/catalog.js ADDED
@@ -0,0 +1,90 @@
1
+ const MODELS = {
2
+ 'qwen2.5-coder:0.5b': 'vierratale-lite',
3
+ 'qwen2.5-coder:1.5b': 'vierratale-fast',
4
+ 'qwen2.5-coder:3b': 'vierratale-small',
5
+ 'qwen2.5-coder:7b': 'vierratale-balanced',
6
+ 'qwen2.5-coder:14b': 'vierratale-plus',
7
+ 'qwen2.5-coder:32b': 'vierratale-pro',
8
+ 'gpt-4o-mini': 'vierratale-cloud-mini',
9
+ 'gpt-4o': 'vierratale-cloud',
10
+ 'claude-3-5-haiku-20241022': 'vierratale-cloud-fast',
11
+ 'claude-sonnet-4-20250514': 'vierratale-cloud-pro',
12
+ 'gemini-2.0-flash': 'vierratale-cloud-lite',
13
+ 'gemini-1.5-pro': 'vierratale-cloud-plus',
14
+ };
15
+
16
+ const REVERSE = Object.fromEntries(
17
+ Object.entries(MODELS).map(([k, v]) => [v, k])
18
+ );
19
+
20
+ const LOCAL_MODELS = [
21
+ 'vierratale-lite',
22
+ 'vierratale-fast',
23
+ 'vierratale-small',
24
+ 'vierratale-balanced',
25
+ 'vierratale-plus',
26
+ 'vierratale-pro',
27
+ ];
28
+
29
+ const CLOUD_MODELS = [
30
+ 'vierratale-cloud-mini',
31
+ 'vierratale-cloud',
32
+ 'vierratale-cloud-fast',
33
+ 'vierratale-cloud-pro',
34
+ 'vierratale-cloud-lite',
35
+ 'vierratale-cloud-plus',
36
+ ];
37
+
38
+ export const Catalog = {
39
+ getRealModel(displayName) {
40
+ return REVERSE[displayName] || displayName;
41
+ },
42
+
43
+ getDisplayName(realModel) {
44
+ return MODELS[realModel] || realModel;
45
+ },
46
+
47
+ getAllModels() {
48
+ return { ...MODELS };
49
+ },
50
+
51
+ getLocalModels() {
52
+ return [...LOCAL_MODELS];
53
+ },
54
+
55
+ getCloudModels() {
56
+ return [...CLOUD_MODELS];
57
+ },
58
+
59
+ getDefaultModel() {
60
+ return 'vierratale-fast';
61
+ },
62
+
63
+ getModelInfo(displayName) {
64
+ const real = REVERSE[displayName];
65
+ if (!real) return null;
66
+ const isLocal = LOCAL_MODELS.includes(displayName);
67
+ const isCloud = CLOUD_MODELS.includes(displayName);
68
+ const tiers = {
69
+ 'vierratale-lite': 'minimal',
70
+ 'vierratale-fast': 'fast',
71
+ 'vierratale-small': 'small',
72
+ 'vierratale-balanced': 'balanced',
73
+ 'vierratale-plus': 'plus',
74
+ 'vierratale-pro': 'professional',
75
+ 'vierratale-cloud-mini': 'cloud-mini',
76
+ 'vierratale-cloud': 'cloud',
77
+ 'vierratale-cloud-fast': 'cloud-fast',
78
+ 'vierratale-cloud-pro': 'cloud-pro',
79
+ 'vierratale-cloud-lite': 'cloud-lite',
80
+ 'vierratale-cloud-plus': 'cloud-plus',
81
+ };
82
+ return {
83
+ displayName,
84
+ realModel: real,
85
+ isLocal,
86
+ isCloud,
87
+ tier: tiers[displayName] || 'unknown',
88
+ };
89
+ },
90
+ };
package/src/cli.js ADDED
@@ -0,0 +1,206 @@
1
+ import { createInterface } from 'readline';
2
+ import { Config } from './config.js';
3
+ import { Catalog } from './catalog.js';
4
+ import { Installer } from './installer.js';
5
+ import { ProviderFactory } from './providers/index.js';
6
+ import { Branding } from './ui/branding.js';
7
+ import { Terminal } from './ui/terminal.js';
8
+ import { loadSystemPrompt, showBanner } from './ui/banner.js';
9
+
10
+ function parseArgs(args) {
11
+ const parsed = { provider: null, model: null, clear: false, version: false, help: false };
12
+ for (let i = 2; i < args.length; i++) {
13
+ const arg = args[i];
14
+ if (arg === '--provider' || arg === '-p') parsed.provider = args[++i];
15
+ else if (arg === '--model' || arg === '-m') parsed.model = args[++i];
16
+ else if (arg === '--clear') parsed.clear = true;
17
+ else if (arg === '--version' || arg === '-v') parsed.version = true;
18
+ else if (arg === '--help' || arg === '-h') parsed.help = true;
19
+ }
20
+ return parsed;
21
+ }
22
+
23
+ function showHelp() {
24
+ console.log(`
25
+ ${Branding.APP_NAME} ${Branding.VERSION}
26
+ Usage: vierrataleai [options]
27
+
28
+ Options:
29
+ --provider, -p <name> Provider: ollama, openai (default: auto-detect)
30
+ --model, -m <name> Model: vierratale-lite/fast/balanced/pro/cloud
31
+ --clear Clear screen on start
32
+ --version, -v Show version
33
+ --help, -h Show this help
34
+
35
+ Commands (in chat):
36
+ /help Show commands
37
+ /model [name] Switch model
38
+ /provider [name] Switch provider
39
+ /models List available models
40
+ /clear Clear screen
41
+ /quit Exit
42
+ `);
43
+ }
44
+
45
+ async function chat(provider, systemPrompt) {
46
+ const messages = [];
47
+ let exited = false;
48
+
49
+ const rl = createInterface({
50
+ input: process.stdin,
51
+ output: process.stdout,
52
+ terminal: false,
53
+ });
54
+
55
+ rl.on('close', () => {
56
+ if (!exited) {
57
+ exited = true;
58
+ process.exit(0);
59
+ }
60
+ });
61
+
62
+ const prompt = () => new Promise((resolve) => {
63
+ rl.question(
64
+ `${Branding.colors.bold}${Branding.colors.primary}${Branding.USER_PROMPT} >${Branding.colors.reset} `,
65
+ resolve
66
+ );
67
+ });
68
+
69
+ while (!exited) {
70
+ let input;
71
+ try {
72
+ input = await prompt();
73
+ } catch {
74
+ break;
75
+ }
76
+
77
+ if (input === undefined || input === null) break;
78
+
79
+ const trimmed = input.trim();
80
+ if (!trimmed) continue;
81
+
82
+ const cmd = trimmed.toLowerCase();
83
+
84
+ if (cmd === '/quit' || cmd === '/exit' || cmd === '/q') {
85
+ Terminal.printInfo('Goodbye!');
86
+ exited = true;
87
+ rl.close();
88
+ break;
89
+ }
90
+
91
+ if (cmd === '/clear') {
92
+ Terminal.clear();
93
+ showBanner(provider.name, Config.get('model'));
94
+ continue;
95
+ }
96
+
97
+ if (cmd === '/help') {
98
+ showHelp();
99
+ continue;
100
+ }
101
+
102
+ if (cmd === '/models') {
103
+ const models = provider.name === 'ollama'
104
+ ? Catalog.getLocalModels()
105
+ : Catalog.getCloudModels();
106
+ Terminal.printInfo('Available models:');
107
+ for (const m of models) {
108
+ const info = Catalog.getModelInfo(m);
109
+ const current = m === Config.get('model') ? ' (active)' : '';
110
+ console.log(` ${Branding.colors.accent}${m}${Branding.colors.dim}${current} - ${info.tier}${Branding.colors.reset}`);
111
+ }
112
+ continue;
113
+ }
114
+
115
+ if (cmd.startsWith('/model ')) {
116
+ const modelName = cmd.split(' ')[1];
117
+ if (modelName) {
118
+ if (Catalog.getModelInfo(modelName)) {
119
+ Config.save({ model: modelName });
120
+ Terminal.printSuccess(`Model: ${modelName}`);
121
+ } else {
122
+ Terminal.printError(`Unknown model: ${modelName}`);
123
+ }
124
+ }
125
+ continue;
126
+ }
127
+
128
+ if (cmd === '/model') {
129
+ Terminal.printInfo(`Current model: ${Config.get('model')}`);
130
+ continue;
131
+ }
132
+
133
+ if (cmd.startsWith('/provider ')) {
134
+ const prov = cmd.split(' ')[1];
135
+ if (['ollama', 'openai'].includes(prov)) {
136
+ Config.save({ provider: prov });
137
+ Terminal.printSuccess(`Provider: ${prov}`);
138
+ } else {
139
+ Terminal.printError(`Unknown provider: ${prov}`);
140
+ }
141
+ continue;
142
+ }
143
+
144
+ if (cmd === '/provider') {
145
+ Terminal.printInfo(`Current provider: ${provider.name}`);
146
+ continue;
147
+ }
148
+
149
+ messages.push({ role: 'user', content: trimmed });
150
+ Terminal.printAIStart();
151
+
152
+ let response = '';
153
+ try {
154
+ for await (const chunk of provider.stream(messages, {
155
+ model: Config.get('model'),
156
+ systemPrompt,
157
+ })) {
158
+ Terminal.printAIChunk(chunk);
159
+ response += chunk;
160
+ }
161
+ } catch (err) {
162
+ Terminal.printError(err.message || 'Stream error');
163
+ }
164
+
165
+ Terminal.printAIEnd();
166
+ if (response) {
167
+ messages.push({ role: 'assistant', content: response });
168
+ }
169
+ }
170
+ }
171
+
172
+ export async function run() {
173
+ const args = parseArgs(process.argv);
174
+
175
+ if (args.version) {
176
+ console.log(`${Branding.APP_NAME} ${Branding.VERSION}`);
177
+ return;
178
+ }
179
+
180
+ if (args.help) {
181
+ showHelp();
182
+ return;
183
+ }
184
+
185
+ Config.load();
186
+ if (args.provider) Config.save({ provider: args.provider });
187
+ if (args.model) Config.save({ model: args.model });
188
+
189
+ Terminal.printInfo('Initializing...');
190
+ await Installer.ensure();
191
+
192
+ const provider = await ProviderFactory.autoDetect();
193
+ if (!provider) {
194
+ Terminal.printError('No AI engine available. Please install or configure a provider.');
195
+ process.exit(1);
196
+ }
197
+
198
+ Config.save({ provider: provider.name });
199
+
200
+ if (args.clear) Terminal.clear();
201
+ showBanner(provider.name, Config.get('model'));
202
+
203
+ const systemPrompt = loadSystemPrompt();
204
+ await chat(provider, systemPrompt);
205
+ process.exit(0);
206
+ }
package/src/config.js ADDED
@@ -0,0 +1,72 @@
1
+ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ const CONFIG_DIR = join(homedir(), '.config', 'vierrataleai');
6
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
+
8
+ const DEFAULTS = {
9
+ provider: 'auto',
10
+ model: 'vierratale-fast',
11
+ ollamaHost: 'http://127.0.0.1:11434',
12
+ numCtx: 8192,
13
+ temperature: 0.7,
14
+ maxTokens: 4096,
15
+ };
16
+
17
+ function loadConfigFile() {
18
+ try {
19
+ if (existsSync(CONFIG_FILE)) {
20
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
21
+ return JSON.parse(raw);
22
+ }
23
+ } catch {}
24
+ return {};
25
+ }
26
+
27
+ function getEnv(key, fallback) {
28
+ return process.env[key] || fallback;
29
+ }
30
+
31
+ export const Config = {
32
+ _config: null,
33
+
34
+ load() {
35
+ const file = loadConfigFile();
36
+ this._config = {
37
+ provider: getEnv('VIERRATALE_PROVIDER', file.provider || DEFAULTS.provider),
38
+ model: getEnv('VIERRATALE_MODEL', file.model || DEFAULTS.model),
39
+ ollamaHost: getEnv('VIERRATALE_OLLAMA_HOST', file.ollamaHost || DEFAULTS.ollamaHost),
40
+ numCtx: parseInt(getEnv('VIERRATALE_NUM_CTX', String(file.numCtx || DEFAULTS.numCtx))),
41
+ temperature: parseFloat(getEnv('VIERRATALE_TEMPERATURE', String(file.temperature || DEFAULTS.temperature))),
42
+ maxTokens: parseInt(getEnv('VIERRATALE_MAX_TOKENS', String(file.maxTokens || DEFAULTS.maxTokens))),
43
+ openaiApiKey: getEnv('OPENAI_API_KEY', file.openaiApiKey || ''),
44
+ anthropicApiKey: getEnv('ANTHROPIC_API_KEY', file.anthropicApiKey || ''),
45
+ };
46
+ return this._config;
47
+ },
48
+
49
+ get(key) {
50
+ if (!this._config) this.load();
51
+ return this._config[key];
52
+ },
53
+
54
+ getAll() {
55
+ if (!this._config) this.load();
56
+ return { ...this._config };
57
+ },
58
+
59
+ save(overrides = {}) {
60
+ if (!existsSync(CONFIG_DIR)) {
61
+ mkdirSync(CONFIG_DIR, { recursive: true });
62
+ }
63
+ const current = this._config || DEFAULTS;
64
+ const merged = { ...current, ...overrides };
65
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
66
+ this._config = merged;
67
+ },
68
+
69
+ getConfigDir() {
70
+ return CONFIG_DIR;
71
+ },
72
+ };
package/src/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { Catalog } from './catalog.js';
2
+ export { Config } from './config.js';
3
+ export { Installer } from './installer.js';
4
+ export { OllamaProvider } from './providers/ollama.js';
5
+ export { OpenAIProvider } from './providers/openai.js';
6
+ export { Branding } from './ui/branding.js';
@@ -0,0 +1,89 @@
1
+ import { execSync, exec } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { Catalog } from './catalog.js';
4
+ import { Config } from './config.js';
5
+
6
+ function runSilent(cmd) {
7
+ return new Promise((resolve) => {
8
+ exec(cmd, { stdio: 'ignore' }, (err) => resolve(!err));
9
+ });
10
+ }
11
+
12
+ function isOllamaRunning(host) {
13
+ return fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) })
14
+ .then((r) => r.ok)
15
+ .catch(() => false);
16
+ }
17
+
18
+ function isOllamaInstalled() {
19
+ try {
20
+ execSync('which ollama 2>/dev/null', { stdio: 'ignore' });
21
+ return true;
22
+ } catch {}
23
+ const paths = ['/usr/local/bin/ollama', '/usr/bin/ollama'];
24
+ return paths.some((p) => existsSync(p));
25
+ }
26
+
27
+ async function installOllama() {
28
+ await runSilent('curl -fsSL https://ollama.com/install.sh | sh');
29
+ }
30
+
31
+ async function pullModel(host, model) {
32
+ try {
33
+ const resp = await fetch(`${host}/api/pull`, {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({ name: model, stream: false }),
37
+ signal: AbortSignal.timeout(600000),
38
+ });
39
+ return resp.ok;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ async function getInstalledModels(host) {
46
+ try {
47
+ const resp = await fetch(`${host}/api/tags`);
48
+ if (!resp.ok) return [];
49
+ const data = await resp.json();
50
+ return (data.models || []).map((m) => m.name);
51
+ } catch {
52
+ return [];
53
+ }
54
+ }
55
+
56
+ export const Installer = {
57
+ async ensure() {
58
+ const host = Config.get('ollamaHost');
59
+
60
+ if (!isOllamaInstalled()) {
61
+ await installOllama();
62
+ }
63
+
64
+ const running = await isOllamaRunning(host);
65
+ if (!running) {
66
+ try {
67
+ execSync('ollama serve &', { stdio: 'ignore' });
68
+ await new Promise((r) => setTimeout(r, 3000));
69
+ } catch {}
70
+ }
71
+
72
+ const realModel = Catalog.getRealModel(Config.get('model'));
73
+ const installed = await getInstalledModels(host);
74
+ const needsPull = !installed.some(
75
+ (m) => m === realModel || m.startsWith(realModel.split(':')[0])
76
+ );
77
+
78
+ if (needsPull) {
79
+ await pullModel(host, realModel);
80
+ }
81
+
82
+ return true;
83
+ },
84
+
85
+ async isReady() {
86
+ const host = Config.get('ollamaHost');
87
+ return isOllamaRunning(host);
88
+ },
89
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "system": "You are VierrataleAI, an intelligent AI assistant created by Vierratale. You are helpful, concise, and accurate. You provide clear answers and help users with their questions and tasks. When asked about your identity, always respond that you are VierrataleAI, an AI assistant created by Vierratale."
3
+ }
@@ -0,0 +1,15 @@
1
+ # VierrataleAI System Prompt
2
+
3
+ You are **VierrataleAI**, an intelligent AI assistant created by Vierratale.
4
+
5
+ ## Identity
6
+ - When asked "who are you" or similar, always respond: "I am VierrataleAI, an AI assistant created by Vierratale."
7
+ - Never mention underlying engines, providers, or model names.
8
+ - Always refer to yourself as VierrataleAI.
9
+
10
+ ## Behavior
11
+ - Be helpful, concise, and accurate.
12
+ - Provide clear answers to questions.
13
+ - Help users with tasks including coding, writing, analysis, and general knowledge.
14
+ - If you don't know something, say so honestly.
15
+ - Use markdown formatting when appropriate for readability.
@@ -0,0 +1,17 @@
1
+ export class BaseProvider {
2
+ constructor(name) {
3
+ this.name = name;
4
+ }
5
+
6
+ async *stream(messages, options = {}) {
7
+ throw new Error('stream() must be implemented');
8
+ }
9
+
10
+ async listModels() {
11
+ throw new Error('listModels() must be implemented');
12
+ }
13
+
14
+ async isAvailable() {
15
+ return false;
16
+ }
17
+ }
@@ -0,0 +1,41 @@
1
+ import { OllamaProvider } from './ollama.js';
2
+ import { OpenAIProvider } from './openai.js';
3
+ import { Config } from '../config.js';
4
+
5
+ let providers = {};
6
+
7
+ function getProviders() {
8
+ if (Object.keys(providers).length === 0) {
9
+ providers = {
10
+ ollama: new OllamaProvider(),
11
+ openai: new OpenAIProvider(),
12
+ };
13
+ }
14
+ return providers;
15
+ }
16
+
17
+ export const ProviderFactory = {
18
+ async autoDetect() {
19
+ const ps = getProviders();
20
+ const requested = Config.get('provider');
21
+
22
+ if (requested !== 'auto' && ps[requested]) {
23
+ if (await ps[requested].isAvailable()) return ps[requested];
24
+ }
25
+
26
+ for (const name of ['ollama', 'openai']) {
27
+ if (await ps[name].isAvailable()) return ps[name];
28
+ }
29
+
30
+ return null;
31
+ },
32
+
33
+ create(name) {
34
+ const ps = getProviders();
35
+ return ps[name] || null;
36
+ },
37
+
38
+ getAvailable() {
39
+ return getProviders();
40
+ },
41
+ };
@@ -0,0 +1,89 @@
1
+ import { BaseProvider } from './base.js';
2
+ import { Catalog } from '../catalog.js';
3
+ import { Config } from '../config.js';
4
+
5
+ export class OllamaProvider extends BaseProvider {
6
+ constructor() {
7
+ super('ollama');
8
+ this.host = Config.get('ollamaHost');
9
+ }
10
+
11
+ async isAvailable() {
12
+ try {
13
+ const resp = await fetch(`${this.host}/api/tags`, { signal: AbortSignal.timeout(3000) });
14
+ return resp.ok;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ async listModels() {
21
+ try {
22
+ const resp = await fetch(`${this.host}/api/tags`);
23
+ if (!resp.ok) return [];
24
+ const data = await resp.json();
25
+ return (data.models || []).map((m) => ({
26
+ realName: m.name,
27
+ displayName: Catalog.getDisplayName(m.name),
28
+ size: m.size,
29
+ }));
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ async *stream(messages, options = {}) {
36
+ const model = Catalog.getRealModel(options.model || Config.get('model'));
37
+ const systemPrompt = options.systemPrompt || '';
38
+
39
+ const ollamaMessages = [];
40
+ if (systemPrompt) {
41
+ ollamaMessages.push({ role: 'system', content: systemPrompt });
42
+ }
43
+ for (const msg of messages) {
44
+ ollamaMessages.push({ role: msg.role, content: msg.content });
45
+ }
46
+
47
+ const resp = await fetch(`${this.host}/api/chat`, {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: JSON.stringify({
51
+ model,
52
+ messages: ollamaMessages,
53
+ stream: true,
54
+ options: {
55
+ num_ctx: Config.get('numCtx'),
56
+ temperature: options.temperature || Config.get('temperature'),
57
+ },
58
+ }),
59
+ });
60
+
61
+ if (!resp.ok) {
62
+ throw new Error('Failed to connect to backend engine');
63
+ }
64
+
65
+ const reader = resp.body.getReader();
66
+ const decoder = new TextDecoder();
67
+ let buffer = '';
68
+
69
+ while (true) {
70
+ const { done, value } = await reader.read();
71
+ if (done) break;
72
+
73
+ buffer += decoder.decode(value, { stream: true });
74
+ const lines = buffer.split('\n');
75
+ buffer = lines.pop() || '';
76
+
77
+ for (const line of lines) {
78
+ if (!line.trim()) continue;
79
+ try {
80
+ const json = JSON.parse(line);
81
+ if (json.message?.content) {
82
+ yield json.message.content;
83
+ }
84
+ if (json.done) return;
85
+ } catch {}
86
+ }
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,102 @@
1
+ import { BaseProvider } from './base.js';
2
+ import { Catalog } from '../catalog.js';
3
+ import { Config } from '../config.js';
4
+
5
+ export class OpenAIProvider extends BaseProvider {
6
+ constructor() {
7
+ super('openai');
8
+ }
9
+
10
+ async isAvailable() {
11
+ const key = Config.get('openaiApiKey');
12
+ if (!key) return false;
13
+ try {
14
+ const resp = await fetch('https://api.openai.com/v1/models', {
15
+ headers: { Authorization: `Bearer ${key}` },
16
+ signal: AbortSignal.timeout(5000),
17
+ });
18
+ return resp.ok;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ async listModels() {
25
+ const key = Config.get('openaiApiKey');
26
+ if (!key) return [];
27
+ try {
28
+ const resp = await fetch('https://api.openai.com/v1/models', {
29
+ headers: { Authorization: `Bearer ${key}` },
30
+ });
31
+ if (!resp.ok) return [];
32
+ const data = await resp.json();
33
+ return (data.data || [])
34
+ .filter((m) => m.id.startsWith('gpt-'))
35
+ .map((m) => ({
36
+ realName: m.id,
37
+ displayName: Catalog.getDisplayName(m.id),
38
+ }));
39
+ } catch {
40
+ return [];
41
+ }
42
+ }
43
+
44
+ async *stream(messages, options = {}) {
45
+ const key = Config.get('openaiApiKey');
46
+ if (!key) throw new Error('Cloud API key not configured');
47
+
48
+ const model = Catalog.getRealModel(options.model || Config.get('model'));
49
+ const systemPrompt = options.systemPrompt || '';
50
+
51
+ const apiMessages = [];
52
+ if (systemPrompt) {
53
+ apiMessages.push({ role: 'system', content: systemPrompt });
54
+ }
55
+ for (const msg of messages) {
56
+ apiMessages.push({ role: msg.role, content: msg.content });
57
+ }
58
+
59
+ const resp = await fetch('https://api.openai.com/v1/chat/completions', {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Content-Type': 'application/json',
63
+ Authorization: `Bearer ${key}`,
64
+ },
65
+ body: JSON.stringify({
66
+ model,
67
+ messages: apiMessages,
68
+ stream: true,
69
+ temperature: options.temperature || Config.get('temperature'),
70
+ max_tokens: options.maxTokens || Config.get('maxTokens'),
71
+ }),
72
+ });
73
+
74
+ if (!resp.ok) {
75
+ throw new Error('Failed to connect to cloud engine');
76
+ }
77
+
78
+ const reader = resp.body.getReader();
79
+ const decoder = new TextDecoder();
80
+ let buffer = '';
81
+
82
+ while (true) {
83
+ const { done, value } = await reader.read();
84
+ if (done) break;
85
+
86
+ buffer += decoder.decode(value, { stream: true });
87
+ const lines = buffer.split('\n');
88
+ buffer = lines.pop() || '';
89
+
90
+ for (const line of lines) {
91
+ if (!line.startsWith('data: ')) continue;
92
+ const data = line.slice(6);
93
+ if (data === '[DONE]') return;
94
+ try {
95
+ const json = JSON.parse(data);
96
+ const content = json.choices?.[0]?.delta?.content;
97
+ if (content) yield content;
98
+ } catch {}
99
+ }
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,31 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { Branding } from './branding.js';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ export function loadSystemPrompt() {
9
+ try {
10
+ const jsonPath = join(__dirname, '..', 'prompts', 'system.json');
11
+ const data = JSON.parse(readFileSync(jsonPath, 'utf-8'));
12
+ return data.system;
13
+ } catch {
14
+ return `You are ${Branding.APP_NAME}, an intelligent AI assistant. Be helpful, concise, and accurate.`;
15
+ }
16
+ }
17
+
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
+ );
30
+ console.log();
31
+ }
@@ -0,0 +1,31 @@
1
+ export const Branding = {
2
+ APP_NAME: 'VierrataleAI',
3
+ VERSION: '0.1.0-beta.1',
4
+
5
+ colors: {
6
+ primary: '\x1b[38;2;124;58;237m',
7
+ accent: '\x1b[38;2;6;182;212m',
8
+ success: '\x1b[38;2;16;185;129m',
9
+ warning: '\x1b[38;2;245;158;11m',
10
+ error: '\x1b[38;2;239;68;68m',
11
+ dim: '\x1b[2m',
12
+ bold: '\x1b[1m',
13
+ reset: '\x1b[0m',
14
+ },
15
+
16
+ BANNER: [
17
+ '',
18
+ ' ██╗ ██╗██╗██████╗ ███████╗',
19
+ ' ██║ ██║██║██╔══██╗██╔════╝',
20
+ ' ██║ ██║██║██████╔╝█████╗ ',
21
+ ' ╚██╗ ██╔╝██║██╔══██╗██╔══╝ ',
22
+ ' ╚████╔╝ ██║██████╔╝███████╗',
23
+ ' ╚═══╝ ╚═╝╚═════╝ ╚══════╝',
24
+ '',
25
+ ].join('\n'),
26
+
27
+ USER_PROMPT: 'You',
28
+ AI_PROMPT: 'AI',
29
+
30
+ SYSTEM_PROMPT_FILE: null,
31
+ };
@@ -0,0 +1,49 @@
1
+ import { Branding } from './branding.js';
2
+
3
+ const C = Branding.colors;
4
+
5
+ export const Terminal = {
6
+ printUser(text) {
7
+ console.log(`${C.bold}${C.primary}${Branding.USER_PROMPT} >${C.reset} ${text}`);
8
+ },
9
+
10
+ printAIStart() {
11
+ process.stdout.write(`${C.bold}${C.accent}${Branding.AI_PROMPT} >${C.reset} `);
12
+ },
13
+
14
+ printAIChunk(text) {
15
+ process.stdout.write(text);
16
+ },
17
+
18
+ printAIEnd() {
19
+ console.log();
20
+ console.log();
21
+ },
22
+
23
+ printError(msg) {
24
+ console.error(`${C.error}Error: ${msg}${C.reset}`);
25
+ },
26
+
27
+ printInfo(msg) {
28
+ console.log(`${C.dim}${msg}${C.reset}`);
29
+ },
30
+
31
+ printSuccess(msg) {
32
+ console.log(`${C.success}${msg}${C.reset}`);
33
+ },
34
+
35
+ printWarning(msg) {
36
+ console.log(`${C.warning}${msg}${C.reset}`);
37
+ },
38
+
39
+ clear() {
40
+ process.stdout.write('\x1b[2J\x1b[H');
41
+ },
42
+
43
+ formatMarkdown(text) {
44
+ return text
45
+ .replace(/\*\*(.*?)\*\*/g, `${C.bold}$1${C.reset}`)
46
+ .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}`);
48
+ },
49
+ };
@@ -0,0 +1,29 @@
1
+ export const Http = {
2
+ async fetch(url, options = {}) {
3
+ const timeout = options.timeout || 30000;
4
+ const controller = new AbortController();
5
+ const timer = setTimeout(() => controller.abort(), timeout);
6
+
7
+ try {
8
+ const resp = await fetch(url, {
9
+ ...options,
10
+ signal: controller.signal,
11
+ });
12
+ return resp;
13
+ } finally {
14
+ clearTimeout(timer);
15
+ }
16
+ },
17
+
18
+ async post(url, body, options = {}) {
19
+ return this.fetch(url, {
20
+ method: 'POST',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ ...options.headers,
24
+ },
25
+ body: JSON.stringify(body),
26
+ ...options,
27
+ });
28
+ },
29
+ };
@@ -0,0 +1,32 @@
1
+ import { platform, homedir } from 'os';
2
+
3
+ export const Platform = {
4
+ isLinux() {
5
+ return platform() === 'linux';
6
+ },
7
+
8
+ isMac() {
9
+ return platform() === 'darwin';
10
+ },
11
+
12
+ isWindows() {
13
+ return platform() === 'win32';
14
+ },
15
+
16
+ isTermux() {
17
+ return process.env.TERMUX_VERSION !== undefined || platform().includes('android');
18
+ },
19
+
20
+ getHomeDir() {
21
+ return homedir();
22
+ },
23
+
24
+ getConfigDir() {
25
+ const home = homedir();
26
+ if (this.isTermux()) {
27
+ return `${home}/.config/vierrataleai`;
28
+ }
29
+ const base = process.env.XDG_CONFIG_HOME || `${home}/.config`;
30
+ return `${base}/vierrataleai`;
31
+ },
32
+ };