@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 +6 -6
- package/package.json +1 -1
- package/src/catalog.js +6 -6
- package/src/cli.js +36 -2
- package/src/installer.js +20 -5
- package/src/providers/cortex.js +17 -0
- package/src/ui/branding.js +1 -1
- package/src/ui/terminal.js +49 -16
- package/src/utils/intents.js +12 -0
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 |
|
|
36
|
-
| vierratale-fast |
|
|
37
|
-
| vierratale-small |
|
|
38
|
-
| vierratale-balanced | qwen2.5
|
|
39
|
-
| vierratale-plus |
|
|
40
|
-
| vierratale-pro | qwen2.5
|
|
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
package/src/catalog.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const MODELS = {
|
|
2
|
-
'
|
|
3
|
-
'
|
|
4
|
-
'
|
|
5
|
-
'qwen2.5
|
|
6
|
-
'
|
|
7
|
-
'qwen2.5
|
|
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.
|
|
370
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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.
|
|
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);
|
package/src/providers/cortex.js
CHANGED
|
@@ -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
|
}
|
package/src/ui/branding.js
CHANGED
package/src/ui/terminal.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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}
|
|
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(
|
|
69
|
+
console.log(` ${C.error}✖${C.reset} ${msg}`);
|
|
56
70
|
},
|
|
57
71
|
|
|
58
72
|
printInfo(msg) {
|
|
59
|
-
console.log(
|
|
73
|
+
console.log(` ${C.dim}${msg}${C.reset}`);
|
|
60
74
|
},
|
|
61
75
|
|
|
62
76
|
printSuccess(msg) {
|
|
63
|
-
console.log(
|
|
77
|
+
console.log(` ${C.success}✔${C.reset} ${msg}`);
|
|
64
78
|
},
|
|
65
79
|
|
|
66
80
|
printWarning(msg) {
|
|
67
|
-
console.log(
|
|
81
|
+
console.log(` ${C.warning}⚠${C.reset} ${msg}`);
|
|
68
82
|
},
|
|
69
83
|
|
|
70
84
|
printSearchResults(results) {
|
|
71
|
-
console.log(
|
|
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
|
-
|
|
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() {
|
package/src/utils/intents.js
CHANGED
|
@@ -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
|
|