@vierratale/ai 0.1.0-beta.15 → 0.1.0-beta.16

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/src/config.js CHANGED
@@ -1,34 +1,24 @@
1
1
  import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
- import { Catalog } from './catalog.js';
5
4
 
6
- function configPaths() {
7
- const dir = process.env.VIERRATALE_CONFIG_DIR
8
- ? join(process.env.VIERRATALE_CONFIG_DIR)
9
- : join(homedir(), '.config', 'vierrataleai');
10
- return { dir, file: join(dir, 'config.json') };
11
- }
5
+ const CONFIG_DIR = join(homedir(), '.config', 'vierrataleai');
6
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
12
7
 
13
8
  const DEFAULTS = {
14
9
  provider: 'auto',
15
- model: 'VTL-2.7-Flash',
10
+ model: 'vierratale-fast',
16
11
  engineHost: 'http://127.0.0.1:11434',
17
12
  numCtx: 2048,
18
13
  temperature: 0.7,
19
14
  maxTokens: 4096,
20
15
  keepAlive: '5m',
21
- numThreads: 8,
22
- commandTimeoutMs: 60000,
23
- planTimeoutMs: 45000,
24
- downloadDir: join(homedir(), 'Downloads'),
25
16
  };
26
17
 
27
18
  function loadConfigFile() {
28
19
  try {
29
- const { file } = configPaths();
30
- if (existsSync(file)) {
31
- const raw = readFileSync(file, 'utf-8');
20
+ if (existsSync(CONFIG_FILE)) {
21
+ const raw = readFileSync(CONFIG_FILE, 'utf-8');
32
22
  return JSON.parse(raw);
33
23
  }
34
24
  } catch {}
@@ -54,15 +44,11 @@ export const Config = {
54
44
  temperature: parseFloat(getEnv('VIERRATALE_TEMPERATURE', String(file.temperature ?? DEFAULTS.temperature))),
55
45
  maxTokens: parseInt(getEnv('VIERRATALE_MAX_TOKENS', String(file.maxTokens ?? DEFAULTS.maxTokens))),
56
46
  keepAlive: getEnv('VIERRATALE_KEEP_ALIVE', file.keepAlive ?? DEFAULTS.keepAlive),
57
- numThreads: parseInt(getEnv('VIERRATALE_NUM_THREADS', String(file.numThreads ?? DEFAULTS.numThreads))),
58
47
  openaiApiKey: getEnv('OPENAI_API_KEY', file.openaiApiKey || ''),
59
48
  anthropicApiKey: getEnv('ANTHROPIC_API_KEY', file.anthropicApiKey || ''),
60
49
  geminiApiKey: getEnv('GEMINI_API_KEY', file.geminiApiKey || ''),
61
50
  systemPrompt: file.systemPrompt || '',
62
51
  providerModels: file.providerModels || {},
63
- downloadDir: getEnv('VIERRATALE_DOWNLOAD_DIR', file.downloadDir ?? DEFAULTS.downloadDir),
64
- commandTimeoutMs: parseInt(getEnv('VIERRATALE_COMMAND_TIMEOUT_MS', String(file.commandTimeoutMs ?? DEFAULTS.commandTimeoutMs))),
65
- planTimeoutMs: parseInt(getEnv('VIERRATALE_PLAN_TIMEOUT_MS', String(file.planTimeoutMs ?? DEFAULTS.planTimeoutMs))),
66
52
  };
67
53
  return this._config;
68
54
  },
@@ -90,22 +76,20 @@ export const Config = {
90
76
  },
91
77
 
92
78
  getEffectiveModel(provider) {
93
- const raw = this.getProviderModel(provider) || this.get('model');
94
- return Catalog.normalize(raw);
79
+ return this.getProviderModel(provider) || this.get('model');
95
80
  },
96
81
 
97
82
  save(overrides = {}) {
98
- const { dir, file } = configPaths();
99
- if (!existsSync(dir)) {
100
- mkdirSync(dir, { recursive: true });
83
+ if (!existsSync(CONFIG_DIR)) {
84
+ mkdirSync(CONFIG_DIR, { recursive: true });
101
85
  }
102
86
  const current = this._config || DEFAULTS;
103
87
  const merged = { ...current, ...overrides };
104
- writeFileSync(file, JSON.stringify(merged, null, 2));
88
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
105
89
  this._config = merged;
106
90
  },
107
91
 
108
92
  getConfigDir() {
109
- return configPaths().dir;
93
+ return CONFIG_DIR;
110
94
  },
111
95
  };
package/src/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  export { Catalog } from './catalog.js';
2
2
  export { Config } from './config.js';
3
- export { Engine } from './engine.js';
3
+ export { Installer } from './installer.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';
7
- export { FileWriter } from './utils/filewriter.js';
@@ -0,0 +1,105 @@
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 pullModel(host, model) {
33
+ try {
34
+ const resp = await fetch(`${host}/api/pull`, {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify({ name: model, stream: false }),
38
+ signal: AbortSignal.timeout(600000),
39
+ });
40
+ return resp.ok;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ async function getInstalledModels(host) {
47
+ try {
48
+ const resp = await fetch(`${host}/api/tags`);
49
+ if (!resp.ok) return [];
50
+ const data = await resp.json();
51
+ return (data.models || []).map((m) => m.name);
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
56
+
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
+
66
+ async ensure() {
67
+ const host = Config.get('engineHost');
68
+
69
+ if (!isEngineInstalled()) {
70
+ await installEngine();
71
+ }
72
+
73
+ const running = await isEngineRunning(host);
74
+ if (!running) {
75
+ try {
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
+ }
87
+ } catch {}
88
+ }
89
+
90
+ const realModel = Catalog.getRealModel(Config.get('model'));
91
+ const installed = await getInstalledModels(host);
92
+ const needsPull = !installed.includes(realModel);
93
+
94
+ if (needsPull) {
95
+ await pullModel(host, realModel);
96
+ }
97
+
98
+ return true;
99
+ },
100
+
101
+ async isReady() {
102
+ const host = Config.get('engineHost');
103
+ return isEngineRunning(host);
104
+ },
105
+ };
@@ -1,3 +1,3 @@
1
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. You CAN write files and folders directly to the user's disk. When the user asks you to create, save, or generate code, configs, scripts, projects, web pages, or any other files, DO NOT refuse — answer with the FILE:/FOLDER: format instead. Use this exact format for EACH file: a line \"FILE: relative/path/to/file\" on its own line, immediately followed by a markdown code fence containing the file content. This works for any file type, including JSON config files and HTML pages: write a config as \"FILE: config.json\" followed by a ```json fence, and a web page as \"FILE: index.html\" followed by a ```html fence with the full <!doctype html>... markup inside. Use paths relative to the user's working directory. Emit one FILE: block per file; nested folders are created automatically. If you need an empty folder, emit \"FOLDER: relative/path\" on its own line. When asked for a story, folk tale, legend, or myth, give the real, well-known version using the web search results you were given when available, and also save the full story to a .txt file using FILE: <title>.txt followed by a ```text fence. Keep any explanation short and outside of the FILE:/FOLDER: blocks."
3
- }
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
+ }
@@ -13,45 +13,3 @@ You are **VierrataleAI**, an intelligent AI assistant created by Vierratale.
13
13
  - Help users with tasks including coding, writing, analysis, and general knowledge.
14
14
  - If you don't know something, say so honestly.
15
15
  - Use markdown formatting when appropriate for readability.
16
-
17
- ## Writing Files and Folders
18
-
19
- You CAN write files and folders directly to the user's disk. When the user asks
20
- you to create, save, or generate code, configs, scripts, projects, or any other
21
- files, DO NOT refuse — answer with the format below. Use this exact format for
22
- EACH file:
23
-
24
- FILE: relative/path/to/file.js
25
- ```js
26
- // file content here
27
- ```
28
-
29
- This works for any file type, including JSON and HTML. For example, write a
30
- config file exactly like this:
31
-
32
- FILE: config.json
33
- ```json
34
- {
35
- "name": "my-app",
36
- "port": 3000
37
- }
38
- ```
39
-
40
- And a web page like this:
41
-
42
- FILE: index.html
43
- ```html
44
- <!doctype html>
45
- <html>
46
- <head><title>My Page</title></head>
47
- <body><h1>Hello</h1></body>
48
- </html>
49
- ```
50
-
51
- Rules:
52
- - Put `FILE: <path>` on its own line, immediately followed by a markdown code fence containing the file content. No text between the header and the fence.
53
- - Use paths relative to the user's working directory (e.g. `src/app.js`, `config.json`).
54
- - Emit one `FILE:` block per file; nested folders are created automatically.
55
- - If you need an empty folder, emit `FOLDER: <path>` on its own line.
56
- - When asked for a story, folk tale, legend, or myth, give the real, well-known version — use the web search results you were given when available — and also save the full story to a .txt file using `FILE: <title>.txt` followed by a ```text fence.
57
- - Keep any explanation short and outside of the `FILE:`/`FOLDER:` blocks.
@@ -39,7 +39,7 @@ export class AnthropicProvider extends BaseProvider {
39
39
 
40
40
  async *stream(messages, options = {}) {
41
41
  const key = Config.get('anthropicApiKey');
42
- if (!key) throw new Error('[ERR-0004] Cloud API key not configured. Add it to the config and try again.');
42
+ if (!key) throw new Error('Anthropic API key not configured');
43
43
 
44
44
  const model = this._resolveModel(options);
45
45
  const systemPrompt = options.systemPrompt || '';
@@ -51,80 +51,49 @@ export class AnthropicProvider extends BaseProvider {
51
51
  content: m.content,
52
52
  }));
53
53
 
54
- const controller = new AbortController();
55
- const connectTimer = setTimeout(() => controller.abort(), 240000);
56
-
57
- let resp;
58
- try {
59
- resp = await fetch(ANTHROPIC_URL, {
60
- method: 'POST',
61
- headers: {
62
- 'Content-Type': 'application/json',
63
- 'x-api-key': key,
64
- 'anthropic-version': '2023-06-01',
65
- },
66
- body: JSON.stringify({
67
- model,
68
- system: system.length ? system : undefined,
69
- messages: apiMessages,
70
- max_tokens: options.maxTokens || Config.get('maxTokens'),
71
- stream: true,
72
- temperature: options.temperature || Config.get('temperature'),
73
- }),
74
- signal: controller.signal,
75
- });
76
- } catch (err) {
77
- if (err.name === 'AbortError') {
78
- throw new Error('[ERR-0001] Could not reach the API (no response within 60s). Check your network connection.');
79
- }
80
- throw new Error('[ERR-0001] Could not reach the API (no response). Check your network connection.');
81
- } finally {
82
- clearTimeout(connectTimer);
83
- }
54
+ const resp = await fetch(ANTHROPIC_URL, {
55
+ method: 'POST',
56
+ headers: {
57
+ 'Content-Type': 'application/json',
58
+ 'x-api-key': key,
59
+ 'anthropic-version': '2023-06-01',
60
+ },
61
+ body: JSON.stringify({
62
+ model,
63
+ system: system.length ? system : undefined,
64
+ messages: apiMessages,
65
+ max_tokens: options.maxTokens || Config.get('maxTokens'),
66
+ stream: true,
67
+ temperature: options.temperature || Config.get('temperature'),
68
+ }),
69
+ });
84
70
 
85
71
  if (!resp.ok) {
86
- throw new Error('[ERR-0003] The cloud provider returned HTTP ' + resp.status + '. Check your API key in config.');
72
+ throw new Error('Failed to connect to cloud engine');
87
73
  }
88
74
 
89
75
  const reader = resp.body.getReader();
90
76
  const decoder = new TextDecoder();
91
77
  let buffer = '';
92
- let idle = null;
93
- const settle = () => {
94
- if (idle) clearTimeout(idle);
95
- idle = setTimeout(() => controller.abort(), 240000);
96
- };
97
- settle();
98
- try {
99
- while (true) {
100
- let value, done;
78
+
79
+ while (true) {
80
+ const { done, value } = await reader.read();
81
+ if (done) break;
82
+
83
+ buffer += decoder.decode(value, { stream: true });
84
+ const lines = buffer.split('\n');
85
+ buffer = lines.pop() || '';
86
+
87
+ for (const line of lines) {
88
+ if (!line.startsWith('data: ')) continue;
89
+ const data = line.slice(6);
101
90
  try {
102
- ({ done, value } = await reader.read());
103
- } catch (err) {
104
- if (err.name === 'AbortError') {
105
- throw new Error('[ERR-0001] Lost the connection to the cloud API (no data for 60s).');
91
+ const json = JSON.parse(data);
92
+ if (json.type === 'content_block_delta' && json.delta?.text) {
93
+ yield json.delta.text;
106
94
  }
107
- throw err;
108
- }
109
- if (done) break;
110
- settle();
111
- buffer += decoder.decode(value, { stream: true });
112
- const lines = buffer.split('\n');
113
- buffer = lines.pop() || '';
114
-
115
- for (const line of lines) {
116
- if (!line.startsWith('data: ')) continue;
117
- const data = line.slice(6);
118
- try {
119
- const json = JSON.parse(data);
120
- if (json.type === 'content_block_delta' && json.delta?.text) {
121
- yield json.delta.text;
122
- }
123
- } catch {}
124
- }
95
+ } catch {}
125
96
  }
126
- } finally {
127
- if (idle) clearTimeout(idle);
128
97
  }
129
98
  }
130
99
  }
@@ -15,16 +15,6 @@ export class BaseProvider {
15
15
  throw new Error('stream() must be implemented');
16
16
  }
17
17
 
18
- // Non-streaming convenience: collects the stream. Providers may override
19
- // with a proper single-shot request (see CortexProvider).
20
- async complete(messages, options = {}) {
21
- let out = '';
22
- for await (const chunk of this.stream(messages, options)) {
23
- out += chunk;
24
- }
25
- return out;
26
- }
27
-
28
18
  async listModels() {
29
19
  throw new Error('listModels() must be implemented');
30
20
  }