@vierratale/ai 0.1.0-beta.8 → 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 +864 -103
- 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 +130 -2
- 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 +223 -40
- 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/utils/filewriter.js +97 -0
- package/src/utils/intents.js +61 -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,32 +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
|
+
}
|
|
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
|
+
}
|
|
229
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
|
-
|
|
240
|
-
|
|
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
|
+
}
|
|
241
812
|
} else {
|
|
242
|
-
|
|
813
|
+
applyModel(canonical);
|
|
814
|
+
chatUI.notify(`Model: ${canonical}`);
|
|
243
815
|
}
|
|
244
816
|
}
|
|
817
|
+
chatUI.render(messages);
|
|
245
818
|
continue;
|
|
246
819
|
}
|
|
247
820
|
|
|
248
821
|
if (cmd === '/model') {
|
|
249
|
-
|
|
822
|
+
chatUI.clearNotices();
|
|
823
|
+
chatUI.notify(`Current model: ${Config.getEffectiveModel(provider.name)}`);
|
|
824
|
+
chatUI.render(messages);
|
|
250
825
|
continue;
|
|
251
826
|
}
|
|
252
827
|
|
|
@@ -257,54 +832,76 @@ async function chat(provider, systemPrompt) {
|
|
|
257
832
|
const next = ProviderFactory.create(prov);
|
|
258
833
|
if (next && await next.isAvailable()) {
|
|
259
834
|
provider = next;
|
|
260
|
-
|
|
835
|
+
chatUI.notify(`Provider: ${prov}`);
|
|
261
836
|
} else {
|
|
262
|
-
|
|
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}.`);
|
|
263
845
|
}
|
|
264
846
|
} else {
|
|
265
|
-
|
|
847
|
+
chatUI.notify(`Unknown provider: ${prov}`);
|
|
266
848
|
}
|
|
849
|
+
chatUI.render(messages);
|
|
267
850
|
continue;
|
|
268
851
|
}
|
|
269
852
|
|
|
270
853
|
if (cmd === '/provider') {
|
|
271
|
-
|
|
854
|
+
chatUI.clearNotices();
|
|
855
|
+
chatUI.notify(`Current provider: ${provider.name}`);
|
|
856
|
+
chatUI.render(messages);
|
|
272
857
|
continue;
|
|
273
858
|
}
|
|
274
859
|
|
|
275
860
|
if (cmd.startsWith('/system')) {
|
|
276
861
|
const prompt = trimmed.slice(7).trim();
|
|
862
|
+
chatUI.clearNotices();
|
|
277
863
|
if (prompt.toLowerCase() === 'reset') {
|
|
278
864
|
Config.save({ systemPrompt: '' });
|
|
279
865
|
systemPrompt = loadSystemPrompt();
|
|
280
|
-
|
|
866
|
+
chatUI.notify('System prompt reset to default.');
|
|
281
867
|
} else if (prompt) {
|
|
282
868
|
Config.save({ systemPrompt: prompt });
|
|
283
869
|
systemPrompt = prompt;
|
|
284
|
-
|
|
870
|
+
chatUI.notify('System prompt updated.');
|
|
285
871
|
} else {
|
|
286
|
-
|
|
872
|
+
chatUI.notify(`Current system prompt: ${systemPrompt}`);
|
|
287
873
|
}
|
|
874
|
+
chatUI.render(messages);
|
|
288
875
|
continue;
|
|
289
876
|
}
|
|
290
877
|
|
|
291
878
|
if (cmd.startsWith('/search ')) {
|
|
292
879
|
const query = trimmed.slice(8).trim();
|
|
293
880
|
if (!query) {
|
|
294
|
-
|
|
881
|
+
chatUI.notify('Usage: /search <query>');
|
|
882
|
+
chatUI.render(messages);
|
|
295
883
|
continue;
|
|
296
884
|
}
|
|
297
|
-
|
|
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);
|
|
298
890
|
continue;
|
|
299
891
|
}
|
|
300
892
|
|
|
301
893
|
if (cmd.startsWith('/fetch ') || cmd === '/fetch') {
|
|
302
894
|
const urlPart = trimmed.slice(6).trim().split(/\s+/)[0];
|
|
303
895
|
if (!urlPart) {
|
|
304
|
-
|
|
896
|
+
chatUI.notify('Usage: /fetch <url>');
|
|
897
|
+
chatUI.render(messages);
|
|
305
898
|
continue;
|
|
306
899
|
}
|
|
307
|
-
|
|
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);
|
|
308
905
|
continue;
|
|
309
906
|
}
|
|
310
907
|
|
|
@@ -312,71 +909,217 @@ async function chat(provider, systemPrompt) {
|
|
|
312
909
|
if (intent.type === 'self') {
|
|
313
910
|
const model = Config.getEffectiveModel(provider.name);
|
|
314
911
|
const info = Catalog.getModelInfo(model);
|
|
315
|
-
|
|
912
|
+
chatUI.clearNotices();
|
|
913
|
+
chatUI.notify(
|
|
316
914
|
`I'm running on ${model} (${info ? info.realModel : model}) via the ${provider.displayName} engine.`
|
|
317
915
|
);
|
|
916
|
+
chatUI.render(messages);
|
|
318
917
|
continue;
|
|
319
918
|
}
|
|
320
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
|
+
|
|
321
946
|
if (intent.type === 'knowledge') {
|
|
322
|
-
|
|
323
|
-
|
|
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);
|
|
324
951
|
continue;
|
|
325
952
|
}
|
|
326
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
|
+
|
|
327
974
|
// If the message contains a bare URL, open it automatically and ask the AI to summarize.
|
|
328
975
|
if (/https?:\/\/\S+/i.test(trimmed)) {
|
|
329
976
|
const url = extractUrlFromText(trimmed);
|
|
330
977
|
if (url) {
|
|
331
|
-
|
|
332
|
-
|
|
978
|
+
chatUI.clearNotices();
|
|
979
|
+
chatUI.setStatus('');
|
|
980
|
+
messages.push({ role: 'user', content: trimmed });
|
|
981
|
+
await answerWithFetch(messages, provider, url, systemPrompt, askConfirm, chatUI, trimmed);
|
|
333
982
|
continue;
|
|
334
983
|
}
|
|
335
984
|
}
|
|
336
985
|
|
|
337
986
|
messages.push({ role: 'user', content: trimmed });
|
|
987
|
+
if (looksLikeFileRequest(trimmed)) {
|
|
988
|
+
messages.push({ role: 'user', content: FILE_BLOCK_PROMPT, hidden: true });
|
|
989
|
+
}
|
|
338
990
|
Session.save(messages);
|
|
339
|
-
Terminal.printUser(trimmed);
|
|
340
|
-
Terminal.printAIStart();
|
|
341
|
-
Terminal.beginThinking();
|
|
342
|
-
Terminal._thinking = true;
|
|
343
991
|
|
|
992
|
+
chatUI.setThinking(true);
|
|
993
|
+
chatUI.render(messages);
|
|
344
994
|
let response = '';
|
|
995
|
+
const frame = new FrameThrottle(() => chatUI.render(messages));
|
|
345
996
|
try {
|
|
346
997
|
for await (const chunk of provider.stream(messages, {
|
|
347
998
|
model: Config.getEffectiveModel(provider.name),
|
|
348
999
|
systemPrompt,
|
|
1000
|
+
maxTokens: 600,
|
|
349
1001
|
})) {
|
|
350
|
-
Terminal.printAIChunk(chunk);
|
|
351
1002
|
response += chunk;
|
|
1003
|
+
chatUI.setStreaming(response);
|
|
1004
|
+
frame.schedule();
|
|
352
1005
|
}
|
|
353
1006
|
} catch (err) {
|
|
354
|
-
|
|
1007
|
+
chatUI.notify(err.message || 'Stream error');
|
|
1008
|
+
logger.log('ERROR', err.message || 'Stream error');
|
|
355
1009
|
}
|
|
356
|
-
|
|
357
|
-
Terminal.printAIEnd();
|
|
1010
|
+
frame.flush();
|
|
358
1011
|
if (response) {
|
|
359
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));
|
|
360
1016
|
}
|
|
1017
|
+
chatUI.render(messages);
|
|
361
1018
|
Session.save(messages);
|
|
362
1019
|
}
|
|
363
1020
|
}
|
|
364
1021
|
|
|
365
|
-
async function ensureLocalModel(modelName) {
|
|
1022
|
+
async function ensureLocalModel(modelName, notify) {
|
|
366
1023
|
const info = Catalog.getModelInfo(modelName);
|
|
367
|
-
if (!info
|
|
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) {
|
|
368
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 = [];
|
|
369
1058
|
try {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
1059
|
+
const out = await runWithTools({
|
|
1060
|
+
provider,
|
|
1061
|
+
messages,
|
|
1062
|
+
requestText,
|
|
1063
|
+
systemPrompt,
|
|
1064
|
+
chatUI,
|
|
1065
|
+
cwd,
|
|
1066
|
+
timeoutMs,
|
|
1067
|
+
});
|
|
1068
|
+
results = out.results;
|
|
375
1069
|
} catch (err) {
|
|
376
|
-
|
|
1070
|
+
chatUI.notify(err.message || 'Tool error');
|
|
1071
|
+
logger.log('ERROR', err.message || 'Tool error');
|
|
1072
|
+
chatUI.render(messages);
|
|
1073
|
+
return true;
|
|
377
1074
|
}
|
|
378
|
-
|
|
1075
|
+
if (!results.length) return false;
|
|
379
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
|
+
}
|
|
380
1123
|
export async function run() {
|
|
381
1124
|
const args = parseArgs(process.argv);
|
|
382
1125
|
|
|
@@ -392,7 +1135,7 @@ export async function run() {
|
|
|
392
1135
|
|
|
393
1136
|
Config.load();
|
|
394
1137
|
if (args.provider) Config.save({ provider: args.provider });
|
|
395
|
-
if (args.model) Config.save({ model: args.model });
|
|
1138
|
+
if (args.model) Config.save({ model: Catalog.normalize(args.model) });
|
|
396
1139
|
|
|
397
1140
|
await Terminal.showProgress(
|
|
398
1141
|
['Checking engine', 'Installing dependencies', 'Downloading model', 'Optimizing'],
|
|
@@ -403,16 +1146,34 @@ export async function run() {
|
|
|
403
1146
|
|
|
404
1147
|
const provider = await ProviderFactory.autoDetect();
|
|
405
1148
|
if (!provider) {
|
|
1149
|
+
logger.init();
|
|
1150
|
+
logger.log('ERROR', 'no provider available');
|
|
406
1151
|
Terminal.printError('No AI engine available. Please install or configure a provider.');
|
|
407
1152
|
process.exit(1);
|
|
408
1153
|
}
|
|
409
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
|
+
|
|
410
1169
|
Config.save({ provider: provider.name });
|
|
411
1170
|
|
|
412
1171
|
if (provider.warmup) provider.warmup();
|
|
413
1172
|
|
|
414
|
-
|
|
415
|
-
|
|
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();
|
|
416
1177
|
|
|
417
1178
|
const systemPrompt = loadSystemPrompt();
|
|
418
1179
|
await chat(provider, systemPrompt);
|