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