@vierratale/ai 0.1.0-beta.2 → 0.1.0-beta.3
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 +18 -0
- package/package.json +1 -1
- package/src/cli.js +66 -39
- package/src/session.js +41 -0
- package/src/ui/branding.js +1 -1
- package/src/utils/intents.js +30 -0
- package/src/utils/websearch.js +7 -7
package/README.md
CHANGED
|
@@ -42,8 +42,26 @@ vierrataleai --model vierratale-pro # Use specific model
|
|
|
42
42
|
- `/models` - List available models
|
|
43
43
|
- `/search <query>` - Search the web and summarize with AI
|
|
44
44
|
- `/clear` - Clear screen
|
|
45
|
+
- `/new` - Start a new conversation (clear memory)
|
|
45
46
|
- `/quit` - Exit
|
|
46
47
|
|
|
48
|
+
## Smart Search
|
|
49
|
+
|
|
50
|
+
Asking a question (Who/What/When/Where/Why/How...?) automatically searches the
|
|
51
|
+
web and summarizes the results with AI. For example:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
> Who is Cristiano Ronaldo?
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
triggers a live web search and answers from the results. Use `/search <query>`
|
|
58
|
+
to force a search explicitly.
|
|
59
|
+
|
|
60
|
+
## Conversation Memory
|
|
61
|
+
|
|
62
|
+
Your conversation is saved automatically and restored on the next launch, so
|
|
63
|
+
you can continue where you left off. Use `/new` to start fresh.
|
|
64
|
+
|
|
47
65
|
## Configuration
|
|
48
66
|
|
|
49
67
|
Config file: `~/.config/vierrataleai/config.json`
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -7,6 +7,8 @@ import { Branding } from './ui/branding.js';
|
|
|
7
7
|
import { Terminal } from './ui/terminal.js';
|
|
8
8
|
import { loadSystemPrompt, showBanner } from './ui/banner.js';
|
|
9
9
|
import { WebSearch } from './utils/websearch.js';
|
|
10
|
+
import { detectIntent } from './utils/intents.js';
|
|
11
|
+
import { Session } from './session.js';
|
|
10
12
|
|
|
11
13
|
function parseArgs(args) {
|
|
12
14
|
const parsed = { provider: null, model: null, clear: false, version: false, help: false };
|
|
@@ -40,14 +42,59 @@ Commands (in chat):
|
|
|
40
42
|
/models List available models
|
|
41
43
|
/search <query> Search the web and summarize with AI
|
|
42
44
|
/clear Clear screen
|
|
45
|
+
/new Start a new conversation (clear memory)
|
|
43
46
|
/quit Exit
|
|
44
47
|
`);
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
async function answerWithSearch(messages, provider, query) {
|
|
51
|
+
Terminal.printInfo('Searching the web...');
|
|
52
|
+
const results = await WebSearch.search(query);
|
|
53
|
+
|
|
54
|
+
if (results.length === 0) {
|
|
55
|
+
Terminal.printError('No results found for the query.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Terminal.printSearchResults(results);
|
|
60
|
+
|
|
61
|
+
const searchContext = results.map((r, i) =>
|
|
62
|
+
`${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.snippet}`
|
|
63
|
+
).join('\n\n');
|
|
64
|
+
|
|
65
|
+
messages.push({
|
|
66
|
+
role: 'user',
|
|
67
|
+
content: `Web search results for "${query}":\n\n${searchContext}\n\nPlease summarize these results and answer based on them. Be concise.`,
|
|
68
|
+
});
|
|
69
|
+
Session.save(messages);
|
|
70
|
+
|
|
71
|
+
Terminal.printAIStart();
|
|
72
|
+
let response = '';
|
|
73
|
+
try {
|
|
74
|
+
for await (const chunk of provider.stream(messages, {
|
|
75
|
+
model: Config.get('model'),
|
|
76
|
+
systemPrompt,
|
|
77
|
+
})) {
|
|
78
|
+
Terminal.printAIChunk(chunk);
|
|
79
|
+
response += chunk;
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
Terminal.printError(err.message || 'Stream error');
|
|
83
|
+
}
|
|
84
|
+
Terminal.printAIEnd();
|
|
85
|
+
if (response) {
|
|
86
|
+
messages.push({ role: 'assistant', content: response });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
47
90
|
async function chat(provider, systemPrompt) {
|
|
48
|
-
const messages =
|
|
91
|
+
const messages = Session.load();
|
|
49
92
|
let exited = false;
|
|
50
93
|
|
|
94
|
+
if (messages.length > 0) {
|
|
95
|
+
Terminal.printInfo(`Resumed ${messages.filter((m) => m.role === 'user').length} previous message(s).`);
|
|
96
|
+
}
|
|
97
|
+
|
|
51
98
|
const rl = createInterface({
|
|
52
99
|
input: process.stdin,
|
|
53
100
|
output: process.stdout,
|
|
@@ -101,6 +148,13 @@ async function chat(provider, systemPrompt) {
|
|
|
101
148
|
continue;
|
|
102
149
|
}
|
|
103
150
|
|
|
151
|
+
if (cmd === '/new') {
|
|
152
|
+
Session.clear();
|
|
153
|
+
messages.length = 0;
|
|
154
|
+
Terminal.printSuccess('Started a new conversation.');
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
104
158
|
if (cmd === '/models') {
|
|
105
159
|
const models = provider.name === 'ollama'
|
|
106
160
|
? Catalog.getLocalModels()
|
|
@@ -148,53 +202,25 @@ async function chat(provider, systemPrompt) {
|
|
|
148
202
|
continue;
|
|
149
203
|
}
|
|
150
204
|
|
|
151
|
-
if (cmd.startsWith('/search ')
|
|
152
|
-
const query =
|
|
205
|
+
if (cmd.startsWith('/search ')) {
|
|
206
|
+
const query = trimmed.slice(8).trim();
|
|
153
207
|
if (!query) {
|
|
154
208
|
Terminal.printWarning('Usage: /search <query>');
|
|
155
209
|
continue;
|
|
156
210
|
}
|
|
211
|
+
await answerWithSearch(messages, provider, query);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
157
214
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
Terminal.printError('No results found for the query.');
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
Terminal.printSearchResults(results);
|
|
167
|
-
|
|
168
|
-
const searchContext = results.map((r, i) =>
|
|
169
|
-
`${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.snippet}`
|
|
170
|
-
).join('\n\n');
|
|
171
|
-
|
|
172
|
-
messages.push({
|
|
173
|
-
role: 'user',
|
|
174
|
-
content: `Web search results for "${query}":\n\n${searchContext}\n\nPlease summarize these results and answer based on them. Be concise.`,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
Terminal.printAIStart();
|
|
178
|
-
let response = '';
|
|
179
|
-
try {
|
|
180
|
-
for await (const chunk of provider.stream(messages, {
|
|
181
|
-
model: Config.get('model'),
|
|
182
|
-
systemPrompt,
|
|
183
|
-
})) {
|
|
184
|
-
Terminal.printAIChunk(chunk);
|
|
185
|
-
response += chunk;
|
|
186
|
-
}
|
|
187
|
-
} catch (err) {
|
|
188
|
-
Terminal.printError(err.message || 'Stream error');
|
|
189
|
-
}
|
|
190
|
-
Terminal.printAIEnd();
|
|
191
|
-
if (response) {
|
|
192
|
-
messages.push({ role: 'assistant', content: response });
|
|
193
|
-
}
|
|
215
|
+
const intent = detectIntent(trimmed);
|
|
216
|
+
if (intent.type === 'knowledge') {
|
|
217
|
+
Terminal.printSuccess(`Auto-search: "${trimmed}"`);
|
|
218
|
+
await answerWithSearch(messages, provider, trimmed);
|
|
194
219
|
continue;
|
|
195
220
|
}
|
|
196
221
|
|
|
197
222
|
messages.push({ role: 'user', content: trimmed });
|
|
223
|
+
Session.save(messages);
|
|
198
224
|
Terminal.printAIStart();
|
|
199
225
|
|
|
200
226
|
let response = '';
|
|
@@ -214,6 +240,7 @@ async function chat(provider, systemPrompt) {
|
|
|
214
240
|
if (response) {
|
|
215
241
|
messages.push({ role: 'assistant', content: response });
|
|
216
242
|
}
|
|
243
|
+
Session.save(messages);
|
|
217
244
|
}
|
|
218
245
|
}
|
|
219
246
|
|
package/src/session.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { Config } from './config.js';
|
|
4
|
+
|
|
5
|
+
const SESSION_FILE = join(Config.getConfigDir(), 'history.json');
|
|
6
|
+
const MAX_MESSAGES = 20;
|
|
7
|
+
|
|
8
|
+
export const Session = {
|
|
9
|
+
load() {
|
|
10
|
+
try {
|
|
11
|
+
if (existsSync(SESSION_FILE)) {
|
|
12
|
+
const raw = readFileSync(SESSION_FILE, 'utf-8');
|
|
13
|
+
const data = JSON.parse(raw || '[]');
|
|
14
|
+
if (Array.isArray(data)) return data.slice(-MAX_MESSAGES);
|
|
15
|
+
}
|
|
16
|
+
} catch {}
|
|
17
|
+
return [];
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
save(messages) {
|
|
21
|
+
try {
|
|
22
|
+
const dir = Config.getConfigDir();
|
|
23
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
24
|
+
writeFileSync(SESSION_FILE, JSON.stringify(messages.slice(-MAX_MESSAGES), null, 2));
|
|
25
|
+
} catch {}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
clear() {
|
|
29
|
+
try {
|
|
30
|
+
if (existsSync(SESSION_FILE)) writeFileSync(SESSION_FILE, '[]');
|
|
31
|
+
} catch {}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
hasHistory() {
|
|
35
|
+
try {
|
|
36
|
+
return existsSync(SESSION_FILE) && (JSON.parse(readFileSync(SESSION_FILE, 'utf-8') || '[]').length > 0);
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
package/src/ui/branding.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const QUESTION_PREFIXES = [
|
|
2
|
+
'who', 'what', 'when', 'where', 'why', 'how',
|
|
3
|
+
'which', 'whose', 'whom', 'is', 'are', 'was', 'were',
|
|
4
|
+
'can', 'could', 'does', 'do', 'did', 'would', 'should', 'will',
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
const KNOWLEDGE_PREFIXES = [
|
|
8
|
+
'tell me about', "what is", "what's", "who is", "who's",
|
|
9
|
+
'what are', 'who was', 'when did', 'when was', 'where is',
|
|
10
|
+
'where was', 'how did', 'how does', 'how to', 'why did', 'why is',
|
|
11
|
+
'latest', 'news', 'current',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function detectIntent(text) {
|
|
15
|
+
const lower = text.toLowerCase().trim();
|
|
16
|
+
if (lower.startsWith('/')) return { type: 'command' };
|
|
17
|
+
|
|
18
|
+
const isQuestion = /\?$/.test(lower);
|
|
19
|
+
const firstWord = lower.split(/\s+/)[0] || '';
|
|
20
|
+
|
|
21
|
+
let needsWeb = false;
|
|
22
|
+
if (isQuestion && QUESTION_PREFIXES.includes(firstWord)) {
|
|
23
|
+
needsWeb = true;
|
|
24
|
+
}
|
|
25
|
+
if (KNOWLEDGE_PREFIXES.some((p) => lower.startsWith(p + ' ') || lower === p)) {
|
|
26
|
+
needsWeb = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { type: needsWeb ? 'knowledge' : 'chat' };
|
|
30
|
+
}
|
package/src/utils/websearch.js
CHANGED
|
@@ -79,15 +79,15 @@ export class WebSearch {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
static _stripTags(text) {
|
|
82
|
+
const map = {
|
|
83
|
+
'&': '&', '<': '<', '>': '>', '"': '"',
|
|
84
|
+
''': "'", ''': "'", ''': "'", ' ': ' ',
|
|
85
|
+
''': "'", '…': '...',
|
|
86
|
+
};
|
|
82
87
|
return String(text)
|
|
83
88
|
.replace(/<[^>]*>/g, '')
|
|
84
|
-
.replace(/&
|
|
85
|
-
.replace(
|
|
86
|
-
.replace(/>/g, '>')
|
|
87
|
-
.replace(/"/g, '"')
|
|
88
|
-
.replace(/'/g, "'")
|
|
89
|
-
.replace(/'/g, "'")
|
|
90
|
-
.replace(/ /g, ' ')
|
|
89
|
+
.replace(/&[a-zA-Z0-9#]+;/g, (m) => map[m] ?? '')
|
|
90
|
+
.replace(/\s{2,}/g, ' ')
|
|
91
91
|
.trim();
|
|
92
92
|
}
|
|
93
93
|
}
|