@vierratale/ai 0.1.0-beta.7 → 0.1.0-beta.8

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
@@ -32,12 +32,12 @@ Local models run through the **Cortex** engine:
32
32
 
33
33
  | Model | Backend |
34
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 |
35
+ | vierratale-lite | qwen3:0.6b |
36
+ | vierratale-fast | gemma3:1b |
37
+ | vierratale-small | llama3.2:1b |
38
+ | vierratale-balanced | qwen2.5:1.5b |
39
+ | vierratale-plus | qwen3:1.7b |
40
+ | vierratale-pro | qwen2.5:3b |
41
41
 
42
42
  Cloud models are available when the matching provider + API key is configured:
43
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vierratale/ai",
3
- "version": "0.1.0-beta.7",
3
+ "version": "0.1.0-beta.8",
4
4
  "description": "VierrataleAI - Intelligent terminal assistant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/catalog.js CHANGED
@@ -1,10 +1,10 @@
1
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',
2
+ 'qwen3:0.6b': 'vierratale-lite',
3
+ 'gemma3:1b': 'vierratale-fast',
4
+ 'llama3.2:1b': 'vierratale-small',
5
+ 'qwen2.5:1.5b': 'vierratale-balanced',
6
+ 'qwen3:1.7b': 'vierratale-plus',
7
+ 'qwen2.5:3b': 'vierratale-pro',
8
8
  'gpt-4o-mini': 'vierratale-cloud-mini',
9
9
  'gpt-4o': 'vierratale-cloud',
10
10
  'claude-3-5-haiku-20241022': 'vierratale-cloud-fast',
package/src/cli.js CHANGED
@@ -237,6 +237,7 @@ async function chat(provider, systemPrompt) {
237
237
  Config.save({ model: modelName });
238
238
  Config.setProviderModel(provider.name, modelName);
239
239
  Terminal.printSuccess(`Model: ${modelName}`);
240
+ await ensureLocalModel(modelName);
240
241
  } else {
241
242
  Terminal.printError(`Unknown model: ${modelName}`);
242
243
  }
@@ -308,6 +309,15 @@ async function chat(provider, systemPrompt) {
308
309
  }
309
310
 
310
311
  const intent = detectIntent(trimmed);
