@vierratale/ai 0.1.0-beta.13 → 0.1.0-beta.14

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 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.
@@ -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 { Installer } from '../src/installer.js';
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
- const result = await fakeProgress(INSTALL_STEPS, Installer.install());
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, Installer.ensure());
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.13",
3
+ "version": "0.1.0-beta.14",
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/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 { Installer } from './installer.js';
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';
@@ -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 Installer.getInstalledModels(Config.get('engineHost'))) {
814
+ for (const m of await Engine.getInstalledModels(Config.get('engineHost'))) {
815
815
  installedSet.add(m);
816
816
  }
817
817
  }
@@ -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 Installer.resolveModel(host, realModel);
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 Installer.pullModel(host, realModel);
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 Installer.firstInstalled(host);
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
- () => Installer.ensure()
1267
+ () => Engine.ensure()
1268
1268
  );
1269
1269
 
1270
1270
  await ensureLocalModel(Config.get('model'));
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 { Installer } from './installer.js';
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';
@@ -1,6 +1,6 @@
1
1
  export const Branding = {
2
2
  APP_NAME: 'VierrataleAI',
3
- VERSION: '0.1.0-beta.12',
3
+ VERSION: '0.1.0-beta.14',
4
4
 
5
5
  colors: {
6
6
  primary: '\x1b[38;2;124;58;237m',
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
- };