@vierratale/ai 0.1.0-beta.13 → 0.1.0-beta.15
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/LICENSE +21 -0
- package/README.md +30 -22
- package/bin/vierrataleai.js +3 -13
- package/package.json +2 -1
- package/scripts/postinstall.js +2 -2
- package/src/catalog.js +98 -59
- package/src/cli.js +11 -11
- package/src/config.js +1 -1
- package/src/engine.js +150 -0
- package/src/index.js +1 -1
- package/src/providers/cortex.js +3 -3
- package/src/ui/branding.js +1 -1
- package/src/installer.js +0 -300
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vierratale
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ npx @vierratale/ai
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
The first launch detects and, if needed, installs and starts the local engine
|
|
25
|
-
(**Cortex**
|
|
25
|
+
(**Cortex**), then pulls the model you use.
|
|
26
26
|
|
|
27
27
|
## Usage
|
|
28
28
|
|
|
@@ -32,36 +32,44 @@ vierrataleai --provider cortex # Use local models
|
|
|
32
32
|
vierrataleai --provider openai # Use cloud models (OpenAI)
|
|
33
33
|
vierrataleai --provider anthropic # Use cloud models (Claude)
|
|
34
34
|
vierrataleai --provider gemini # Use cloud models (Gemini)
|
|
35
|
-
vierrataleai --model
|
|
35
|
+
vierrataleai --model VTL-3.7-Ultra # Use specific model
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Models
|
|
39
39
|
|
|
40
40
|
Local models run through the **Cortex** engine:
|
|
41
41
|
|
|
42
|
-
| Model |
|
|
43
|
-
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
42
|
+
| Model | Tier |
|
|
43
|
+
|-------|------|
|
|
44
|
+
| VTL-2.5-Mini | minimal |
|
|
45
|
+
| VTL-2.7-Flash | fast |
|
|
46
|
+
| VTL-2.9-Core | small |
|
|
47
|
+
| VTL-3.2-Orbit | small |
|
|
48
|
+
| VTL-3.1-Plus | balanced |
|
|
49
|
+
| VTL-3.3-Pro | coder |
|
|
50
|
+
| VTL-3.5-Reason | reasoning |
|
|
51
|
+
| VTL-3.7-Ultra | ultra |
|
|
52
|
+
| VTL-4.7-Gusto | balanced |
|
|
53
|
+
| VTL-5.4-Tempo | balanced |
|
|
54
|
+
| VTL-5.2-Pinnacle | pro |
|
|
55
|
+
| VTL-5.9-Sovereign | pro |
|
|
50
56
|
|
|
51
57
|
Cloud models are available when the matching provider + API key is configured:
|
|
52
58
|
|
|
53
|
-
| Model |
|
|
54
|
-
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
| Model | Tier |
|
|
60
|
+
|-------|------|
|
|
61
|
+
| VTL-4.0 | cloud-mini |
|
|
62
|
+
| VTL-4.2-Omni | cloud |
|
|
63
|
+
| VTL-4.5-Plus | cloud-fast |
|
|
64
|
+
| VTL-5.0-Pro | cloud-pro |
|
|
65
|
+
| VTL-6.0-Reason | cloud-lite |
|
|
66
|
+
| VTL-7.0-Omnij | cloud-plus |
|
|
67
|
+
| VTL-8.0-Fabric | cloud-fabric |
|
|
68
|
+
| VTL-9.0-Forge | cloud-forge |
|
|
69
|
+
| VTL-10.0-Singularity | cloud-singularity |
|
|
70
|
+
|
|
71
|
+
> Legacy `vierratale-*` and old `VRTL-*` model names (e.g. `vierratale-fast`,
|
|
72
|
+
> `vierratale-cloud`) still work and map to the VTL names.
|
|
65
73
|
|
|
66
74
|
## Commands
|
|
67
75
|
|
package/bin/vierrataleai.js
CHANGED
|
@@ -9,11 +9,9 @@
|
|
|
9
9
|
// Installation progress is always the fake animated bar — real engine/model
|
|
10
10
|
// output is deliberately hidden so first-run feels quick and clean.
|
|
11
11
|
import { run } from '../src/cli.js';
|
|
12
|
-
import {
|
|
12
|
+
import { Engine } from '../src/engine.js';
|
|
13
13
|
import { Config } from '../src/config.js';
|
|
14
14
|
import { Branding } from '../src/ui/branding.js';
|
|
15
|
-
import { Terminal } from '../src/ui/terminal.js';
|
|
16
|
-
import { dirname } from 'path';
|
|
17
15
|
|
|
18
16
|
const C = Branding.colors;
|
|
19
17
|
|
|
@@ -63,19 +61,11 @@ async function main() {
|
|
|
63
61
|
|
|
64
62
|
if (doInstall) {
|
|
65
63
|
console.log(`\n ${C.bold}${C.primary}${Branding.APP_NAME} ${Branding.VERSION}${C.reset} ${C.dim}— installer${C.reset}\n`);
|
|
66
|
-
|
|
67
|
-
if (!result.command) {
|
|
68
|
-
Terminal.printInfo('Could not create a global command; run it via the bin script.');
|
|
69
|
-
} else {
|
|
70
|
-
Terminal.printSuccess(`Engine + model installed; "vierrataleai" (or "vierratale") command available at ${result.commandAlias || result.command}`);
|
|
71
|
-
if (!result.commandOnPath) {
|
|
72
|
-
Terminal.printInfo(`"vierrataleai" won't resolve in a new shell: add ${dirname(result.command)} to PATH, e.g. export PATH="$HOME/.local/bin:$PATH", then reopen the terminal.`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
64
|
+
await fakeProgress(INSTALL_STEPS, Engine.ensure());
|
|
75
65
|
console.log();
|
|
76
66
|
} else if (!quick) {
|
|
77
67
|
// Auto-detect: install anything that's missing (engine, model) before starting.
|
|
78
|
-
await fakeProgress(READY_STEPS,
|
|
68
|
+
await fakeProgress(READY_STEPS, Engine.ensure());
|
|
79
69
|
console.log();
|
|
80
70
|
}
|
|
81
71
|
|
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.15",
|
|
4
4
|
"description": "VierrataleAI - Intelligent terminal assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"bin",
|
|
19
19
|
"src",
|
|
20
20
|
"scripts",
|
|
21
|
+
"LICENSE",
|
|
21
22
|
"README.md"
|
|
22
23
|
],
|
|
23
24
|
"keywords": [
|
package/scripts/postinstall.js
CHANGED
|
@@ -25,9 +25,9 @@ try {
|
|
|
25
25
|
execSync('curl -fsSL https://ollama.com/install.sh | sh', { stdio: 'inherit' });
|
|
26
26
|
} else {
|
|
27
27
|
console.log(
|
|
28
|
-
'\n[vierrataleai] The local engine
|
|
28
|
+
'\n[vierrataleai] The local engine was not detected.\n' +
|
|
29
29
|
' - Run the CLI: it will attempt to install and start the engine automatically.\n' +
|
|
30
|
-
' - Or install
|
|
30
|
+
' - Or run ./install.sh (in the project repo) to set up the engine.\n'
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
} catch {
|
package/src/catalog.js
CHANGED
|
@@ -1,36 +1,59 @@
|
|
|
1
|
-
// Model catalog. Display names use the
|
|
2
|
-
//
|
|
1
|
+
// Model catalog. Display names use the opaque VTL branding:
|
|
2
|
+
// VTL-<number>.<name>, e.g. VTL-2.7-Flash, VTL-4.0.
|
|
3
|
+
// The real engine tags are mapped to display names here and kept internal.
|
|
3
4
|
const MODELS = {
|
|
4
|
-
'qwen3:0.6b': '
|
|
5
|
-
'gemma3:1b': '
|
|
6
|
-
'llama3.2:1b': '
|
|
7
|
-
'
|
|
8
|
-
'qwen2.5
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
5
|
+
'qwen3:0.6b': 'VTL-2.5-Mini',
|
|
6
|
+
'gemma3:1b': 'VTL-2.7-Flash',
|
|
7
|
+
'llama3.2:1b': 'VTL-2.9-Core',
|
|
8
|
+
'llama3.2:3b': 'VTL-3.2-Orbit',
|
|
9
|
+
'qwen2.5:1.5b': 'VTL-3.1-Plus',
|
|
10
|
+
'qwen2.5-coder:1.5b': 'VTL-3.3-Pro',
|
|
11
|
+
'qwen3:1.7b': 'VTL-3.5-Reason',
|
|
12
|
+
'qwen2.5:3b': 'VTL-3.7-Ultra',
|
|
13
|
+
'gemma3:4b': 'VTL-4.7-Gusto',
|
|
14
|
+
'phi4-mini': 'VTL-5.4-Tempo',
|
|
15
|
+
'glm4:9b': 'VTL-5.2-Pinnacle',
|
|
16
|
+
'qwen3:4b': 'VTL-5.9-Sovereign',
|
|
17
|
+
'gpt-4o-mini': 'VTL-4.0',
|
|
18
|
+
'gpt-4o': 'VTL-4.2-Omni',
|
|
19
|
+
'claude-3-5-haiku-20241022': 'VTL-4.5-Plus',
|
|
20
|
+
'claude-sonnet-4-20250514': 'VTL-5.0-Pro',
|
|
21
|
+
'gemini-2.0-flash': 'VTL-6.0-Reason',
|
|
22
|
+
'gemini-1.5-pro': 'VTL-7.0-Omnij',
|
|
23
|
+
'kimi-k2.6:cloud': 'VTL-8.0-Fabric',
|
|
24
|
+
'kimi-k2.7-code:cloud': 'VTL-9.0-Forge',
|
|
25
|
+
'kimi-k3': 'VTL-10.0-Singularity',
|
|
17
26
|
};
|
|
18
27
|
|
|
19
|
-
//
|
|
20
|
-
// compatibility (existing config files may
|
|
28
|
+
// Legacy display names (older `VRTL-*` and `vierratale-<word>`), accepted
|
|
29
|
+
// everywhere for backwards compatibility (existing config files may contain
|
|
30
|
+
// them). All map onto the current VTL display names.
|
|
21
31
|
const LEGACY_DISPLAY = {
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
32
|
+
'VRTL-1.lite': 'VTL-2.5-Mini',
|
|
33
|
+
'VRTL-2.fast': 'VTL-2.7-Flash',
|
|
34
|
+
'VRTL-3.small': 'VTL-2.9-Core',
|
|
35
|
+
'VRTL-4.balanced': 'VTL-3.1-Plus',
|
|
36
|
+
'VRTL-4.coder': 'VTL-3.3-Pro',
|
|
37
|
+
'VRTL-5.plus': 'VTL-3.5-Reason',
|
|
38
|
+
'VRTL-6.pro': 'VTL-3.7-Ultra',
|
|
39
|
+
'VRTL-7.cloud-mini': 'VTL-4.0',
|
|
40
|
+
'VRTL-8.cloud': 'VTL-4.2-Omni',
|
|
41
|
+
'VRTL-9.cloud-fast': 'VTL-4.5-Plus',
|
|
42
|
+
'VRTL-10.cloud-pro': 'VTL-5.0-Pro',
|
|
43
|
+
'VRTL-11.cloud-lite': 'VTL-6.0-Reason',
|
|
44
|
+
'VRTL-12.cloud-plus': 'VTL-7.0-Omnij',
|
|
45
|
+
'vierratale-lite': 'VTL-2.5-Mini',
|
|
46
|
+
'vierratale-fast': 'VTL-2.7-Flash',
|
|
47
|
+
'vierratale-small': 'VTL-2.9-Core',
|
|
48
|
+
'vierratale-balanced': 'VTL-3.1-Plus',
|
|
49
|
+
'vierratale-plus': 'VTL-3.5-Reason',
|
|
50
|
+
'vierratale-pro': 'VTL-3.7-Ultra',
|
|
51
|
+
'vierratale-cloud-mini': 'VTL-4.0',
|
|
52
|
+
'vierratale-cloud': 'VTL-4.2-Omni',
|
|
53
|
+
'vierratale-cloud-fast': 'VTL-4.5-Plus',
|
|
54
|
+
'vierratale-cloud-pro': 'VTL-5.0-Pro',
|
|
55
|
+
'vierratale-cloud-lite': 'VTL-6.0-Reason',
|
|
56
|
+
'vierratale-cloud-plus': 'VTL-7.0-Omnij',
|
|
34
57
|
};
|
|
35
58
|
|
|
36
59
|
const REVERSE = Object.fromEntries(
|
|
@@ -38,42 +61,58 @@ const REVERSE = Object.fromEntries(
|
|
|
38
61
|
);
|
|
39
62
|
|
|
40
63
|
const LOCAL_MODELS = [
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
64
|
+
'VTL-2.5-Mini',
|
|
65
|
+
'VTL-2.7-Flash',
|
|
66
|
+
'VTL-2.9-Core',
|
|
67
|
+
'VTL-3.2-Orbit',
|
|
68
|
+
'VTL-3.1-Plus',
|
|
69
|
+
'VTL-3.3-Pro',
|
|
70
|
+
'VTL-3.5-Reason',
|
|
71
|
+
'VTL-3.7-Ultra',
|
|
72
|
+
'VTL-4.7-Gusto',
|
|
73
|
+
'VTL-5.4-Tempo',
|
|
74
|
+
'VTL-5.2-Pinnacle',
|
|
75
|
+
'VTL-5.9-Sovereign',
|
|
48
76
|
];
|
|
49
77
|
|
|
50
78
|
const CLOUD_MODELS = [
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
79
|
+
'VTL-4.0',
|
|
80
|
+
'VTL-4.2-Omni',
|
|
81
|
+
'VTL-4.5-Plus',
|
|
82
|
+
'VTL-5.0-Pro',
|
|
83
|
+
'VTL-6.0-Reason',
|
|
84
|
+
'VTL-7.0-Omnij',
|
|
85
|
+
'VTL-8.0-Fabric',
|
|
86
|
+
'VTL-9.0-Forge',
|
|
87
|
+
'VTL-10.0-Singularity',
|
|
57
88
|
];
|
|
58
89
|
|
|
59
90
|
const TIERS = {
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
91
|
+
'VTL-2.5-Mini': 'minimal',
|
|
92
|
+
'VTL-2.7-Flash': 'fast',
|
|
93
|
+
'VTL-2.9-Core': 'small',
|
|
94
|
+
'VTL-3.2-Orbit': 'small',
|
|
95
|
+
'VTL-3.1-Plus': 'balanced',
|
|
96
|
+
'VTL-3.3-Pro': 'coder',
|
|
97
|
+
'VTL-3.5-Reason': 'reasoning',
|
|
98
|
+
'VTL-3.7-Ultra': 'ultra',
|
|
99
|
+
'VTL-4.7-Gusto': 'balanced',
|
|
100
|
+
'VTL-5.4-Tempo': 'balanced',
|
|
101
|
+
'VTL-5.2-Pinnacle': 'pro',
|
|
102
|
+
'VTL-5.9-Sovereign': 'pro',
|
|
103
|
+
'VTL-4.0': 'cloud-mini',
|
|
104
|
+
'VTL-4.2-Omni': 'cloud',
|
|
105
|
+
'VTL-4.5-Plus': 'cloud-fast',
|
|
106
|
+
'VTL-5.0-Pro': 'cloud-pro',
|
|
107
|
+
'VTL-6.0-Reason': 'cloud-lite',
|
|
108
|
+
'VTL-7.0-Omnij': 'cloud-plus',
|
|
109
|
+
'VTL-8.0-Fabric': 'cloud-fabric',
|
|
110
|
+
'VTL-9.0-Forge': 'cloud-forge',
|
|
111
|
+
'VTL-10.0-Singularity': 'cloud-singularity',
|
|
73
112
|
};
|
|
74
113
|
|
|
75
114
|
export const Catalog = {
|
|
76
|
-
// Map any known name (new or legacy) to the current
|
|
115
|
+
// Map any known name (new or legacy) to the current VTL display name.
|
|
77
116
|
normalize(displayName) {
|
|
78
117
|
return LEGACY_DISPLAY[displayName] || displayName;
|
|
79
118
|
},
|
|
@@ -99,11 +138,11 @@ export const Catalog = {
|
|
|
99
138
|
},
|
|
100
139
|
|
|
101
140
|
getDefaultModel() {
|
|
102
|
-
return '
|
|
141
|
+
return 'VTL-2.7-Flash';
|
|
103
142
|
},
|
|
104
143
|
|
|
105
144
|
getDefaultCloudModel() {
|
|
106
|
-
return '
|
|
145
|
+
return 'VTL-4.0';
|
|
107
146
|
},
|
|
108
147
|
|
|
109
148
|
isLocalModel(displayName) {
|
|
@@ -136,4 +175,4 @@ export const Catalog = {
|
|
|
136
175
|
tier: TIERS[name] || 'unknown',
|
|
137
176
|
};
|
|
138
177
|
},
|
|
139
|
-
};
|
|
178
|
+
};
|
package/src/cli.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from 'child_process';
|
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { Config } from './config.js';
|
|
4
4
|
import { Catalog } from './catalog.js';
|
|
5
|
-
import {
|
|
5
|
+
import { Engine } from './engine.js';
|
|
6
6
|
import { ProviderFactory } from './providers/index.js';
|
|
7
7
|
import { Branding } from './ui/branding.js';
|
|
8
8
|
import { Terminal } from './ui/terminal.js';
|
|
@@ -41,7 +41,7 @@ Usage: vierrataleai [options]
|
|
|
41
41
|
|
|
42
42
|
Options:
|
|
43
43
|
--provider, -p <name> Provider: cortex, openai (default: auto-detect)
|
|
44
|
-
--model, -m <name> Model:
|
|
44
|
+
--model, -m <name> Model: e.g. VTL-2.7-Flash/3.5-Reason/6.0-Reason
|
|
45
45
|
--clear (deprecated: screen is always cleared on start)
|
|
46
46
|
--install, --setup Install engine + model + a "vierrataleai" command, then run
|
|
47
47
|
--version, -v Show version
|
|
@@ -811,7 +811,7 @@ async function chat(provider, systemPrompt) {
|
|
|
811
811
|
: Catalog.getCloudModels();
|
|
812
812
|
const installedSet = new Set();
|
|
813
813
|
if (provider.isLocal) {
|
|
814
|
-
for (const m of await
|
|
814
|
+
for (const m of await Engine.getInstalledModels(Config.get('engineHost'))) {
|
|
815
815
|
installedSet.add(m);
|
|
816
816
|
}
|
|
817
817
|
}
|
|
@@ -830,8 +830,8 @@ async function chat(provider, systemPrompt) {
|
|
|
830
830
|
const extras = [...installedSet].filter(
|
|
831
831
|
(m) => !Catalog.getModelInfo(m) && m !== Config.getEffectiveModel(provider.name)
|
|
832
832
|
);
|
|
833
|
-
|
|
834
|
-
chatUI.notify(`
|
|
833
|
+
if (extras.length) {
|
|
834
|
+
chatUI.notify(` (+${extras.length} additional engine models available)`);
|
|
835
835
|
}
|
|
836
836
|
}
|
|
837
837
|
chatUI.render(messages);
|
|
@@ -850,7 +850,7 @@ async function chat(provider, systemPrompt) {
|
|
|
850
850
|
chatUI.setModel(model, provider.displayName);
|
|
851
851
|
};
|
|
852
852
|
if (provider.isLocal && Catalog.isCloudModel(canonical)) {
|
|
853
|
-
chatUI.notify(`Cannot use cloud model "${canonical}" on ${provider.displayName} (local). Use /model
|
|
853
|
+
chatUI.notify(`Cannot use cloud model "${canonical}" on ${provider.displayName} (local). Use /model VTL-3.7-Ultra or /provider openai.`);
|
|
854
854
|
} else if (!info) {
|
|
855
855
|
if (provider.isLocal) {
|
|
856
856
|
const switched = await ensureRawLocalModel(modelName, (m) => chatUI.notify(m));
|
|
@@ -993,7 +993,7 @@ async function chat(provider, systemPrompt) {
|
|
|
993
993
|
const info = Catalog.getModelInfo(model);
|
|
994
994
|
chatUI.clearNotices();
|
|
995
995
|
chatUI.notify(
|
|
996
|
-
`I'm running on ${model}
|
|
996
|
+
`I'm running on ${model} via the ${provider.displayName} engine.`
|
|
997
997
|
);
|
|
998
998
|
chatUI.render(messages);
|
|
999
999
|
continue;
|
|
@@ -1114,18 +1114,18 @@ async function ensureLocalModel(modelName, notify) {
|
|
|
1114
1114
|
// dead-ends while at least one local model exists.
|
|
1115
1115
|
async function ensureRawLocalModel(realModel, notify) {
|
|
1116
1116
|
const host = Config.get('engineHost');
|
|
1117
|
-
const resolved = await
|
|
1117
|
+
const resolved = await Engine.resolveModel(host, realModel);
|
|
1118
1118
|
if (resolved.model) {
|
|
1119
1119
|
return { ok: true, model: resolved.model, reason: resolved.reason };
|
|
1120
1120
|
}
|
|
1121
1121
|
if (notify) notify(`Downloading ${realModel}… (this can take a while)`);
|
|
1122
|
-
const pulled = await
|
|
1122
|
+
const pulled = await Engine.pullModel(host, realModel);
|
|
1123
1123
|
if (pulled) {
|
|
1124
1124
|
if (notify) notify(`Model ${realModel} ready.`);
|
|
1125
1125
|
return { ok: true, model: realModel, reason: null };
|
|
1126
1126
|
}
|
|
1127
1127
|
if (notify) notify(`Could not download ${realModel}.`);
|
|
1128
|
-
const first = await
|
|
1128
|
+
const first = await Engine.firstInstalled(host);
|
|
1129
1129
|
if (first) {
|
|
1130
1130
|
return { ok: true, model: first, reason: `Could not download ${realModel}; using installed ${first}` };
|
|
1131
1131
|
}
|
|
@@ -1264,7 +1264,7 @@ export async function run() {
|
|
|
1264
1264
|
|
|
1265
1265
|
await Terminal.showProgress(
|
|
1266
1266
|
['Checking engine', 'Installing dependencies', 'Downloading model', 'Optimizing'],
|
|
1267
|
-
() =>
|
|
1267
|
+
() => Engine.ensure()
|
|
1268
1268
|
);
|
|
1269
1269
|
|
|
1270
1270
|
await ensureLocalModel(Config.get('model'));
|
package/src/config.js
CHANGED
package/src/engine.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
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 isEngineRunning(host) {
|
|
13
|
+
return fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) })
|
|
14
|
+
.then((r) => r.ok)
|
|
15
|
+
.catch(() => false);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isEngineInstalled() {
|
|
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 installEngine() {
|
|
28
|
+
await runSilent('curl -fsSL https://ollama.com/install.sh | sh');
|
|
29
|
+
return isEngineInstalled();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function getInstalledModels(host) {
|
|
33
|
+
try {
|
|
34
|
+
const resp = await fetch(`${host}/api/tags`);
|
|
35
|
+
if (!resp.ok) return [];
|
|
36
|
+
const data = await resp.json();
|
|
37
|
+
return (data.models || []).map((m) => m.name);
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function pullModel(host, model) {
|
|
44
|
+
try {
|
|
45
|
+
const resp = await fetch(`${host}/api/pull`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({ name: model, stream: false }),
|
|
49
|
+
signal: AbortSignal.timeout(600000),
|
|
50
|
+
});
|
|
51
|
+
return resp.ok;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function familyOf(model) {
|
|
58
|
+
return model
|
|
59
|
+
.split(':')[0]
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/-(coder|instruct|reasoning|chat|base|tiny|nano)$/, '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sizeOf(model) {
|
|
65
|
+
const size = model.split(':')[1] || '';
|
|
66
|
+
const match = size.match(/(\d+(?:\.\d+)?)(m|b)/i);
|
|
67
|
+
if (!match) return Number.POSITIVE_INFINITY;
|
|
68
|
+
const value = parseFloat(match[1]);
|
|
69
|
+
return match[2].toLowerCase() === 'b' ? value * 1000 : value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function chooseModel(installed, realModel) {
|
|
73
|
+
if (installed.includes(realModel)) {
|
|
74
|
+
return { model: realModel, substituted: false, reason: null };
|
|
75
|
+
}
|
|
76
|
+
const fam = familyOf(realModel);
|
|
77
|
+
let best = null;
|
|
78
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
79
|
+
for (const m of installed) {
|
|
80
|
+
if (familyOf(m) !== fam) continue;
|
|
81
|
+
const dist = Math.abs(sizeOf(m) - sizeOf(realModel)) + 0.5;
|
|
82
|
+
if (dist < bestDist) {
|
|
83
|
+
bestDist = dist;
|
|
84
|
+
best = m;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (best) {
|
|
88
|
+
return {
|
|
89
|
+
model: best,
|
|
90
|
+
substituted: true,
|
|
91
|
+
reason: `"${realModel}" is not installed; using ${best} (same family)`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { model: null, substituted: true, reason: `"${realModel}" is not installed` };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function resolveModel(host, realModel) {
|
|
98
|
+
const installed = await getInstalledModels(host);
|
|
99
|
+
return chooseModel(installed, realModel);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function ensure() {
|
|
103
|
+
const host = Config.get('engineHost');
|
|
104
|
+
|
|
105
|
+
if (!isEngineInstalled()) {
|
|
106
|
+
await installEngine();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const running = await isEngineRunning(host);
|
|
110
|
+
if (!running) {
|
|
111
|
+
try {
|
|
112
|
+
const child = exec('ollama serve', {
|
|
113
|
+
stdio: 'ignore',
|
|
114
|
+
detached: true,
|
|
115
|
+
});
|
|
116
|
+
if (child.unref) child.unref();
|
|
117
|
+
for (let i = 0; i < 30; i++) {
|
|
118
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
119
|
+
if (await isEngineRunning(host)) break;
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const realModel = Catalog.getRealModel(Config.get('model'));
|
|
125
|
+
const installed = await getInstalledModels(host);
|
|
126
|
+
const resolved = chooseModel(installed, realModel);
|
|
127
|
+
if (resolved.model == null) {
|
|
128
|
+
await pullModel(host, realModel);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function isReady() {
|
|
135
|
+
const host = Config.get('engineHost');
|
|
136
|
+
return isEngineRunning(host);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const Engine = {
|
|
140
|
+
ensure,
|
|
141
|
+
isReady,
|
|
142
|
+
pullModel,
|
|
143
|
+
getInstalledModels,
|
|
144
|
+
resolveModel,
|
|
145
|
+
chooseModel,
|
|
146
|
+
firstInstalled: async (host) => {
|
|
147
|
+
const installed = await getInstalledModels(host);
|
|
148
|
+
return installed[0] || null;
|
|
149
|
+
},
|
|
150
|
+
};
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Catalog } from './catalog.js';
|
|
2
2
|
export { Config } from './config.js';
|
|
3
|
-
export {
|
|
3
|
+
export { Engine } from './engine.js';
|
|
4
4
|
export { CortexProvider } from './providers/cortex.js';
|
|
5
5
|
export { OpenAIProvider } from './providers/openai.js';
|
|
6
6
|
export { Branding } from './ui/branding.js';
|
package/src/providers/cortex.js
CHANGED
|
@@ -285,14 +285,14 @@ export class CortexProvider extends BaseProvider {
|
|
|
285
285
|
// engine host is deliberately kept out of the message; errors carry codes.
|
|
286
286
|
async function describeEngineError(resp, model) {
|
|
287
287
|
if (/not found|does not exist|model.*missing/i.test(String(resp.statusText))) {
|
|
288
|
-
return `[ERR-0002] Model "${model}" is not installed on the engine. If you picked a cloud model, use /model
|
|
288
|
+
return `[ERR-0002] Model "${model}" is not installed on the engine. If you picked a cloud model, use /model VTL-3.7-Ultra or /provider openai.`;
|
|
289
289
|
}
|
|
290
290
|
let body = '';
|
|
291
291
|
try {
|
|
292
292
|
body = (await resp.text()).slice(0, 300);
|
|
293
293
|
} catch {}
|
|
294
294
|
if (/not found|does not exist|model.*missing/i.test(body)) {
|
|
295
|
-
return `[ERR-0002] Model "${model}" is not installed on the engine. If you picked a cloud model, use /model
|
|
295
|
+
return `[ERR-0002] Model "${model}" is not installed on the engine. If you picked a cloud model, use /model VTL-3.7-Ultra or /provider openai.`;
|
|
296
296
|
}
|
|
297
|
-
return `[ERR-0003] The engine returned HTTP ${resp.status}. Check the engine status or run /model
|
|
297
|
+
return `[ERR-0003] The engine returned HTTP ${resp.status}. Check the engine status or run /model VTL-2.7-Flash.`;
|
|
298
298
|
}
|
package/src/ui/branding.js
CHANGED
package/src/installer.js
DELETED
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
import { execSync, exec } from 'child_process';
|
|
2
|
-
import { existsSync, mkdirSync, symlinkSync, chmodSync, writeFileSync, lstatSync, readlinkSync, rmSync, realpathSync } from 'fs';
|
|
3
|
-
import { join, dirname, resolve } from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { homedir, platform } from 'os';
|
|
6
|
-
import { Catalog } from './catalog.js';
|
|
7
|
-
import { Config } from './config.js';
|
|
8
|
-
|
|
9
|
-
function runSilent(cmd) {
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
exec(cmd, { stdio: 'ignore' }, (err) => resolve(!err));
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isEngineRunning(host) {
|
|
16
|
-
return fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) })
|
|
17
|
-
.then((r) => r.ok)
|
|
18
|
-
.catch(() => false);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function isEngineInstalled() {
|
|
22
|
-
try {
|
|
23
|
-
execSync('which ollama 2>/dev/null', { stdio: 'ignore' });
|
|
24
|
-
return true;
|
|
25
|
-
} catch {}
|
|
26
|
-
const paths = ['/usr/local/bin/ollama', '/usr/bin/ollama'];
|
|
27
|
-
return paths.some((p) => existsSync(p));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function installEngine() {
|
|
31
|
-
await runSilent('curl -fsSL https://ollama.com/install.sh | sh');
|
|
32
|
-
return isEngineInstalled();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function pullModel(host, model) {
|
|
36
|
-
try {
|
|
37
|
-
const resp = await fetch(`${host}/api/pull`, {
|
|
38
|
-
method: 'POST',
|
|
39
|
-
headers: { 'Content-Type': 'application/json' },
|
|
40
|
-
body: JSON.stringify({ name: model, stream: false }),
|
|
41
|
-
signal: AbortSignal.timeout(600000),
|
|
42
|
-
});
|
|
43
|
-
return resp.ok;
|
|
44
|
-
} catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function getInstalledModels(host) {
|
|
50
|
-
try {
|
|
51
|
-
const resp = await fetch(`${host}/api/tags`);
|
|
52
|
-
if (!resp.ok) return [];
|
|
53
|
-
const data = await resp.json();
|
|
54
|
-
return (data.models || []).map((m) => m.name);
|
|
55
|
-
} catch {
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Scripts in the same model family share a base name before any variant
|
|
61
|
-
// suffix (e.g. qwen2.5-coder:1.5b is the sibling of qwen2.5:1.5b).
|
|
62
|
-
function familyOf(model) {
|
|
63
|
-
return model
|
|
64
|
-
.split(':')[0]
|
|
65
|
-
.toLowerCase()
|
|
66
|
-
.replace(/-(coder|instruct|reasoning|chat|base|tiny|nano)$/, '');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function sizeOf(model) {
|
|
70
|
-
const size = model.split(':')[1] || '';
|
|
71
|
-
const match = size.match(/(\d+(?:\.\d+)?)(m|b)/i);
|
|
72
|
-
if (!match) return Number.POSITIVE_INFINITY;
|
|
73
|
-
const value = parseFloat(match[1]);
|
|
74
|
-
return match[2].toLowerCase() === 'b' ? value * 1000 : value;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Find the already-installed model that best matches the requested one.
|
|
78
|
-
// Prefers an exact match, then any same-family sibling (closest size).
|
|
79
|
-
function chooseModel(installed, realModel) {
|
|
80
|
-
if (installed.includes(realModel)) {
|
|
81
|
-
return { model: realModel, substituted: false, reason: null };
|
|
82
|
-
}
|
|
83
|
-
const fam = familyOf(realModel);
|
|
84
|
-
let best = null;
|
|
85
|
-
let bestDist = Number.POSITIVE_INFINITY;
|
|
86
|
-
for (const m of installed) {
|
|
87
|
-
if (familyOf(m) !== fam) continue;
|
|
88
|
-
const dist = Math.abs(sizeOf(m) - sizeOf(realModel)) + 0.5;
|
|
89
|
-
if (dist < bestDist) {
|
|
90
|
-
bestDist = dist;
|
|
91
|
-
best = m;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (best) {
|
|
95
|
-
return {
|
|
96
|
-
model: best,
|
|
97
|
-
substituted: true,
|
|
98
|
-
reason: `"${realModel}" is not installed; using ${best} (same family)`,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
return { model: null, substituted: true, reason: `"${realModel}" is not installed` };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async function resolveModel(host, realModel) {
|
|
105
|
-
const installed = await getInstalledModels(host);
|
|
106
|
-
return chooseModel(installed, realModel);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Best local bin dir for a globally-callable command on this platform.
|
|
110
|
-
function localBinDir() {
|
|
111
|
-
if (process.env.XDG_BIN_HOME) return process.env.XDG_BIN_HOME;
|
|
112
|
-
if (process.env.HOME) return join(homedir(), '.local', 'bin');
|
|
113
|
-
return '/usr/local/bin';
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function isOnPath(dir) {
|
|
117
|
-
const sep = platform() === 'win32' ? ';' : ':';
|
|
118
|
-
let dirResolved;
|
|
119
|
-
try {
|
|
120
|
-
dirResolved = existsSync(dir) ? realpathSync(dir) : resolve(dir);
|
|
121
|
-
} catch {
|
|
122
|
-
dirResolved = resolve(dir);
|
|
123
|
-
}
|
|
124
|
-
return String(process.env.PATH || '')
|
|
125
|
-
.split(sep)
|
|
126
|
-
.some((entry) => {
|
|
127
|
-
entry = entry.trim();
|
|
128
|
-
if (!entry) return false;
|
|
129
|
-
try {
|
|
130
|
-
const e = existsSync(entry) ? realpathSync(entry) : resolve(entry);
|
|
131
|
-
return e === dirResolved;
|
|
132
|
-
} catch {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Prefer a bin dir that is already on PATH (and writable) so the installed
|
|
139
|
-
// command is callable immediately; fall back to the local bin dir.
|
|
140
|
-
function chooseBinDir() {
|
|
141
|
-
const local = localBinDir();
|
|
142
|
-
if (isOnPath(local)) return local;
|
|
143
|
-
for (const candidate of ['/usr/local/bin', '/usr/bin']) {
|
|
144
|
-
try {
|
|
145
|
-
mkdirSync(candidate, { recursive: true, mode: 0o755 });
|
|
146
|
-
const probe = join(candidate, `.vierrataleai-write-test-${process.pid}`);
|
|
147
|
-
writeFileSync(probe, '');
|
|
148
|
-
rmSync(probe, { force: true });
|
|
149
|
-
return candidate;
|
|
150
|
-
} catch {}
|
|
151
|
-
}
|
|
152
|
-
return local;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Create (or refresh) a launcher link for `name` pointing at the node bin.
|
|
156
|
-
function writeLauncher(dir, name, target) {
|
|
157
|
-
const launcherPath = join(dir, name);
|
|
158
|
-
if (platform() === 'win32') {
|
|
159
|
-
if (!existsSync(launcherPath)) {
|
|
160
|
-
writeFileSync(launcherPath, `@echo off\r\nnode "${target}" %*\r\n`);
|
|
161
|
-
}
|
|
162
|
-
return launcherPath;
|
|
163
|
-
}
|
|
164
|
-
// Prefer a real symlink; fall back to a small shim that calls the node bin.
|
|
165
|
-
let live = false;
|
|
166
|
-
try {
|
|
167
|
-
live = lstatSync(launcherPath).isSymbolicLink() && readlinkSync(launcherPath) === target;
|
|
168
|
-
} catch {}
|
|
169
|
-
if (!live) {
|
|
170
|
-
rmSync(launcherPath, { force: true });
|
|
171
|
-
try {
|
|
172
|
-
symlinkSync(target, launcherPath);
|
|
173
|
-
chmodSync(launcherPath, 0o755);
|
|
174
|
-
} catch {
|
|
175
|
-
writeFileSync(launcherPath, `#!/bin/sh\nnode "${target}" "$@"\n`);
|
|
176
|
-
chmodSync(launcherPath, 0o755);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return launcherPath;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function installAppCommand() {
|
|
183
|
-
try {
|
|
184
|
-
const dir = chooseBinDir();
|
|
185
|
-
mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
186
|
-
const defaultTarget = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'vierrataleai.js');
|
|
187
|
-
const target = process.argv[1] && existsSync(process.argv[1]) ? process.argv[1] : defaultTarget;
|
|
188
|
-
// Both spellings run the assistant.
|
|
189
|
-
const launcherPath = writeLauncher(dir, 'vierrataleai', target);
|
|
190
|
-
writeLauncher(dir, 'vierratale', target);
|
|
191
|
-
return launcherPath;
|
|
192
|
-
} catch {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export const Installer = {
|
|
198
|
-
async pullModel(host, model) {
|
|
199
|
-
return pullModel(host, model);
|
|
200
|
-
},
|
|
201
|
-
|
|
202
|
-
async getInstalledModels(host) {
|
|
203
|
-
return getInstalledModels(host);
|
|
204
|
-
},
|
|
205
|
-
|
|
206
|
-
async resolveModel(host, realModel) {
|
|
207
|
-
return resolveModel(host, realModel);
|
|
208
|
-
},
|
|
209
|
-
|
|
210
|
-
async firstInstalled(host) {
|
|
211
|
-
const installed = await getInstalledModels(host);
|
|
212
|
-
return installed[0] || null;
|
|
213
|
-
},
|
|
214
|
-
|
|
215
|
-
chooseModel,
|
|
216
|
-
|
|
217
|
-
installAppCommand,
|
|
218
|
-
isOnPath,
|
|
219
|
-
|
|
220
|
-
async ensure() {
|
|
221
|
-
const host = Config.get('engineHost');
|
|
222
|
-
|
|
223
|
-
if (!isEngineInstalled()) {
|
|
224
|
-
await installEngine();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const running = await isEngineRunning(host);
|
|
228
|
-
if (!running) {
|
|
229
|
-
try {
|
|
230
|
-
// No systemd available here, so launch the daemon directly, detached,
|
|
231
|
-
// and poll until it responds instead of relying on a service manager.
|
|
232
|
-
const child = exec('ollama serve', {
|
|
233
|
-
stdio: 'ignore',
|
|
234
|
-
detached: true,
|
|
235
|
-
});
|
|
236
|
-
if (child.unref) child.unref();
|
|
237
|
-
for (let i = 0; i < 30; i++) {
|
|
238
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
239
|
-
if (await isEngineRunning(host)) break;
|
|
240
|
-
}
|
|
241
|
-
} catch {}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const realModel = Catalog.getRealModel(Config.get('model'));
|
|
245
|
-
const installed = await getInstalledModels(host);
|
|
246
|
-
const resolved = chooseModel(installed, realModel);
|
|
247
|
-
const needsPull = resolved.model == null;
|
|
248
|
-
|
|
249
|
-
if (needsPull) {
|
|
250
|
-
await pullModel(host, realModel);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return true;
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
async isReady() {
|
|
257
|
-
const host = Config.get('engineHost');
|
|
258
|
-
return isEngineRunning(host);
|
|
259
|
-
},
|
|
260
|
-
|
|
261
|
-
// Full install: engine + model + a global "vierrataleai" command.
|
|
262
|
-
async install({ includeCommand = true } = {}) {
|
|
263
|
-
const host = Config.get('engineHost');
|
|
264
|
-
|
|
265
|
-
if (!isEngineInstalled()) {
|
|
266
|
-
await installEngine();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const running = await isEngineRunning(host);
|
|
270
|
-
if (!running) {
|
|
271
|
-
try {
|
|
272
|
-
const child = exec('ollama serve', { stdio: 'ignore', detached: true });
|
|
273
|
-
if (child.unref) child.unref();
|
|
274
|
-
for (let i = 0; i < 30; i++) {
|
|
275
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
276
|
-
if (await isEngineRunning(host)) break;
|
|
277
|
-
}
|
|
278
|
-
} catch {}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const realModel = Catalog.getRealModel(Config.get('model'));
|
|
282
|
-
const installed = await getInstalledModels(host);
|
|
283
|
-
const resolved = chooseModel(installed, realModel);
|
|
284
|
-
if (resolved.model == null) {
|
|
285
|
-
await pullModel(host, realModel);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
let command = null;
|
|
289
|
-
let commandAlias = null;
|
|
290
|
-
let commandOnPath = false;
|
|
291
|
-
if (includeCommand) {
|
|
292
|
-
command = installAppCommand();
|
|
293
|
-
if (command) {
|
|
294
|
-
commandAlias = join(dirname(command), 'vierratale');
|
|
295
|
-
commandOnPath = isOnPath(dirname(command));
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return { installed: await isEngineInstalled(), running: await isEngineRunning(host), command, commandAlias, commandOnPath };
|
|
299
|
-
},
|
|
300
|
-
};
|