312
+ if (intent.type === 'self') {
313
+ const model = Config.getEffectiveModel(provider.name);
314
+ const info = Catalog.getModelInfo(model);
315
+ Terminal.printSuccess(
316
+ `I'm running on ${model} (${info ? info.realModel : model}) via the ${provider.displayName} engine.`
317
+ );
318
+ continue;
319
+ }
320
+
311
321
  if (intent.type === 'knowledge') {
312
322
  Terminal.printSuccess(`Auto-search: "${trimmed}"`);
313
323
  await answerWithSearch(messages, provider, trimmed, systemPrompt);
@@ -326,7 +336,10 @@ async function chat(provider, systemPrompt) {
326
336
 
327
337
  messages.push({ role: 'user', content: trimmed });
328
338
  Session.save(messages);
339
+ Terminal.printUser(trimmed);
329
340
  Terminal.printAIStart();
341
+ Terminal.beginThinking();
342
+ Terminal._thinking = true;
330
343
 
331
344
  let response = '';
332
345
  try {
@@ -349,6 +362,21 @@ async function chat(provider, systemPrompt) {
349
362
  }
350
363
  }
351
364
 
365
+ async function ensureLocalModel(modelName) {
366
+ const info = Catalog.getModelInfo(modelName);
367
+ if (!info || !info.isLocal) return;
368
+ const host = Config.get('engineHost');
369
+ try {
370
+ const installed = await Installer.getInstalledModels(host);
371
+ if (installed.includes(info.realModel)) return;
372
+ Terminal.printInfo(`Downloading ${info.realModel} for "${modelName}"…`);
373
+ await Installer.pullModel(host, info.realModel);
374
+ Terminal.printSuccess(`Model ${info.realModel} ready.`);
375
+ } catch (err) {
376
+ Terminal.printError(`Could not download model: ${err.message}`);
377
+ }
378
+ }
379
+
352
380
  export async function run() {
353
381
  const args = parseArgs(process.argv);
354
382
 
@@ -366,8 +394,12 @@ export async function run() {
366
394
  if (args.provider) Config.save({ provider: args.provider });
367
395
  if (args.model) Config.save({ model: args.model });
368
396
 
369
- Terminal.printInfo('Initializing...');
370
- await Installer.ensure();
397
+ await Terminal.showProgress(
398
+ ['Checking engine', 'Installing dependencies', 'Downloading model', 'Optimizing'],
399
+ () => Installer.ensure()
400
+ );
401
+
402
+ await ensureLocalModel(Config.get('model'));
371
403
 
372
404
  const provider = await ProviderFactory.autoDetect();
373
405
  if (!provider) {
@@ -377,6 +409,8 @@ export async function run() {
377
409
 
378
410
  Config.save({ provider: provider.name });
379
411
 
412
+ if (provider.warmup) provider.warmup();
413
+
380
414
  if (args.clear) Terminal.clear();
381
415
  showBanner(provider.name, Config.getEffectiveModel(provider.name), provider.displayName);
382
416
 
package/src/installer.js CHANGED
@@ -55,6 +55,14 @@ async function getInstalledModels(host) {
55
55
  }
56
56
 
57
57
  export const Installer = {
58
+ async pullModel(host, model) {
59
+ return pullModel(host, model);
60
+ },
61
+
62
+ async getInstalledModels(host) {
63
+ return getInstalledModels(host);
64
+ },
65
+
58
66
  async ensure() {
59
67
  const host = Config.get('engineHost');
60
68
 
@@ -65,16 +73,23 @@ export const Installer = {
65
73
  const running = await isEngineRunning(host);
66
74
  if (!running) {
67
75
  try {
68
- execSync('ollama serve &', { stdio: 'ignore' });
69
- await new Promise((r) => setTimeout(r, 3000));
76
+ // No systemd available here, so launch the daemon directly, detached,
77
+ // and poll until it responds instead of relying on a service manager.
78
+ const child = exec('ollama serve', {
79
+ stdio: 'ignore',
80
+ detached: true,
81
+ });
82
+ if (child.unref) child.unref();
83
+ for (let i = 0; i < 30; i++) {
84
+ await new Promise((r) => setTimeout(r, 1000));
85
+ if (await isEngineRunning(host)) break;
86
+ }
70
87
  } catch {}
71
88
  }
72
89
 
73
90
  const realModel = Catalog.getRealModel(Config.get('model'));
74
91
  const installed = await getInstalledModels(host);
75
- const needsPull = !installed.some(
76
- (m) => m === realModel || m.startsWith(realModel.split(':')[0])
77
- );
92
+ const needsPull = !installed.includes(realModel);
78
93
 
79
94
  if (needsPull) {
80
95
  await pullModel(host, realModel);
@@ -95,4 +95,21 @@ export class CortexProvider extends BaseProvider {
95
95
  }
96
96
  }
97
97
  }
98
+
99
+ async warmup() {
100
+ // Fire-and-forget: send a tiny request so ollama loads the model into
101
+ // memory eagerly, so the first real query doesn't pay the cold-load cost.
102
+ const model = Catalog.getRealModel(Config.get('model'));
103
+ fetch(`${this.host}/api/chat`, {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({
107
+ model,
108
+ messages: [{ role: 'user', content: 'hi' }],
109
+ stream: false,
110
+ keep_alive: Config.get('keepAlive'),
111
+ options: { num_ctx: Config.get('numCtx') },
112
+ }),
113
+ }).catch(() => {});
114
+ }
98
115
  }
@@ -1,6 +1,6 @@
1
1
  export const Branding = {
2
2
  APP_NAME: 'VierrataleAI',
3
- VERSION: '0.1.0-beta.6',
3
+ VERSION: '0.1.0-beta.8',
4
4
 
5
5
  colors: {
6
6
  primary: '\x1b[38;2;124;58;237m',
@@ -9,25 +9,40 @@ export const Terminal = {
9
9
 
10
10
  printUser(text) {
11
11
  const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
12
+ process.stdout.write('\r\x1b[K');
13
+ console.log();
14
+ console.log(` ${C.bold}${C.primary}You${C.reset} ${C.dim}${time}${C.reset}`);
15
+ console.log(` ${text}`);
12
16
  console.log();
13
- console.log(`${this.divider('─', 4)} ${C.bold}${C.primary}YOU${C.reset} ${C.dim}${time}${C.reset} ${this.divider('─', 34)}`);
14
- console.log(`${C.primary}┃${C.reset} ${text}`);
15
17
  },
16
18
 
17
19
  printAIStart() {
18
20
  const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
19
- this.spinner = { frame: 0, timer: null };
20
21
  console.log();
21
- console.log(`${this.divider('─', 4)} ${C.bold}${C.accent}${Branding.AI_PROMPT}${C.reset} ${C.dim}${time}${C.reset} ${this.divider('─', 34)}`);
22
+ console.log(` ${C.bold}${C.accent}${Branding.AI_PROMPT}${C.reset} ${C.dim}${time}${C.reset}`);
22
23
  this.curLine = '';
23
24
  this._printPrefix();
24
25
  },
25
26
 
26
27
  _printPrefix() {
27
- process.stdout.write(`${C.accent}┃ ${C.reset}`);
28
+ process.stdout.write(` ${C.accent}│${C.reset} `);
29
+ },
30
+
31
+ beginThinking() {
32
+ process.stdout.write(`${C.dim}· · ·${C.reset}`);
33
+ },
34
+
35
+ endThinking() {
36
+ process.stdout.write('\r\x1b[2K');
37
+ this._printPrefix();
28
38
  },
29
39
 
30
40
  printAIChunk(text) {
41
+ this._thinking = this._thinking || false;
42
+ if (this._thinking) {
43
+ this.endThinking();
44
+ this._thinking = false;
45
+ }
31
46
  this.curLine = (this.curLine || '') + text;
32
47
  process.stdout.write(text);
33
48
  this._maybeWrap();
@@ -39,7 +54,7 @@ export const Terminal = {
39
54
  if (words.length > 1) {
40
55
  const lastWord = words[words.length - 1];
41
56
  const rest = this.curLine.slice(0, this.curLine.length - lastWord.length).trimEnd();
42
- process.stdout.write(`\n${C.accent}┃ ${C.reset}`);
57
+ process.stdout.write(`\n${C.accent}│${C.reset} `);
43
58
  this.curLine = lastWord;
44
59
  }
45
60
  }
@@ -47,36 +62,54 @@ export const Terminal = {
47
62
 
48
63
  printAIEnd() {
49
64
  process.stdout.write('\n');
50
- console.log(`${C.accent}┗${C.reset}${this.divider('─', 44)}`);
51
65
  console.log();
52
66
  },
53
67
 
54
68
  printError(msg) {
55
- console.log(`${C.error}✖ ${msg}${C.reset}`);
69
+ console.log(` ${C.error}✖${C.reset} ${msg}`);
56
70
  },
57
71
 
58
72
  printInfo(msg) {
59
- console.log(`${C.dim}${msg}${C.reset}`);
73
+ console.log(` ${C.dim}${msg}${C.reset}`);
60
74
  },
61
75
 
62
76
  printSuccess(msg) {
63
- console.log(`${C.success}✔ ${msg}${C.reset}`);
77
+ console.log(` ${C.success}✔${C.reset} ${msg}`);
64
78
  },
65
79
 
66
80
  printWarning(msg) {
67
- console.log(`${C.warning}⚠ ${msg}${C.reset}`);
81
+ console.log(` ${C.warning}⚠${C.reset} ${msg}`);
68
82
  },
69
83
 
70
84
  printSearchResults(results) {
71
- console.log(`${C.warning}┌── Web Search Results ──${C.reset}`);
85
+ console.log(` ${C.warning}Web Search Results${C.reset}`);
72
86
  results.forEach((r, i) => {
73
- console.log(
74
- ` ${C.warning}${i + 1}.${C.reset} ${C.bold}${r.title}${C.reset}`
75
- );
87
+ console.log(` ${C.warning}${i + 1}.${C.reset} ${C.bold}${r.title}${C.reset}`);
76
88
  console.log(` ${C.dim}${r.url}${C.reset}`);
77
89
  if (r.snippet) console.log(` ${r.snippet.slice(0, 120)}`);
78
90
  });
79
- console.log(`${C.warning}└──${C.reset}`);
91
+ },
92
+
93
+ async showProgress(steps, work) {
94
+ // Hidden real progress: the work() task runs silently while we paint a
95
+ // fake animated progress line so the install feels alive.
96
+ const frames = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
97
+ let i = 0;
98
+ const timer = setInterval(() => {
99
+ i++;
100
+ const stage = steps[Math.floor((i / 8) % steps.length)];
101
+ const bar = frames[i % frames.length];
102
+ const fill = '='.repeat(1 + (i % 18)) + '>' + ' '.repeat(18 - (i % 18));
103
+ process.stdout.write(`\r ${C.accent}${bar}${C.reset} ${C.dim}${stage}${C.reset} [${C.accent}${fill}${C.reset}]`);
104
+ }, 100);
105
+ try {
106
+ await work();
107
+ } finally {
108
+ clearInterval(timer);
109
+ process.stdout.write('\r\x1b[2K');
110
+ console.log(` ${C.success}✔${C.reset} ${C.dim}Ready${C.reset}`);
111
+ console.log();
112
+ }
80
113
  },
81
114
 
82
115
  clear() {
@@ -11,10 +11,22 @@ const KNOWLEDGE_PREFIXES = [
11
11
  'latest', 'news', 'current',
12
12
  ];
13
13
 
14
+ const SELF_PHRASES = [
15
+ 'what model', 'which model', 'what model are you', 'which model are you',
16
+ 'what are you', 'who are you', "what's your name", 'what is your name',
17
+ 'what language model', 'what llm', 'what ai model', 'what ai are you',
18
+ 'your model', 'what model do you use', 'what model you use',
19
+ 'what engine', 'which engine', 'are you gpt', 'are you chatgpt',
20
+ ];
21
+
14
22
  export function detectIntent(text) {
15
23
  const lower = text.toLowerCase().trim();
16
24
  if (lower.startsWith('/')) return { type: 'command' };
17
25
 
26
+ if (SELF_PHRASES.some((p) => lower.includes(p))) {
27
+ return { type: 'self' };
28
+ }
29
+
18
30
  const isQuestion = /\?$/.test(lower);
19
31
  const firstWord = lower.split(/\s+/)[0] || '';
20
32