@vierratale/ai 0.1.0-beta.7 → 0.1.0-beta.9
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 +120 -16
- package/bin/vierrataleai.js +74 -1
- package/package.json +7 -1
- package/src/catalog.js +78 -49
- package/src/cli.js +886 -91
- package/src/cmd/agent.js +1695 -0
- package/src/cmd/executor.js +257 -0
- package/src/cmd/tools.js +131 -0
- package/src/config.js +24 -10
- package/src/index.js +1 -0
- package/src/installer.js +149 -6
- package/src/prompts/system.json +2 -2
- package/src/prompts/system.md +42 -0
- package/src/providers/anthropic.js +65 -34
- package/src/providers/base.js +10 -0
- package/src/providers/cortex.js +236 -36
- package/src/providers/gemini.js +62 -31
- package/src/providers/openai.js +64 -33
- package/src/session.js +195 -15
- package/src/ui/branding.js +1 -1
- package/src/ui/chatbox.js +271 -0
- package/src/ui/terminal.js +49 -16
- package/src/utils/filewriter.js +97 -0
- package/src/utils/intents.js +73 -1
- package/src/utils/logger.js +67 -0
- package/src/utils/math.js +142 -0
- package/build.sh +0 -17
package/src/cli.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { createInterface } from 'readline';
|
|
2
|
+
import { execFile } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
2
4
|
import { Config } from './config.js';
|
|
3
5
|
import { Catalog } from './catalog.js';
|
|
4
6
|
import { Installer } from './installer.js';
|
|
5
7
|
import { ProviderFactory } from './providers/index.js';
|
|
6
8
|
import { Branding } from './ui/branding.js';
|
|
7
9
|
import { Terminal } from './ui/terminal.js';
|
|
8
|
-
import { loadSystemPrompt
|
|
10
|
+
import { loadSystemPrompt } from './ui/banner.js';
|
|
9
11
|
import { WebSearch } from './utils/websearch.js';
|
|
10
12
|
import { WebFetch } from './utils/webfetch.js';
|
|
11
|
-
import {
|
|
13
|
+
import { FileWriter } from './utils/filewriter.js';
|
|
14
|
+
import { detectIntent, extractSearchTopic, looksLikeStoryRequest } from './utils/intents.js';
|
|
15
|
+
import { trySolveMath } from './utils/math.js';
|
|
12
16
|
import { Session } from './session.js';
|
|
17
|
+
import { ChatUI, FrameThrottle } from './ui/chatbox.js';
|
|
18
|
+
import { logger } from './utils/logger.js';
|
|
19
|
+
import { runWithTools, TOOL_RESULTS_PROMPT, looksLikeOperationRequest } from './cmd/agent.js';
|
|
13
20
|
|
|
14
21
|
function parseArgs(args) {
|
|
15
|
-
const parsed = { provider: null, model: null, clear: false, version: false, help: false };
|
|
22
|
+
const parsed = { provider: null, model: null, clear: false, version: false, help: false, install: false };
|
|
16
23
|
for (let i = 2; i < args.length; i++) {
|
|
17
24
|
const arg = args[i];
|
|
18
25
|
if (arg === '--provider' || arg === '-p') parsed.provider = args[++i];
|
|
@@ -20,6 +27,7 @@ function parseArgs(args) {
|
|
|
20
27
|
else if (arg === '--clear') parsed.clear = true;
|
|
21
28
|
else if (arg === '--version' || arg === '-v') parsed.version = true;
|
|
22
29
|
else if (arg === '--help' || arg === '-h') parsed.help = true;
|
|
30
|
+
else if (arg === '--install' || arg === '--setup' || arg === 'install') parsed.install = true;
|
|
23
31
|
}
|
|
24
32
|
return parsed;
|
|
25
33
|
}
|
|
@@ -31,8 +39,9 @@ Usage: vierrataleai [options]
|
|
|
31
39
|
|
|
32
40
|
Options:
|
|
33
41
|
--provider, -p <name> Provider: cortex, openai (default: auto-detect)
|
|
34
|
-
--model, -m <name> Model:
|
|
35
|
-
--clear
|
|
42
|
+
--model, -m <name> Model: VRTL-1.lite/2.fast/4.balanced/6.pro/8.cloud
|
|
43
|
+
--clear (deprecated: screen is always cleared on start)
|
|
44
|
+
--install, --setup Install engine + model + a "vierrataleai" command, then run
|
|
36
45
|
--version, -v Show version
|
|
37
46
|
--help, -h Show this help
|
|
38
47
|
|
|
@@ -43,50 +52,359 @@ Commands (in chat):
|
|
|
43
52
|
/models List available models
|
|
44
53
|
/search <query> Search the web and summarize with AI
|
|
45
54
|
/fetch <url> Open a link and summarize its content
|
|
55
|
+
/log [n|path] Show the last n log entries (default 30), or the log file path
|
|
46
56
|
/clear Clear screen
|
|
47
|
-
/new Start a new
|
|
57
|
+
/new Start a new empty session (keeps old ones)
|
|
58
|
+
/sessions List saved sessions
|
|
59
|
+
/session new [name] Create and switch to a session
|
|
60
|
+
/session open <name|#> Resume a session (e.g. /session open #2)
|
|
61
|
+
/session delete <name|#> Delete a session
|
|
62
|
+
/session backup [name|#] Back up a session to a .bak.json (auto on fresh start)
|
|
63
|
+
/session backups List session backups
|
|
64
|
+
/session restore <n|#> Restore a backup into a session
|
|
48
65
|
/quit Exit
|
|
49
66
|
`);
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
const FILE_VERBS =
|
|
70
|
+
'write|save|make|create|generate|build|code|design|implement|produce|add|store|put|give me|show me|make me|help me|need|want|have';
|
|
71
|
+
const FILE_NOUNS =
|
|
72
|
+
'file|folder|directory|txt|json|md|markdown|text file|document|html|css|js|javascript|script|page|webpage|website|code|project|config|app|application|api|bot|game|tool|crawler|scraper';
|
|
73
|
+
const KNOWLEDGE_MARKERS =
|
|
74
|
+
/\b(how|why|what is|what are|what's|whats|explain|learn|teach me|meaning|means|used for|tutorial|why is|what does|is it|does it)\b/i;
|
|
75
|
+
const CONCRETE_FILE_ASK = new RegExp(
|
|
76
|
+
`\\b(?:create|make|write|save|generate|build|produce)\\b[^.!?\\n]{0,60}\\b(?:${FILE_NOUNS})\\b`,
|
|
77
|
+
'i'
|
|
78
|
+
);
|
|
79
|
+
const FILE_ASK_STRONG = new RegExp(
|
|
80
|
+
`\\b(?:${FILE_VERBS})\\b[^.!?]{0,80}\\b(?:${FILE_NOUNS})\\b`,
|
|
81
|
+
'i'
|
|
82
|
+
);
|
|
83
|
+
const FILE_ASK_SENTENCE = new RegExp(
|
|
84
|
+
`\\b(?:${FILE_VERBS})\\b[^.!?\\n]{0,160}\\b(?:${FILE_NOUNS})\\b`,
|
|
85
|
+
'i'
|
|
86
|
+
);
|
|
55
87
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
// Attached as a hidden user message to file requests so small local models
|
|
89
|
+
// reliably emit complete FILE: blocks with closed fences instead of free-style
|
|
90
|
+
// prose that gets truncated or never closes its code fences.
|
|
91
|
+
const FILE_BLOCK_PROMPT = `If the user asked you to create a file or project with code, output your whole answer as one or more file blocks — nothing else:
|
|
92
|
+
|
|
93
|
+
FILE: path/to/file.ext
|
|
94
|
+
${'```'}language
|
|
95
|
+
<complete, working code>
|
|
96
|
+
${'```'}
|
|
97
|
+
|
|
98
|
+
Rules:
|
|
99
|
+
- One FILE: header at the start of each file, immediately followed by its fenced code block.
|
|
100
|
+
- Always close every fence with ${'```'}.
|
|
101
|
+
- Write real, complete code on the first try — no placeholders, no "[0]", no Lorem ipsum, no "This is a basic example" filler, no fabricated external script URLs.
|
|
102
|
+
- If the request mentions serving or routing the files (e.g. a python web server, flask, routes), also write the server file (like app.py) that serves them.`;
|
|
103
|
+
|
|
104
|
+
function looksLikeFileRequest(text) {
|
|
105
|
+
const s = String(text || '');
|
|
106
|
+
if (KNOWLEDGE_MARKERS.test(s) && !CONCRETE_FILE_ASK.test(s)) return false;
|
|
107
|
+
if (FILE_ASK_STRONG.test(s)) return true;
|
|
108
|
+
return FILE_ASK_SENTENCE.test(s);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const CODE_FILE_RE = /([\w.-]+\.(?:html?|css|json|jsx|tsx|mjs|cjs|ts|js|md|markdown|txt|py|php|rb|go|java|c|cpp|h|sh|bat|yml|yaml|xml|sql|svg|csv|env))/i;
|
|
112
|
+
|
|
113
|
+
function requestedFileName(text) {
|
|
114
|
+
const m = String(text || '').match(CODE_FILE_RE);
|
|
115
|
+
if (m) return m[1];
|
|
116
|
+
const lower = String(text || '').toLowerCase();
|
|
117
|
+
if (/\b(python|py)\b/.test(lower) && /\b(html?|css|javascript|js|website|webpage|web|route)\b/.test(lower)) return 'app.py';
|
|
118
|
+
if (/\b(python|py)\b/.test(lower)) return 'script.py';
|
|
119
|
+
if (/\b(flask|django|fastapi|streamlit)\b/.test(lower)) return 'app.py';
|
|
120
|
+
if (/\b(bash|zsh|shell)\b/.test(lower)) return 'script.sh';
|
|
121
|
+
if (/\b(javascript|js|script)\b/.test(lower)) return 'script.js';
|
|
122
|
+
if (/\b(typescript|ts)\b/.test(lower)) return 'script.ts';
|
|
123
|
+
if (/\bjson\b/.test(lower)) return 'output.json';
|
|
124
|
+
if (/\b(markdown|md)\b/.test(lower)) return 'output.md';
|
|
125
|
+
if (/\b(html?|webpage|website|page)\b/.test(lower)) return 'index.html';
|
|
126
|
+
if (/\bcss\b/.test(lower)) return 'style.css';
|
|
127
|
+
if (/\bapp\b/.test(lower)) return 'app.py';
|
|
128
|
+
if (/\b(yaml|yml)\b/.test(lower)) return 'output.yml';
|
|
129
|
+
if (/\bcsv\b/.test(lower)) return 'output.csv';
|
|
130
|
+
if (/\bsql\b/.test(lower)) return 'output.sql';
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function extractFallbackFile(response, requestText) {
|
|
135
|
+
const lines = String(response || '').split('\n');
|
|
136
|
+
const headerIdx = lines.findIndex((l) => /^FILE:\s*.+$/i.test(l));
|
|
137
|
+
if (headerIdx !== -1) {
|
|
138
|
+
const path = lines[headerIdx].replace(/^FILE:\s*/i, '').trim();
|
|
139
|
+
const content = lines
|
|
140
|
+
.slice(headerIdx + 1)
|
|
141
|
+
.map((l) => (/^```/.test(l.trim()) ? '' : l))
|
|
142
|
+
.join('\n')
|
|
143
|
+
.trim();
|
|
144
|
+
return { path, content };
|
|
59
145
|
}
|
|
146
|
+
const fenced = String(response || '').match(/```([\w./+-]*)[^\n]*\n?([\s\S]*?)(?:```|$)/);
|
|
147
|
+
const content = fenced ? fenced[2].trim() : String(response || '').replace(/^FILE:\s*\S+\s*$/gim, '').trim();
|
|
148
|
+
const path = requestedFileName(requestText) || languageFileName(fenced ? fenced[1] : null) || storyFileName(requestText);
|
|
149
|
+
return { path, content };
|
|
150
|
+
}
|
|
60
151
|
|
|
61
|
-
|
|
152
|
+
function storyFileName(text) {
|
|
153
|
+
const topic = extractSearchTopic(text);
|
|
154
|
+
let raw = (topic || 'story')
|
|
155
|
+
.toLowerCase()
|
|
156
|
+
.replace(/\s+and\s+(?:write|save|make|create|generate|build|give|put)\b.*$/i, '')
|
|
157
|
+
.replace(/\s+(?:in|into|to)\s+.*?(?:file|txt|json|md|csv)\b.*$/i, '')
|
|
158
|
+
.replace(/\s+for\s+me\s*$/i, '')
|
|
159
|
+
.trim();
|
|
160
|
+
const slug = raw.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
161
|
+
return `${slug || 'story'}.txt`;
|
|
162
|
+
}
|
|
62
163
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
164
|
+
function responseWantsFile(response) {
|
|
165
|
+
return /^(?:FILE|FOLDER):\s*\S/im.test(String(response || ''));
|
|
166
|
+
}
|
|
66
167
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
168
|
+
function languageFileName(lang) {
|
|
169
|
+
const map = {
|
|
170
|
+
html: 'index.html', htm: 'index.html', xml: 'index.html',
|
|
171
|
+
css: 'style.css', js: 'script.js', javascript: 'script.js',
|
|
172
|
+
ts: 'script.ts', json: 'output.json', md: 'output.md',
|
|
173
|
+
markdown: 'output.md', py: 'script.py', python: 'script.py',
|
|
174
|
+
txt: 'output.txt', sh: 'script.sh', bash: 'script.sh',
|
|
175
|
+
yaml: 'output.yml', yml: 'output.yml', csv: 'output.csv',
|
|
176
|
+
svg: 'output.svg', sql: 'output.sql', go: 'main.go',
|
|
177
|
+
java: 'Main.java', rb: 'script.rb', php: 'script.php',
|
|
178
|
+
c: 'main.c', cpp: 'main.cpp', h: 'main.h',
|
|
179
|
+
};
|
|
180
|
+
return map[String(lang || '').toLowerCase()] || null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// True when the reply is basically "just code": one or more substantial
|
|
184
|
+
// fenced code blocks and very little surrounding prose.
|
|
185
|
+
function responseIsSoloCode(response) {
|
|
186
|
+
const text = String(response || '');
|
|
187
|
+
const blocks = [...text.matchAll(/```[\w./+-]*[^\n]*\n?([\s\S]*?)(?:```|$)/g)];
|
|
188
|
+
if (!blocks.length) return false;
|
|
189
|
+
const totalCode = blocks.reduce((n, b) => n + b[1].length, 0);
|
|
190
|
+
const prose = text.replace(/```[\s\S]*?(?:```|$)/g, ' ').replace(/\s+/g, ' ').trim();
|
|
191
|
+
return totalCode >= 50 && prose.length <= 60;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Prose-only replies are fine for story requests (.txt), but a file/code
|
|
195
|
+
// request with no fenced block and no FILE: header must not be saved as code.
|
|
196
|
+
function fallbackHasUsefulCode(response, requestText) {
|
|
197
|
+
const hasCode = /```[\w./+-]*/m.test(String(response || '')) || responseWantsFile(response);
|
|
198
|
+
return looksLikeStoryRequest(requestText) || hasCode;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// After a .py file is saved, compile-check it so truncated or broken replies
|
|
202
|
+
// never sit on disk silently. Returns a promise so callers can await the check
|
|
203
|
+
// before surfacing results (the test previously raced a fire-and-forget exec).
|
|
204
|
+
async function validateWrittenCode(fileName, notify) {
|
|
205
|
+
if (!/\.py$/i.test(fileName)) return;
|
|
206
|
+
try {
|
|
207
|
+
await promisify(execFile)(
|
|
208
|
+
'python3',
|
|
209
|
+
['-c', 'import sys; compile(open(sys.argv[1]).read(), sys.argv[1], "exec")', fileName]
|
|
210
|
+
);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const msg = `Saved ${fileName}, but Python couldn't compile it (the reply may have been cut short).`;
|
|
213
|
+
notify(msg);
|
|
214
|
+
logger.log('ERROR', `invalid python saved to ${fileName}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Small local models often emit recognizable placeholder/sample scaffolding
|
|
219
|
+
// (token artifacts like "[0]", fabricated external script URLs, "this is a
|
|
220
|
+
// basic example" text) that compiles but is not real, useful code. Detect
|
|
221
|
+
// the telltale signs so the user is warned instead of silently saving junk.
|
|
222
|
+
function looksLikeJunkCode(content, fileName) {
|
|
223
|
+
const text = String(content || '');
|
|
224
|
+
return /(code\.google\.com|chrome-extension|Lorem ipsum|placeholder text|\b[0-9]+\b\s*#\s*(This is|Python)|\bnot real code\b|\bcopy[-\s]?paste\b)/i.test(text);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Runs the junk check after a successful write and warns + logs if flagged.
|
|
228
|
+
function warnIfJunkCode(fileName, content, notify) {
|
|
229
|
+
if (!looksLikeJunkCode(content, fileName)) return;
|
|
230
|
+
const msg = `Heads up: ${fileName} looks like placeholder/sample content (the local model may be too weak for real code). Check it, or try importing from a stronger engine or model.`;
|
|
231
|
+
notify(msg);
|
|
232
|
+
logger.log('WARN', `possible junk content saved to ${fileName}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Like FileWriter.parse, but tolerant of an unclosed trailing fence (the
|
|
236
|
+
// model often truncates) so truncated FILE: blocks can still be rescued.
|
|
237
|
+
function extractFileBlocks(response) {
|
|
238
|
+
const blocks = [];
|
|
239
|
+
const lines = String(response || '').split('\n');
|
|
240
|
+
const n = lines.length;
|
|
241
|
+
let i = 0;
|
|
242
|
+
while (i < n) {
|
|
243
|
+
const m = /^FILE:\s*(.+)\s*$/i.exec(lines[i]);
|
|
244
|
+
if (m) {
|
|
245
|
+
let j = i + 1;
|
|
246
|
+
while (j < n && lines[j].trim() === '') j++;
|
|
247
|
+
const fence = /^```([\w./+-]*)/.exec(lines[j] || '');
|
|
248
|
+
if (fence) {
|
|
249
|
+
const content = [];
|
|
250
|
+
let k = j + 1;
|
|
251
|
+
while (k < n && !/^```/.test(lines[k])) {
|
|
252
|
+
content.push(lines[k]);
|
|
253
|
+
k++;
|
|
254
|
+
}
|
|
255
|
+
blocks.push({ path: m[1].trim(), language: fence[1], content: content.join('\n').trim() });
|
|
256
|
+
i = Math.max(k, j + 1);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
i++;
|
|
261
|
+
}
|
|
262
|
+
return blocks;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function writeFilesFromResponse(response, confirm, requestText, notify) {
|
|
266
|
+
const { files, folders } = FileWriter.parse(response);
|
|
267
|
+
const contentByPath = new Map(files.map((f) => [f.path, f.content]));
|
|
268
|
+
const results = await FileWriter.write({ files, folders, overwrite: false, confirm });
|
|
269
|
+
let wroteAny = false;
|
|
270
|
+
for (const r of results) {
|
|
271
|
+
const label = r.kind === 'folder' ? 'folder' : 'file';
|
|
272
|
+
if (r.status === 'written' || r.status === 'created') {
|
|
273
|
+
wroteAny = true;
|
|
274
|
+
notify(`Wrote ${label}: ${r.path}`);
|
|
275
|
+
logger.log('FILE', `wrote ${label} ${r.path}${r.target ? ` (${r.target})` : ''}`);
|
|
276
|
+
await validateWrittenCode(r.path, notify);
|
|
277
|
+
warnIfJunkCode(r.path, contentByPath.get(r.path) || '', notify);
|
|
278
|
+
} else if (r.status === 'skipped') {
|
|
279
|
+
notify(`Skipped ${label}: ${r.path} (${r.error})`);
|
|
280
|
+
logger.log('FILE', `skipped ${label} ${r.path}${r.target ? ` (${r.target})` : ''}: ${r.error}`);
|
|
281
|
+
} else {
|
|
282
|
+
notify(`Failed to write ${label}: ${r.path} (${r.error})`);
|
|
283
|
+
logger.log('ERROR', `failed writing ${label} ${r.path}: ${r.error}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Rescue FILE: blocks the parser dropped because their trailing fence was
|
|
288
|
+
// truncated (the model truncates often); otherwise data is lost silently.
|
|
289
|
+
const handledPaths = new Set(results.filter((x) => x.kind === 'file').map((x) => x.path));
|
|
290
|
+
for (const b of extractFileBlocks(response)) {
|
|
291
|
+
if (handledPaths.has(b.path) || !b.content.trim()) continue;
|
|
292
|
+
const rescue = await FileWriter.write({
|
|
293
|
+
files: [{ path: b.path, content: b.content }],
|
|
294
|
+
overwrite: false,
|
|
295
|
+
confirm,
|
|
296
|
+
});
|
|
297
|
+
const r0 = rescue[0];
|
|
298
|
+
if (r0.status === 'written') {
|
|
299
|
+
wroteAny = true;
|
|
300
|
+
notify(`Wrote file: ${b.path} (recovered from truncated reply)`);
|
|
301
|
+
logger.log('FILE', `wrote file ${b.path} (rescued truncated block)${r0.target ? ` at ${r0.target}` : ''}`);
|
|
302
|
+
await validateWrittenCode(b.path, notify);
|
|
303
|
+
warnIfJunkCode(b.path, b.content, notify);
|
|
304
|
+
} else if (r0.status === 'skipped') {
|
|
305
|
+
notify(`Skipped file: ${b.path} (${r0.error})`);
|
|
306
|
+
} else {
|
|
307
|
+
notify(`Failed to write file: ${b.path} (${r0.error})`);
|
|
308
|
+
logger.log('ERROR', `failed writing file ${b.path}: ${r0.error}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!wroteAny && response && (
|
|
313
|
+
responseWantsFile(response) ||
|
|
314
|
+
responseIsSoloCode(response) ||
|
|
315
|
+
looksLikeFileRequest(requestText) ||
|
|
316
|
+
looksLikeStoryRequest(requestText)
|
|
317
|
+
)) {
|
|
318
|
+
const { path: name, content } = extractFallbackFile(response, requestText);
|
|
319
|
+
if (!fallbackHasUsefulCode(response, requestText)) {
|
|
320
|
+
notify('The model replied without any code blocks — nothing was saved. Try again.');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (!content.trim()) {
|
|
324
|
+
notify('The model returned no code — nothing was saved. Try again.');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
let fallback;
|
|
328
|
+
try {
|
|
329
|
+
fallback = await FileWriter.write({
|
|
330
|
+
files: [{ path: name, content }],
|
|
331
|
+
overwrite: false,
|
|
332
|
+
confirm,
|
|
333
|
+
});
|
|
334
|
+
} catch (err) {
|
|
335
|
+
notify(`Failed to write file: ${name} (${err.message})`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const res = fallback[0];
|
|
339
|
+
if (res.status === 'written') {
|
|
340
|
+
notify(`Wrote file: ${name}`);
|
|
341
|
+
logger.log('FILE', `wrote file ${name}${res.target ? ` (${res.target})` : ''}`);
|
|
342
|
+
await validateWrittenCode(name, notify);
|
|
343
|
+
warnIfJunkCode(name, content, notify);
|
|
344
|
+
const fenceCount = [...String(response).matchAll(/```[\w./+-]*[^\n]*\n?([\s\S]*?)(?:```|$)/g)].length;
|
|
345
|
+
if (fenceCount > 1) {
|
|
346
|
+
notify(`Note: the reply contained ${fenceCount} code blocks but only ${name} was saved. Ask me to save the others (one FILE: request per file).`);
|
|
347
|
+
logger.log('WARN', `multi-block reply: saved only ${name} of ${fenceCount} code blocks`);
|
|
348
|
+
}
|
|
349
|
+
} else if (res.status === 'skipped') {
|
|
350
|
+
notify(`Skipped file: ${name} (${res.error})`);
|
|
351
|
+
logger.log('FILE', `skipped file ${name}${res.target ? ` (${res.target})` : ''}: ${res.error}`);
|
|
352
|
+
} else {
|
|
353
|
+
notify(`Failed to write file: ${name} (${res.error})`);
|
|
354
|
+
logger.log('ERROR', `failed writing file ${name}: ${res.error}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function answerWithSearch(messages, provider, query, systemPrompt, confirm, requestText, chatUI) {
|
|
360
|
+
chatUI.clearNotices();
|
|
361
|
+
chatUI.setStatus(`Searching the web for "${query}"…`);
|
|
362
|
+
chatUI.render(messages);
|
|
363
|
+
const results = await WebSearch.search(query);
|
|
364
|
+
|
|
365
|
+
let userContent;
|
|
366
|
+
if (results.length > 0) {
|
|
367
|
+
chatUI.setStatus('');
|
|
368
|
+
chatUI.notify(`Found ${results.length} web result${results.length === 1 ? '' : 's'} for "${query}"`);
|
|
369
|
+
const searchContext = results.map((r, i) =>
|
|
370
|
+
`${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.snippet}`
|
|
371
|
+
).join('\n\n');
|
|
372
|
+
userContent = `Web search results for "${query}":\n\n${searchContext}\n\nAnswer based on these results.${looksLikeStoryRequest(requestText) ? ' If this is a famous story, folk tale, legend, or myth, tell the real, well-known version fully and save it to a .txt file too.' : ''}`;
|
|
373
|
+
} else {
|
|
374
|
+
chatUI.setStatus('');
|
|
375
|
+
chatUI.notify('No web results — answering from my own knowledge.');
|
|
376
|
+
userContent = query;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
messages.push({ role: 'user', content: userContent, hidden: true });
|
|
71
380
|
Session.save(messages);
|
|
72
381
|
|
|
73
|
-
|
|
382
|
+
chatUI.setThinking(true);
|
|
383
|
+
chatUI.render(messages);
|
|
74
384
|
let response = '';
|
|
385
|
+
const frame = new FrameThrottle(() => chatUI.render(messages));
|
|
75
386
|
try {
|
|
76
387
|
for await (const chunk of provider.stream(messages, {
|
|
77
388
|
model: Config.getEffectiveModel(provider.name),
|
|
78
389
|
systemPrompt,
|
|
390
|
+
maxTokens: 600,
|
|
79
391
|
})) {
|
|
80
|
-
Terminal.printAIChunk(chunk);
|
|
81
392
|
response += chunk;
|
|
393
|
+
chatUI.setStreaming(response);
|
|
394
|
+
frame.schedule();
|
|
82
395
|
}
|
|
83
396
|
} catch (err) {
|
|
84
|
-
|
|
397
|
+
chatUI.notify(err.message || 'Stream error');
|
|
398
|
+
logger.log('ERROR', err.message || 'Stream error');
|
|
85
399
|
}
|
|
86
|
-
|
|
400
|
+
frame.flush();
|
|
87
401
|
if (response) {
|
|
88
402
|
messages.push({ role: 'assistant', content: response });
|
|
403
|
+
logger.log('AI', response);
|
|
404
|
+
chatUI.setStreaming(null);
|
|
405
|
+
await writeFilesFromResponse(response, confirm, requestText, (msg) => chatUI.notify(msg));
|
|
89
406
|
}
|
|
407
|
+
chatUI.render(messages);
|
|
90
408
|
Session.save(messages);
|
|
91
409
|
}
|
|
92
410
|
|
|
@@ -95,55 +413,85 @@ function extractUrlFromText(text) {
|
|
|
95
413
|
return m ? m[0] : null;
|
|
96
414
|
}
|
|
97
415
|
|
|
98
|
-
async function answerWithFetch(messages, provider, url, systemPrompt) {
|
|
99
|
-
|
|
416
|
+
async function answerWithFetch(messages, provider, url, systemPrompt, confirm, chatUI, requestText = '') {
|
|
417
|
+
chatUI.clearNotices();
|
|
418
|
+
chatUI.setStatus(`Opening ${url}…`);
|
|
419
|
+
chatUI.render(messages);
|
|
100
420
|
let page;
|
|
101
421
|
try {
|
|
102
422
|
page = await WebFetch.fetch(url);
|
|
103
423
|
} catch (err) {
|
|
104
|
-
|
|
424
|
+
chatUI.setStatus('');
|
|
425
|
+
chatUI.notify(err.message || 'Could not fetch the link.');
|
|
426
|
+
logger.log('ERROR', err.message || 'Could not fetch the link.');
|
|
427
|
+
chatUI.render(messages);
|
|
105
428
|
return;
|
|
106
429
|
}
|
|
107
430
|
|
|
108
|
-
|
|
431
|
+
chatUI.setStatus('');
|
|
432
|
+
chatUI.notify(`Opened: ${page.title || page.url}`);
|
|
109
433
|
const content = page.text.trim();
|
|
110
434
|
if (!content) {
|
|
111
|
-
|
|
435
|
+
chatUI.notify('Nothing readable found on that page.');
|
|
436
|
+
chatUI.render(messages);
|
|
112
437
|
return;
|
|
113
438
|
}
|
|
114
439
|
|
|
115
440
|
messages.push({
|
|
116
441
|
role: 'user',
|
|
117
442
|
content: `Here is the content fetched from the URL "${page.url}" (${page.title || 'no title'}):\n\n${content}\n\nPlease summarize and answer based on this content. Be concise.`,
|
|
443
|
+
hidden: true,
|
|
118
444
|
});
|
|
119
445
|
Session.save(messages);
|
|
120
446
|
|
|
121
|
-
|
|
447
|
+
chatUI.setThinking(true);
|
|
448
|
+
chatUI.render(messages);
|
|
122
449
|
let response = '';
|
|
450
|
+
const frame = new FrameThrottle(() => chatUI.render(messages));
|
|
123
451
|
try {
|
|
124
452
|
for await (const chunk of provider.stream(messages, {
|
|
125
453
|
model: Config.getEffectiveModel(provider.name),
|
|
126
454
|
systemPrompt,
|
|
455
|
+
maxTokens: 600,
|
|
127
456
|
})) {
|
|
128
|
-
Terminal.printAIChunk(chunk);
|
|
129
457
|
response += chunk;
|
|
458
|
+
chatUI.setStreaming(response);
|
|
459
|
+
frame.schedule();
|
|
130
460
|
}
|
|
131
461
|
} catch (err) {
|
|
132
|
-
|
|
462
|
+
chatUI.notify(err.message || 'Stream error');
|
|
463
|
+
logger.log('ERROR', err.message || 'Stream error');
|
|
133
464
|
}
|
|
134
|
-
|
|
465
|
+
frame.flush();
|
|
135
466
|
if (response) {
|
|
136
467
|
messages.push({ role: 'assistant', content: response });
|
|
468
|
+
logger.log('AI', response);
|
|
469
|
+
chatUI.setStreaming(null);
|
|
470
|
+
await writeFilesFromResponse(response, confirm, requestText, (msg) => chatUI.notify(msg));
|
|
137
471
|
}
|
|
472
|
+
chatUI.render(messages);
|
|
138
473
|
Session.save(messages);
|
|
139
474
|
}
|
|
140
475
|
|
|
141
476
|
async function chat(provider, systemPrompt) {
|
|
142
|
-
|
|
477
|
+
// Fresh start every launch: we never resume silently. The old active session
|
|
478
|
+
// is auto-backup'd (if it has messages) and stays on disk, reachable later
|
|
479
|
+
// via /sessions and /session open.
|
|
480
|
+
Session.autoBackup();
|
|
481
|
+
Session.create(null);
|
|
482
|
+
let messages = Session.loadActive();
|
|
483
|
+
const sessionName = Session.activeName();
|
|
143
484
|
let exited = false;
|
|
485
|
+
const chatUI = new ChatUI({
|
|
486
|
+
model: Config.getEffectiveModel(provider.name),
|
|
487
|
+
engine: provider.displayName,
|
|
488
|
+
});
|
|
489
|
+
logger.log('SESSION', `started provider=${provider.name} model=${Config.getEffectiveModel(provider.name)} session=${sessionName}`);
|
|
144
490
|
|
|
145
491
|
if (messages.length > 0) {
|
|
146
|
-
|
|
492
|
+
chatUI.notify(`Fresh session "${sessionName}" (${messages.filter((m) => m.role === 'user').length} previous message(s) carried). Use /sessions to open an old one.`);
|
|
493
|
+
} else {
|
|
494
|
+
chatUI.notify(`Hello! I am VierrataleAI (session "${sessionName}"). Type /help for commands, or just start chatting.`);
|
|
147
495
|
}
|
|
148
496
|
|
|
149
497
|
const rl = createInterface({
|
|
@@ -153,29 +501,50 @@ async function chat(provider, systemPrompt) {
|
|
|
153
501
|
});
|
|
154
502
|
|
|
155
503
|
let eof = false;
|
|
156
|
-
|
|
504
|
+
const lineQueue = [];
|
|
505
|
+
let lineWaiter = null;
|
|
157
506
|
|
|
507
|
+
rl.on('line', (line) => {
|
|
508
|
+
if (lineWaiter) {
|
|
509
|
+
const waiter = lineWaiter;
|
|
510
|
+
lineWaiter = null;
|
|
511
|
+
waiter(line);
|
|
512
|
+
} else if (lineQueue.length < 64) {
|
|
513
|
+
lineQueue.push(line);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
158
516
|
rl.on('close', () => {
|
|
159
|
-
// Graceful shutdown: don't hard-exit mid-command. Mark EOF so the loop
|
|
160
|
-
// stops asking for more input, and let any in-flight command finish.
|
|
161
517
|
eof = true;
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
518
|
+
if (lineWaiter) {
|
|
519
|
+
const waiter = lineWaiter;
|
|
520
|
+
lineWaiter = null;
|
|
521
|
+
waiter(null);
|
|
165
522
|
}
|
|
166
523
|
});
|
|
167
524
|
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
525
|
+
const readLine = (promptText) => {
|
|
526
|
+
if (lineQueue.length) return Promise.resolve(lineQueue.shift());
|
|
527
|
+
if (eof) return Promise.resolve(null);
|
|
528
|
+
process.stdout.write(promptText);
|
|
529
|
+
return new Promise((resolve) => {
|
|
530
|
+
lineWaiter = resolve;
|
|
531
|
+
});
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const prompt = () => readLine(
|
|
535
|
+
`\n${Branding.colors.bold}${Branding.colors.primary}${Branding.USER_PROMPT}${Branding.colors.reset} ${Branding.colors.dim}›${Branding.colors.reset} `
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
const askConfirm = async (msg) => {
|
|
539
|
+
const ans = await readLine(
|
|
540
|
+
` ${Branding.colors.warning}${msg}${Branding.colors.reset} ${Branding.colors.bold}(y/N)${Branding.colors.reset} `
|
|
177
541
|
);
|
|
178
|
-
|
|
542
|
+
if (ans === null) return false;
|
|
543
|
+
const a = ans.trim().toLowerCase();
|
|
544
|
+
return a === 'y' || a === 'yes';
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
chatUI.render(messages);
|
|
179
548
|
|
|
180
549
|
while (!exited && !eof) {
|
|
181
550
|
let input;
|
|
@@ -192,28 +561,185 @@ async function chat(provider, systemPrompt) {
|
|
|
192
561
|
|
|
193
562
|
const cmd = trimmed.toLowerCase();
|
|
194
563
|
|
|
564
|
+
if (!cmd.startsWith('/')) logger.log('USER', trimmed);
|
|
565
|
+
|
|
195
566
|
if (cmd === '/quit' || cmd === '/exit' || cmd === '/q') {
|
|
196
|
-
|
|
567
|
+
chatUI.setStatus('');
|
|
568
|
+
process.stdout.write('\x1b[2K');
|
|
569
|
+
console.log(' Goodbye!');
|
|
570
|
+
logger.log('SESSION', 'ended');
|
|
197
571
|
exited = true;
|
|
198
572
|
rl.close();
|
|
199
573
|
break;
|
|
200
574
|
}
|
|
201
575
|
|
|
202
576
|
if (cmd === '/clear') {
|
|
203
|
-
|
|
204
|
-
|
|
577
|
+
chatUI.clearNotices();
|
|
578
|
+
chatUI.setStatus('');
|
|
579
|
+
chatUI.render(messages);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (cmd === '/log' || cmd.startsWith('/log ')) {
|
|
584
|
+
const rest = cmd === '/log' ? '' : cmd.slice(5).trim();
|
|
585
|
+
chatUI.clearNotices();
|
|
586
|
+
if (rest === 'path' || rest === 'file') {
|
|
587
|
+
chatUI.notify(`Log file: ${logger.getPath()}`);
|
|
588
|
+
} else if (/^\d+$/.test(rest)) {
|
|
589
|
+
const lines = logger.tail(Math.max(1, Math.min(200, parseInt(rest, 10))));
|
|
590
|
+
chatUI.notify(lines.length ? lines.join('\n') : 'No log entries yet.');
|
|
591
|
+
} else {
|
|
592
|
+
const lines = logger.tail(30);
|
|
593
|
+
chatUI.notify(lines.length ? lines.join('\n') : 'No log entries yet.');
|
|
594
|
+
}
|
|
595
|
+
chatUI.render(messages);
|
|
205
596
|
continue;
|
|
206
597
|
}
|
|
207
598
|
|
|
208
599
|
if (cmd === '/help') {
|
|
209
|
-
|
|
600
|
+
chatUI.clearNotices();
|
|
601
|
+
chatUI.notify(
|
|
602
|
+
`Commands:\n /search <query> · /fetch <url> · /model [name] · /provider [name]\n /models · /log [n|path] · /new · /sessions · /session new|open|delete|backup|restore\n /clear · /help · /quit`
|
|
603
|
+
);
|
|
604
|
+
chatUI.render(messages);
|
|
210
605
|
continue;
|
|
211
606
|
}
|
|
212
607
|
|
|
213
608
|
if (cmd === '/new') {
|
|
214
|
-
Session.
|
|
215
|
-
|
|
216
|
-
|
|
609
|
+
Session.autoBackup();
|
|
610
|
+
const name = Session.create(null);
|
|
611
|
+
messages = Session.loadActive();
|
|
612
|
+
chatUI.clearNotices();
|
|
613
|
+
chatUI.notify(`Started a new conversation (session: "${name}"). All messages cleared — old ones are preserved, use /sessions to return.`);
|
|
614
|
+
chatUI.render(messages);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (cmd === '/sessions') {
|
|
619
|
+
chatUI.clearNotices();
|
|
620
|
+
const list = Session.list();
|
|
621
|
+
if (!list.length) {
|
|
622
|
+
chatUI.notify('No sessions yet.');
|
|
623
|
+
} else {
|
|
624
|
+
list.forEach((s, i) => {
|
|
625
|
+
chatUI.notify(`${i + 1}. ${s.name}${s.active ? ' (current)' : ''} — ${s.count} msg${s.count === 1 ? '' : 's'} — ${s.preview}`);
|
|
626
|
+
});
|
|
627
|
+
chatUI.notify('Open with /session open <name|#> · delete with /session delete <name|#>');
|
|
628
|
+
}
|
|
629
|
+
chatUI.render(messages);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (cmd.startsWith('/session ')) {
|
|
634
|
+
const parts = trimmed.split(/\s+/);
|
|
635
|
+
const sub = parts[1];
|
|
636
|
+
chatUI.clearNotices();
|
|
637
|
+
if (sub === 'new') {
|
|
638
|
+
const name = parts.length > 2 ? trimmed.slice(parts[0].length + parts[1].length + 1).trim() : null;
|
|
639
|
+
const created = Session.create(name || null);
|
|
640
|
+
messages = Session.loadActive();
|
|
641
|
+
chatUI.notify(`Session "${created}" created and active.`);
|
|
642
|
+
chatUI.render(messages);
|
|
643
|
+
} else if (sub === 'open') {
|
|
644
|
+
const target = parts.slice(2).join(' ') || null;
|
|
645
|
+
if (!target) {
|
|
646
|
+
chatUI.notify('Usage: /session open <name|#index> — see /sessions.');
|
|
647
|
+
} else {
|
|
648
|
+
const opened = Session.open(target);
|
|
649
|
+
if (!opened) {
|
|
650
|
+
chatUI.notify(`Session "${target}" not found. Use /sessions to list.`);
|
|
651
|
+
} else {
|
|
652
|
+
messages = Session.loadActive();
|
|
653
|
+
chatUI.notify(`Resumed session "${opened}" (${messages.filter((m) => m.role === 'user').length} previous message(s)).`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
chatUI.render(messages);
|
|
657
|
+
} else if (sub === 'delete') {
|
|
658
|
+
const target = parts.slice(2).join(' ') || null;
|
|
659
|
+
if (!target) {
|
|
660
|
+
chatUI.notify('Usage: /session delete <name|#> — see /sessions.');
|
|
661
|
+
} else {
|
|
662
|
+
const removed = Session.remove(target);
|
|
663
|
+
if (!removed) {
|
|
664
|
+
chatUI.notify(`Session "${target}" not found. Use /sessions to list.`);
|
|
665
|
+
} else {
|
|
666
|
+
if (Session.activeName() === removed) {
|
|
667
|
+
messages = Session.loadActive();
|
|
668
|
+
chatUI.notify(`Session "${removed}" deleted. Active session is now "${Session.activeName()}".`);
|
|
669
|
+
} else {
|
|
670
|
+
chatUI.notify(`Session "${removed}" deleted.`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
chatUI.render(messages);
|
|
675
|
+
} else if (sub === 'backup') {
|
|
676
|
+
const target = parts.slice(2).join(' ') || null;
|
|
677
|
+
if (target) {
|
|
678
|
+
const prevActive = Session.activeName();
|
|
679
|
+
const opened = Session.open(target);
|
|
680
|
+
if (!opened) {
|
|
681
|
+
chatUI.notify(`Session "${target}" not found. Use /sessions to list.`);
|
|
682
|
+
} else {
|
|
683
|
+
const saved = Session.autoBackup();
|
|
684
|
+
Session.open(prevActive);
|
|
685
|
+
if (saved) chatUI.notify(`Backed up session "${opened}" → ${saved}. Restore with /session restore.`);
|
|
686
|
+
else chatUI.notify(`Session "${opened}" is empty; nothing to back up.`);
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
const saved = Session.autoBackup();
|
|
690
|
+
if (saved) chatUI.notify(`Backed up current session → ${saved}. Restore with /session restore.`);
|
|
691
|
+
else chatUI.notify('No messages to back up.');
|
|
692
|
+
}
|
|
693
|
+
chatUI.render(messages);
|
|
694
|
+
} else if (sub === 'backups') {
|
|
695
|
+
const list = Session.listBackups();
|
|
696
|
+
chatUI.clearNotices();
|
|
697
|
+
if (!list.length) {
|
|
698
|
+
chatUI.notify('No session backups found yet. They are created automatically on every fresh start.');
|
|
699
|
+
} else {
|
|
700
|
+
chatUI.notify(`Session backups (${list.length}):`);
|
|
701
|
+
list.slice(0, 20).forEach((b, i) => chatUI.notify(`${i + 1}. ${b.name}`));
|
|
702
|
+
chatUI.notify('Restore one with /session restore <name>');
|
|
703
|
+
}
|
|
704
|
+
chatUI.render(messages);
|
|
705
|
+
} else if (sub === 'restore') {
|
|
706
|
+
const target = parts.slice(2).join(' ') || null;
|
|
707
|
+
if (!target) {
|
|
708
|
+
const list = Session.listBackups();
|
|
709
|
+
if (list.length) {
|
|
710
|
+
chatUI.clearNotices();
|
|
711
|
+
chatUI.notify(`Latest backups (${list.length}):`);
|
|
712
|
+
list.slice(0, 10).forEach((b, i) => chatUI.notify(`${i + 1}. ${b.name}`));
|
|
713
|
+
chatUI.notify('Usage: /session restore <backup-name|#index>');
|
|
714
|
+
} else {
|
|
715
|
+
chatUI.notify('No backups found. Usage: /session restore <backup-name|#>');
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
let t = target;
|
|
719
|
+
if (/^#\d+$/.test(t)) {
|
|
720
|
+
const list = Session.listBackups();
|
|
721
|
+
const idx = parseInt(t.slice(1), 10);
|
|
722
|
+
const b = list[idx - 1];
|
|
723
|
+
if (!b) {
|
|
724
|
+
chatUI.notify(`Backup #${t.slice(1)} not found.`);
|
|
725
|
+
chatUI.render(messages);
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
t = b.name;
|
|
729
|
+
}
|
|
730
|
+
const restored = Session.restoreBackup(t);
|
|
731
|
+
if (!restored) {
|
|
732
|
+
chatUI.notify(`Backup "${target}" not found. Use /session backups to list.`);
|
|
733
|
+
} else {
|
|
734
|
+
messages = Session.loadActive();
|
|
735
|
+
chatUI.notify(`Restored backup into session "${restored}".`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
chatUI.render(messages);
|
|
739
|
+
} else {
|
|
740
|
+
chatUI.notify('Usage: /session new [name] · /session open <name|#> · /session delete <name|#> · /session backup [name|#] · /session backups · /session restore <name|#>');
|
|
741
|
+
chatUI.render(messages);
|
|
742
|
+
}
|
|
217
743
|
continue;
|
|
218
744
|
}
|
|
219
745
|
|
|
@@ -221,31 +747,81 @@ async function chat(provider, systemPrompt) {
|
|
|
221
747
|
const models = provider.isLocal
|
|
222
748
|
? Catalog.getLocalModels()
|
|
223
749
|
: Catalog.getCloudModels();
|
|
224
|
-
|
|
750
|
+
const installedSet = new Set();
|
|
751
|
+
if (provider.isLocal) {
|
|
752
|
+
for (const m of await Installer.getInstalledModels(Config.get('engineHost'))) {
|
|
753
|
+
installedSet.add(m);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
chatUI.clearNotices();
|
|
225
757
|
for (const m of models) {
|
|
226
758
|
const info = Catalog.getModelInfo(m);
|
|
227
759
|
const current = m === Config.getEffectiveModel(provider.name) ? ' (active)' : '';
|
|
228
|
-
|
|
760
|
+
if (provider.isLocal) {
|
|
761
|
+
const have = installedSet.has(info.realModel);
|
|
762
|
+
chatUI.notify(`${m}${have ? ' ✓' : ' (not downloaded)'}${current} - ${info.tier}`);
|
|
763
|
+
} else {
|
|
764
|
+
chatUI.notify(`${m}${current} - ${info.tier}`);
|
|
765
|
+
}
|
|
229
766
|
}
|
|
767
|
+
if (provider.isLocal) {
|
|
768
|
+
const extras = [...installedSet].filter(
|
|
769
|
+
(m) => !Catalog.getModelInfo(m) && m !== Config.getEffectiveModel(provider.name)
|
|
770
|
+
);
|
|
771
|
+
for (const m of extras) {
|
|
772
|
+
chatUI.notify(` ${m} ✓ (raw engine model)`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
chatUI.render(messages);
|
|
230
776
|
continue;
|
|
231
777
|
}
|
|
232
778
|
|
|
233
779
|
if (cmd.startsWith('/model ')) {
|
|
234
780
|
const modelName = cmd.split(' ')[1];
|
|
235
781
|
if (modelName) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
782
|
+
const canonical = Catalog.normalize(modelName);
|
|
783
|
+
const current = Config.getEffectiveModel(provider.name);
|
|
784
|
+
const info = Catalog.getModelInfo(canonical);
|
|
785
|
+
const applyModel = (model) => {
|
|
786
|
+
Config.save({ model });
|
|
787
|
+
Config.setProviderModel(provider.name, model);
|
|
788
|
+
chatUI.setModel(model, provider.displayName);
|
|
789
|
+
};
|
|
790
|
+
if (provider.isLocal && Catalog.isCloudModel(canonical)) {
|
|
791
|
+
chatUI.notify(`Cannot use cloud model "${canonical}" on ${provider.displayName} (local). Use /model VRTL-6.pro or /provider openai.`);
|
|
792
|
+
} else if (!info) {
|
|
793
|
+
if (provider.isLocal) {
|
|
794
|
+
const switched = await ensureRawLocalModel(modelName, (m) => chatUI.notify(m));
|
|
795
|
+
if (switched.ok) {
|
|
796
|
+
applyModel(switched.model);
|
|
797
|
+
chatUI.notify(`Model: ${switched.model}${switched.reason ? ` — ${switched.reason}` : ''}`);
|
|
798
|
+
} else {
|
|
799
|
+
chatUI.notify(`Model "${modelName}" is not available. Current: ${current}`);
|
|
800
|
+
}
|
|
801
|
+
} else {
|
|
802
|
+
chatUI.notify(`Unknown model: ${modelName}`);
|
|
803
|
+
}
|
|
804
|
+
} else if (info.isLocal) {
|
|
805
|
+
const switched = await ensureLocalModel(canonical, (m) => chatUI.notify(m));
|
|
806
|
+
if (switched.ok) {
|
|
807
|
+
applyModel(switched.model);
|
|
808
|
+
chatUI.notify(`Model: ${switched.model}${switched.reason ? ` — ${switched.reason}` : ''}`);
|
|
809
|
+
} else {
|
|
810
|
+
chatUI.notify(`Could not switch to "${canonical}": ${switched.reason}. Current: ${current}`);
|
|
811
|
+
}
|
|
240
812
|
} else {
|
|
241
|
-
|
|
813
|
+
applyModel(canonical);
|
|
814
|
+
chatUI.notify(`Model: ${canonical}`);
|
|
242
815
|
}
|
|
243
816
|
}
|
|
817
|
+
chatUI.render(messages);
|
|
244
818
|
continue;
|
|
245
819
|
}
|
|
246
820
|
|
|
247
821
|
if (cmd === '/model') {
|
|
248
|
-
|
|
822
|
+
chatUI.clearNotices();
|
|
823
|
+
chatUI.notify(`Current model: ${Config.getEffectiveModel(provider.name)}`);
|
|
824
|
+
chatUI.render(messages);
|
|
249
825
|
continue;
|
|
250
826
|
}
|
|
251
827
|
|
|
@@ -256,99 +832,294 @@ async function chat(provider, systemPrompt) {
|
|
|
256
832
|
const next = ProviderFactory.create(prov);
|
|
257
833
|
if (next && await next.isAvailable()) {
|
|
258
834
|
provider = next;
|
|
259
|
-
|
|
835
|
+
chatUI.notify(`Provider: ${prov}`);
|
|
260
836
|
} else {
|
|
261
|
-
|
|
837
|
+
chatUI.notify(`Provider saved, but ${prov} is not available right now. Will apply on restart.`);
|
|
838
|
+
}
|
|
839
|
+
if (provider.isLocal && Catalog.isCloudModel(Config.getEffectiveModel(provider.name))) {
|
|
840
|
+
const fallback = Catalog.getDefaultModel();
|
|
841
|
+
Config.save({ model: fallback });
|
|
842
|
+
Config.setProviderModel(provider.name, fallback);
|
|
843
|
+
chatUI.setModel(fallback, provider.displayName);
|
|
844
|
+
chatUI.notify(`Cloud model can't run on ${provider.displayName}; switched to ${fallback}.`);
|
|
262
845
|
}
|
|
263
846
|
} else {
|
|
264
|
-
|
|
847
|
+
chatUI.notify(`Unknown provider: ${prov}`);
|
|
265
848
|
}
|
|
849
|
+
chatUI.render(messages);
|
|
266
850
|
continue;
|
|
267
851
|
}
|
|
268
852
|
|
|
269
853
|
if (cmd === '/provider') {
|
|
270
|
-
|
|
854
|
+
chatUI.clearNotices();
|
|
855
|
+
chatUI.notify(`Current provider: ${provider.name}`);
|
|
856
|
+
chatUI.render(messages);
|
|
271
857
|
continue;
|
|
272
858
|
}
|
|
273
859
|
|
|
274
860
|
if (cmd.startsWith('/system')) {
|
|
275
861
|
const prompt = trimmed.slice(7).trim();
|
|
862
|
+
chatUI.clearNotices();
|
|
276
863
|
if (prompt.toLowerCase() === 'reset') {
|
|
277
864
|
Config.save({ systemPrompt: '' });
|
|
278
865
|
systemPrompt = loadSystemPrompt();
|
|
279
|
-
|
|
866
|
+
chatUI.notify('System prompt reset to default.');
|
|
280
867
|
} else if (prompt) {
|
|
281
868
|
Config.save({ systemPrompt: prompt });
|
|
282
869
|
systemPrompt = prompt;
|
|
283
|
-
|
|
870
|
+
chatUI.notify('System prompt updated.');
|
|
284
871
|
} else {
|
|
285
|
-
|
|
872
|
+
chatUI.notify(`Current system prompt: ${systemPrompt}`);
|
|
286
873
|
}
|
|
874
|
+
chatUI.render(messages);
|
|
287
875
|
continue;
|
|
288
876
|
}
|
|
289
877
|
|
|
290
878
|
if (cmd.startsWith('/search ')) {
|
|
291
879
|
const query = trimmed.slice(8).trim();
|
|
292
880
|
if (!query) {
|
|
293
|
-
|
|
881
|
+
chatUI.notify('Usage: /search <query>');
|
|
882
|
+
chatUI.render(messages);
|
|
294
883
|
continue;
|
|
295
884
|
}
|
|
296
|
-
|
|
885
|
+
chatUI.clearNotices();
|
|
886
|
+
chatUI.setStatus('');
|
|
887
|
+
messages.push({ role: 'user', content: trimmed });
|
|
888
|
+
logger.log('USER', trimmed);
|
|
889
|
+
await answerWithSearch(messages, provider, query, systemPrompt, askConfirm, trimmed, chatUI);
|
|
297
890
|
continue;
|
|
298
891
|
}
|
|
299
892
|
|
|
300
893
|
if (cmd.startsWith('/fetch ') || cmd === '/fetch') {
|
|
301
894
|
const urlPart = trimmed.slice(6).trim().split(/\s+/)[0];
|
|
302
895
|
if (!urlPart) {
|
|
303
|
-
|
|
896
|
+
chatUI.notify('Usage: /fetch <url>');
|
|
897
|
+
chatUI.render(messages);
|
|
304
898
|
continue;
|
|
305
899
|
}
|
|
306
|
-
|
|
900
|
+
chatUI.clearNotices();
|
|
901
|
+
chatUI.setStatus('');
|
|
902
|
+
messages.push({ role: 'user', content: trimmed });
|
|
903
|
+
logger.log('USER', trimmed);
|
|
904
|
+
await answerWithFetch(messages, provider, urlPart, systemPrompt, askConfirm, chatUI, trimmed);
|
|
307
905
|
continue;
|
|
308
906
|
}
|
|
309
907
|
|
|
310
908
|
const intent = detectIntent(trimmed);
|
|
909
|
+
if (intent.type === 'self') {
|
|
910
|
+
const model = Config.getEffectiveModel(provider.name);
|
|
911
|
+
const info = Catalog.getModelInfo(model);
|
|
912
|
+
chatUI.clearNotices();
|
|
913
|
+
chatUI.notify(
|
|
914
|
+
`I'm running on ${model} (${info ? info.realModel : model}) via the ${provider.displayName} engine.`
|
|
915
|
+
);
|
|
916
|
+
chatUI.render(messages);
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (intent.type === 'greeting') {
|
|
921
|
+
messages.push({ role: 'user', content: trimmed });
|
|
922
|
+
const reply = 'Hello! How can I help you today?';
|
|
923
|
+
messages.push({ role: 'assistant', content: reply });
|
|
924
|
+
logger.log('AI', reply);
|
|
925
|
+
Session.save(messages);
|
|
926
|
+
chatUI.clearNotices();
|
|
927
|
+
chatUI.render(messages);
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Deterministic math: answer arithmetic ourselves (the tiny engine often
|
|
932
|
+
// hallucinates), so we never send a pure math ask to the model.
|
|
933
|
+
if (!looksLikeOperationRequest(trimmed)) {
|
|
934
|
+
const mathAnswer = trySolveMath(trimmed);
|
|
935
|
+
if (mathAnswer) {
|
|
936
|
+
messages.push({ role: 'user', content: trimmed });
|
|
937
|
+
messages.push({ role: 'assistant', content: mathAnswer });
|
|
938
|
+
logger.log('AI', mathAnswer);
|
|
939
|
+
Session.save(messages);
|
|
940
|
+
chatUI.clearNotices();
|
|
941
|
+
chatUI.render(messages);
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
311
946
|
if (intent.type === 'knowledge') {
|
|
312
|
-
|
|
313
|
-
|
|
947
|
+
const topic = extractSearchTopic(trimmed) || trimmed;
|
|
948
|
+
chatUI.setStatus('');
|
|
949
|
+
messages.push({ role: 'user', content: trimmed });
|
|
950
|
+
await answerWithSearch(messages, provider, topic, systemPrompt, askConfirm, trimmed, chatUI);
|
|
314
951
|
continue;
|
|
315
952
|
}
|
|
316
953
|
|
|
954
|
+
// Operation requests: plan and run sandboxed tools (create folders/files, etc.).
|
|
955
|
+
if (looksLikeOperationRequest(trimmed)) {
|
|
956
|
+
chatUI.clearNotices();
|
|
957
|
+
const cwd = process.cwd();
|
|
958
|
+
const timeoutMs = Config.get('commandTimeoutMs');
|
|
959
|
+
const done = await answerWithTools(messages, provider, trimmed, systemPrompt, askConfirm, chatUI, cwd, timeoutMs);
|
|
960
|
+
if (done) continue;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Refine the last built website (colours, theme, title, sections...).
|
|
964
|
+
// runWithTools() decides whether it really is a refine request; otherwise
|
|
965
|
+
// answerWithTools() returns false and normal chat below takes over.
|
|
966
|
+
if (
|
|
967
|
+
/[\s\S]*\b(make|switch|change|turn|set|use|add|remove|delete|drop|recolor|restyle|redesign|update|apply|repaint|adjust|rename)\b[\s\S]*\b(it|site|website|page|theme|mode|color|colour|title|name|section|accent|background|style|design)\b[\s\S]*/i.test(trimmed)
|
|
968
|
+
) {
|
|
969
|
+
chatUI.clearNotices();
|
|
970
|
+
const done = await answerWithTools(messages, provider, trimmed, systemPrompt, askConfirm, chatUI, process.cwd(), Config.get('commandTimeoutMs'));
|
|
971
|
+
if (done) continue;
|
|
972
|
+
}
|
|
973
|
+
|
|
317
974
|
// If the message contains a bare URL, open it automatically and ask the AI to summarize.
|
|
318
975
|
if (/https?:\/\/\S+/i.test(trimmed)) {
|
|
319
976
|
const url = extractUrlFromText(trimmed);
|
|
320
977
|
if (url) {
|
|
321
|
-
|
|
322
|
-
|
|
978
|
+
chatUI.clearNotices();
|
|
979
|
+
chatUI.setStatus('');
|
|
980
|
+
messages.push({ role: 'user', content: trimmed });
|
|
981
|
+
await answerWithFetch(messages, provider, url, systemPrompt, askConfirm, chatUI, trimmed);
|
|
323
982
|
continue;
|
|
324
983
|
}
|
|
325
984
|
}
|
|
326
985
|
|
|
327
986
|
messages.push({ role: 'user', content: trimmed });
|
|
987
|
+
if (looksLikeFileRequest(trimmed)) {
|
|
988
|
+
messages.push({ role: 'user', content: FILE_BLOCK_PROMPT, hidden: true });
|
|
989
|
+
}
|
|
328
990
|
Session.save(messages);
|
|
329
|
-
Terminal.printAIStart();
|
|
330
991
|
|
|
992
|
+
chatUI.setThinking(true);
|
|
993
|
+
chatUI.render(messages);
|
|
331
994
|
let response = '';
|
|
995
|
+
const frame = new FrameThrottle(() => chatUI.render(messages));
|
|
332
996
|
try {
|
|
333
997
|
for await (const chunk of provider.stream(messages, {
|
|
334
998
|
model: Config.getEffectiveModel(provider.name),
|
|
335
999
|
systemPrompt,
|
|
1000
|
+
maxTokens: 600,
|
|
336
1001
|
})) {
|
|
337
|
-
Terminal.printAIChunk(chunk);
|
|
338
1002
|
response += chunk;
|
|
1003
|
+
chatUI.setStreaming(response);
|
|
1004
|
+
frame.schedule();
|
|
339
1005
|
}
|
|
340
1006
|
} catch (err) {
|
|
341
|
-
|
|
1007
|
+
chatUI.notify(err.message || 'Stream error');
|
|
1008
|
+
logger.log('ERROR', err.message || 'Stream error');
|
|
342
1009
|
}
|
|
343
|
-
|
|
344
|
-
Terminal.printAIEnd();
|
|
1010
|
+
frame.flush();
|
|
345
1011
|
if (response) {
|
|
346
1012
|
messages.push({ role: 'assistant', content: response });
|
|
1013
|
+
logger.log('AI', response);
|
|
1014
|
+
chatUI.setStreaming(null);
|
|
1015
|
+
await writeFilesFromResponse(response, askConfirm, trimmed, (msg) => chatUI.notify(msg));
|
|
347
1016
|
}
|
|
1017
|
+
chatUI.render(messages);
|
|
348
1018
|
Session.save(messages);
|
|
349
1019
|
}
|
|
350
1020
|
}
|
|
351
1021
|
|
|
1022
|
+
async function ensureLocalModel(modelName, notify) {
|
|
1023
|
+
const info = Catalog.getModelInfo(modelName);
|
|
1024
|
+
if (!info) return ensureRawLocalModel(modelName, notify);
|
|
1025
|
+
if (!info.isLocal) return { ok: false, model: null, reason: `${modelName} is not a local model` };
|
|
1026
|
+
return ensureRawLocalModel(info.realModel, notify);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Ensure a local engine model is usable, returning the actual model that
|
|
1030
|
+
// should be used. Preference: exact match, then an installed same-family
|
|
1031
|
+
// sibling, then a fresh download, then any installed model. Switching never
|
|
1032
|
+
// dead-ends while at least one local model exists.
|
|
1033
|
+
async function ensureRawLocalModel(realModel, notify) {
|
|
1034
|
+
const host = Config.get('engineHost');
|
|
1035
|
+
const resolved = await Installer.resolveModel(host, realModel);
|
|
1036
|
+
if (resolved.model) {
|
|
1037
|
+
return { ok: true, model: resolved.model, reason: resolved.reason };
|
|
1038
|
+
}
|
|
1039
|
+
if (notify) notify(`Downloading ${realModel}… (this can take a while)`);
|
|
1040
|
+
const pulled = await Installer.pullModel(host, realModel);
|
|
1041
|
+
if (pulled) {
|
|
1042
|
+
if (notify) notify(`Model ${realModel} ready.`);
|
|
1043
|
+
return { ok: true, model: realModel, reason: null };
|
|
1044
|
+
}
|
|
1045
|
+
if (notify) notify(`Could not download ${realModel}.`);
|
|
1046
|
+
const first = await Installer.firstInstalled(host);
|
|
1047
|
+
if (first) {
|
|
1048
|
+
return { ok: true, model: first, reason: `Could not download ${realModel}; using installed ${first}` };
|
|
1049
|
+
}
|
|
1050
|
+
return { ok: false, model: null, reason: `Could not download model: ${realModel}` };
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
export { looksLikeFileRequest, requestedFileName, extractFallbackFile, storyFileName, responseWantsFile, responseIsSoloCode, languageFileName, fallbackHasUsefulCode, validateWrittenCode, looksLikeJunkCode, warnIfJunkCode, writeFilesFromResponse };
|
|
1054
|
+
// Command/tool layer: asks the model to plan filesystem/shell operations,
|
|
1055
|
+
// executes them with the sandboxed executor, then streams the final answer.
|
|
1056
|
+
async function answerWithTools(messages, provider, requestText, systemPrompt, confirm, chatUI, cwd, timeoutMs) {
|
|
1057
|
+
let results = [];
|
|
1058
|
+
try {
|
|
1059
|
+
const out = await runWithTools({
|
|
1060
|
+
provider,
|
|
1061
|
+
messages,
|
|
1062
|
+
requestText,
|
|
1063
|
+
systemPrompt,
|
|
1064
|
+
chatUI,
|
|
1065
|
+
cwd,
|
|
1066
|
+
timeoutMs,
|
|
1067
|
+
});
|
|
1068
|
+
results = out.results;
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
chatUI.notify(err.message || 'Tool error');
|
|
1071
|
+
logger.log('ERROR', err.message || 'Tool error');
|
|
1072
|
+
chatUI.render(messages);
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
if (!results.length) return false;
|
|
1076
|
+
|
|
1077
|
+
messages.push({ role: 'user', content: requestText });
|
|
1078
|
+
const resultRows = results.map((r) => {
|
|
1079
|
+
const res = r.result;
|
|
1080
|
+
return {
|
|
1081
|
+
tool: r.name,
|
|
1082
|
+
params: r.name === 'run_command' ? { command: res.command } : { path: r.params.path },
|
|
1083
|
+
success: res.success,
|
|
1084
|
+
stdout: res.stdout,
|
|
1085
|
+
stderr: res.stderr,
|
|
1086
|
+
exitCode: res.exitCode,
|
|
1087
|
+
};
|
|
1088
|
+
});
|
|
1089
|
+
messages.push({ role: 'user', content: TOOL_RESULTS_PROMPT(JSON.stringify(resultRows, null, 2)), hidden: true });
|
|
1090
|
+
Session.save(messages);
|
|
1091
|
+
|
|
1092
|
+
chatUI.setThinking(true);
|
|
1093
|
+
chatUI.render(messages);
|
|
1094
|
+
let response = '';
|
|
1095
|
+
const frame = new FrameThrottle(() => chatUI.render(messages));
|
|
1096
|
+
try {
|
|
1097
|
+
for await (const chunk of provider.stream(messages, {
|
|
1098
|
+
model: Config.getEffectiveModel(provider.name),
|
|
1099
|
+
systemPrompt,
|
|
1100
|
+
maxTokens: 220,
|
|
1101
|
+
})) {
|
|
1102
|
+
response += chunk;
|
|
1103
|
+
chatUI.setStreaming(response);
|
|
1104
|
+
frame.schedule();
|
|
1105
|
+
}
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
chatUI.notify(err.message || 'Stream error');
|
|
1108
|
+
logger.log('ERROR', err.message || 'Stream error');
|
|
1109
|
+
}
|
|
1110
|
+
frame.flush();
|
|
1111
|
+
if (response) {
|
|
1112
|
+
messages.push({ role: 'assistant', content: response });
|
|
1113
|
+
logger.log('AI', response);
|
|
1114
|
+
chatUI.setStreaming(null);
|
|
1115
|
+
if (/```/.test(response) || /FILE:/i.test(response)) {
|
|
1116
|
+
await writeFilesFromResponse(response, confirm, requestText, (msg) => chatUI.notify(msg));
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
chatUI.render(messages);
|
|
1120
|
+
Session.save(messages);
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
352
1123
|
export async function run() {
|
|
353
1124
|
const args = parseArgs(process.argv);
|
|
354
1125
|
|
|
@@ -364,21 +1135,45 @@ export async function run() {
|
|
|
364
1135
|
|
|
365
1136
|
Config.load();
|
|
366
1137
|
if (args.provider) Config.save({ provider: args.provider });
|
|
367
|
-
if (args.model) Config.save({ model: args.model });
|
|
1138
|
+
if (args.model) Config.save({ model: Catalog.normalize(args.model) });
|
|
1139
|
+
|
|
1140
|
+
await Terminal.showProgress(
|
|
1141
|
+
['Checking engine', 'Installing dependencies', 'Downloading model', 'Optimizing'],
|
|
1142
|
+
() => Installer.ensure()
|
|
1143
|
+
);
|
|
368
1144
|
|
|
369
|
-
|
|
370
|
-
await Installer.ensure();
|
|
1145
|
+
await ensureLocalModel(Config.get('model'));
|
|
371
1146
|
|
|
372
1147
|
const provider = await ProviderFactory.autoDetect();
|
|
373
1148
|
if (!provider) {
|
|
1149
|
+
logger.init();
|
|
1150
|
+
logger.log('ERROR', 'no provider available');
|
|
374
1151
|
Terminal.printError('No AI engine available. Please install or configure a provider.');
|
|
375
1152
|
process.exit(1);
|
|
376
1153
|
}
|
|
377
1154
|
|
|
1155
|
+
const chosenModel = Config.getEffectiveModel(provider.name);
|
|
1156
|
+
if (provider.isLocal && Catalog.isCloudModel(chosenModel)) {
|
|
1157
|
+
const fallback = Catalog.getDefaultModel();
|
|
1158
|
+
Config.save({ model: fallback });
|
|
1159
|
+
Config.setProviderModel(provider.name, fallback);
|
|
1160
|
+
await ensureLocalModel(fallback);
|
|
1161
|
+
logger.init();
|
|
1162
|
+
logger.log('WARN', `cloud model "${chosenModel}" cannot run on local provider ${provider.name}; switched to ${fallback}`);
|
|
1163
|
+
Terminal.printInfo(`"${chosenModel}" is a cloud model and can't run on ${provider.displayName}. Switched to ${fallback}.`);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
logger.init();
|
|
1167
|
+
logger.log('APP', `started provider=${provider.name} model=${Config.getEffectiveModel(provider.name)}`);
|
|
1168
|
+
|
|
378
1169
|
Config.save({ provider: provider.name });
|
|
379
1170
|
|
|
380
|
-
if (
|
|
381
|
-
|
|
1171
|
+
if (provider.warmup) provider.warmup();
|
|
1172
|
+
|
|
1173
|
+
// Always clear the screen at startup: everything above the CLI is removed so
|
|
1174
|
+
// the banner + conversation start on a clean slate. The banner itself is
|
|
1175
|
+
// drawn inside ChatUI.render(), so it can't be erased by the first render.
|
|
1176
|
+
Terminal.clear();
|
|
382
1177
|
|
|
383
1178
|
const systemPrompt = loadSystemPrompt();
|
|
384
1179
|
await chat(provider, systemPrompt);
|