@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/session.js
CHANGED
|
@@ -1,141 +1,16 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { Config } from './config.js';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const ACTIVE_FILE = join(ROOT, '.active');
|
|
7
|
-
const MIGRATED_FLAG = join(ROOT, '.migrated');
|
|
5
|
+
const SESSION_FILE = join(Config.getConfigDir(), 'history.json');
|
|
8
6
|
const MAX_MESSAGES = 20;
|
|
9
7
|
|
|
10
|
-
const sanitize = (name) =>
|
|
11
|
-
String(name || '').replace(/[^A-Za-z0-9._-]/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'default';
|
|
12
|
-
|
|
13
|
-
const pathFor = (name) => join(ROOT, `${sanitize(name)}.json`);
|
|
14
|
-
const backupPathFor = (name) => join(ROOT, `${sanitize(name)}.bak.json`);
|
|
15
|
-
|
|
16
|
-
const autoName = () => {
|
|
17
|
-
const d = new Date();
|
|
18
|
-
const p = (n) => String(n).padStart(2, '0');
|
|
19
|
-
return `session-${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}-${p(d.getMilliseconds())}`;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
function ensureDir() {
|
|
23
|
-
if (!existsSync(ROOT)) mkdirSync(ROOT, { recursive: true });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function migrateLegacy() {
|
|
27
|
-
// One-time move of the old single history.json into a session named "default".
|
|
28
|
-
// The .migrated flag guarantees the legacy file can never resurrect a deleted
|
|
29
|
-
// "default" session on later runs.
|
|
30
|
-
try {
|
|
31
|
-
const legacy = join(Config.getConfigDir(), 'history.json');
|
|
32
|
-
if (existsSync(legacy) && !existsSync(MIGRATED_FLAG)) {
|
|
33
|
-
if (!existsSync(pathFor('default'))) {
|
|
34
|
-
const data = JSON.parse(readFileSync(legacy, 'utf-8') || '[]');
|
|
35
|
-
if (Array.isArray(data) && data.length) {
|
|
36
|
-
writeFileSync(pathFor('default'), JSON.stringify(data.slice(-MAX_MESSAGES), null, 2));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
writeFileSync(MIGRATED_FLAG, new Date().toISOString());
|
|
40
|
-
}
|
|
41
|
-
} catch {}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
8
|
export const Session = {
|
|
45
|
-
activeName() {
|
|
46
|
-
ensureDir();
|
|
47
|
-
migrateLegacy();
|
|
48
|
-
try {
|
|
49
|
-
if (existsSync(ACTIVE_FILE)) {
|
|
50
|
-
const name = readFileSync(ACTIVE_FILE, 'utf-8').trim();
|
|
51
|
-
if (name && existsSync(pathFor(name))) return name;
|
|
52
|
-
}
|
|
53
|
-
} catch {}
|
|
54
|
-
if (!existsSync(pathFor('default'))) writeFileSync(pathFor('default'), '[]');
|
|
55
|
-
writeFileSync(ACTIVE_FILE, 'default');
|
|
56
|
-
return 'default';
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
list() {
|
|
60
|
-
ensureDir();
|
|
61
|
-
migrateLegacy();
|
|
62
|
-
const out = [];
|
|
63
|
-
const active = this.activeName();
|
|
64
|
-
for (const f of readdirSync(ROOT)) {
|
|
65
|
-
if (!f.endsWith('.json')) continue;
|
|
66
|
-
const name = f.slice(0, -5);
|
|
67
|
-
try {
|
|
68
|
-
const msgs = JSON.parse(readFileSync(join(ROOT, f), 'utf-8') || '[]');
|
|
69
|
-
const first = msgs.find((m) => m.role === 'user');
|
|
70
|
-
out.push({
|
|
71
|
-
name,
|
|
72
|
-
count: msgs.length,
|
|
73
|
-
preview: first ? String(first.content).slice(0, 60) : '(empty)',
|
|
74
|
-
updated: statSync(join(ROOT, f)).mtimeMs,
|
|
75
|
-
active: name === active,
|
|
76
|
-
});
|
|
77
|
-
} catch {}
|
|
78
|
-
}
|
|
79
|
-
out.sort((a, b) => b.updated - a.updated);
|
|
80
|
-
return out;
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
create(name) {
|
|
84
|
-
ensureDir();
|
|
85
|
-
const safe = sanitize(name || autoName());
|
|
86
|
-
writeFileSync(pathFor(safe), '[]');
|
|
87
|
-
writeFileSync(ACTIVE_FILE, safe);
|
|
88
|
-
return safe;
|
|
89
|
-
},
|
|
90
|
-
|
|
91
|
-
open(name) {
|
|
92
|
-
const sessions = this.list();
|
|
93
|
-
let target = name;
|
|
94
|
-
if (/^#\d+$/.test(name)) {
|
|
95
|
-
const idx = parseInt(name.slice(1), 10);
|
|
96
|
-
const s = sessions[idx - 1];
|
|
97
|
-
if (!s) return null;
|
|
98
|
-
target = s.name;
|
|
99
|
-
}
|
|
100
|
-
const safe = sanitize(target);
|
|
101
|
-
if (!existsSync(pathFor(safe))) return null;
|
|
102
|
-
writeFileSync(ACTIVE_FILE, safe);
|
|
103
|
-
return safe;
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
remove(name) {
|
|
107
|
-
const sessions = this.list();
|
|
108
|
-
let target = name;
|
|
109
|
-
if (/^#\d+$/.test(name)) {
|
|
110
|
-
const idx = parseInt(name.slice(1), 10);
|
|
111
|
-
const s = sessions[idx - 1];
|
|
112
|
-
if (!s) return null;
|
|
113
|
-
target = s.name;
|
|
114
|
-
}
|
|
115
|
-
const safe = sanitize(target);
|
|
116
|
-
const file = pathFor(safe);
|
|
117
|
-
if (!existsSync(file)) return null;
|
|
118
|
-
if (safe === this.activeName()) {
|
|
119
|
-
const others = sessions.filter((s) => s.name !== safe);
|
|
120
|
-
if (others.length) {
|
|
121
|
-
writeFileSync(ACTIVE_FILE, others[0].name);
|
|
122
|
-
} else {
|
|
123
|
-
writeFileSync(ACTIVE_FILE, this.create(null));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
try { unlinkSync(file); } catch {}
|
|
127
|
-
return safe;
|
|
128
|
-
},
|
|
129
|
-
|
|
130
9
|
load() {
|
|
131
|
-
return this.loadActive();
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
loadActive() {
|
|
135
10
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const data = JSON.parse(
|
|
11
|
+
if (existsSync(SESSION_FILE)) {
|
|
12
|
+
const raw = readFileSync(SESSION_FILE, 'utf-8');
|
|
13
|
+
const data = JSON.parse(raw || '[]');
|
|
139
14
|
if (Array.isArray(data)) return data.slice(-MAX_MESSAGES);
|
|
140
15
|
}
|
|
141
16
|
} catch {}
|
|
@@ -144,78 +19,23 @@ export const Session = {
|
|
|
144
19
|
|
|
145
20
|
save(messages) {
|
|
146
21
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
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));
|
|
149
25
|
} catch {}
|
|
150
26
|
},
|
|
151
27
|
|
|
152
|
-
// Snapshot the active session to a timestamped .bak.json before it is
|
|
153
|
-
// replaced or cleared, so a fresh-start or /new never loses the old talk.
|
|
154
|
-
autoBackup() {
|
|
155
|
-
try {
|
|
156
|
-
ensureDir();
|
|
157
|
-
const name = this.activeName();
|
|
158
|
-
const src = pathFor(name);
|
|
159
|
-
if (!existsSync(src)) return null;
|
|
160
|
-
const msgs = JSON.parse(readFileSync(src, 'utf-8') || '[]');
|
|
161
|
-
if (!Array.isArray(msgs) || msgs.length === 0) return null;
|
|
162
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
163
|
-
const dest = backupPathFor(`${name}.${stamp}`);
|
|
164
|
-
writeFileSync(dest, JSON.stringify(msgs, null, 2));
|
|
165
|
-
return `${name}.${stamp}.bak.json`;
|
|
166
|
-
} catch { return null; }
|
|
167
|
-
},
|
|
168
|
-
|
|
169
|
-
backup() {
|
|
170
|
-
return this.autoBackup();
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
// List .bak.json recovery copies for the sessions UI.
|
|
174
|
-
listBackups() {
|
|
175
|
-
ensureDir();
|
|
176
|
-
const out = [];
|
|
177
|
-
for (const f of readdirSync(ROOT)) {
|
|
178
|
-
if (!f.endsWith('.bak.json')) continue;
|
|
179
|
-
const name = f.slice(0, -'.bak.json'.length);
|
|
180
|
-
try {
|
|
181
|
-
out.push({ name, file: f, updated: statSync(join(ROOT, f)).mtimeMs });
|
|
182
|
-
} catch {}
|
|
183
|
-
}
|
|
184
|
-
out.sort((a, b) => b.updated - a.updated);
|
|
185
|
-
return out;
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
// Restore a backup: copy the .bak.json content back into a normal session file
|
|
189
|
-
// (creating <name>-restored if the original still exists) and switch to it.
|
|
190
|
-
restoreBackup(file) {
|
|
191
|
-
try {
|
|
192
|
-
ensureDir();
|
|
193
|
-
let fname = String(file || '').trim();
|
|
194
|
-
if (!fname.endsWith('.bak.json')) {
|
|
195
|
-
if (!fname.endsWith('.json')) fname += '.bak.json';
|
|
196
|
-
}
|
|
197
|
-
const safe = sanitize(fname.replace(/\.bak\.json$/, ''));
|
|
198
|
-
const src = backupPathFor(safe);
|
|
199
|
-
if (!existsSync(src)) return null;
|
|
200
|
-
const msgs = JSON.parse(readFileSync(src, 'utf-8') || '[]');
|
|
201
|
-
if (!Array.isArray(msgs)) return null;
|
|
202
|
-
const base = sanitize(safe.replace(/\.\d{4}-\d{2}-\d{2}T[\d-]+$/, ''));
|
|
203
|
-
let target = base;
|
|
204
|
-
if (existsSync(pathFor(target))) target = `${base}-restored`;
|
|
205
|
-
writeFileSync(pathFor(target), JSON.stringify(msgs.slice(-MAX_MESSAGES), null, 2));
|
|
206
|
-
writeFileSync(ACTIVE_FILE, target);
|
|
207
|
-
return target;
|
|
208
|
-
} catch { return null; }
|
|
209
|
-
},
|
|
210
|
-
|
|
211
28
|
clear() {
|
|
212
29
|
try {
|
|
213
|
-
|
|
214
|
-
writeFileSync(pathFor(this.activeName()), '[]');
|
|
30
|
+
if (existsSync(SESSION_FILE)) writeFileSync(SESSION_FILE, '[]');
|
|
215
31
|
} catch {}
|
|
216
32
|
},
|
|
217
33
|
|
|
218
34
|
hasHistory() {
|
|
219
|
-
|
|
35
|
+
try {
|
|
36
|
+
return existsSync(SESSION_FILE) && (JSON.parse(readFileSync(SESSION_FILE, 'utf-8') || '[]').length > 0);
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
220
40
|
},
|
|
221
|
-
};
|
|
41
|
+
};
|
package/src/ui/branding.js
CHANGED
package/src/utils/intents.js
CHANGED
|
@@ -19,74 +19,14 @@ const SELF_PHRASES = [
|
|
|
19
19
|
'what engine', 'which engine', 'are you gpt', 'are you chatgpt',
|
|
20
20
|
];
|
|
21
21
|
|
|
22
|
-
const ACTION_VERBS = 'create|make|write|save|generate|build|add|new|update|edit|install|put|set';
|
|
23
|
-
const FILE_THINGS = 'file|folder|directory|project|code|script|config|\\.json|app|readme|package\\.json|website|html|css';
|
|
24
|
-
const INFO_ASK = /^(how|what|why|when|where|which)\b/i;
|
|
25
|
-
const FILE_REQUEST = new RegExp(`\\b(${ACTION_VERBS})\\b[^.!?]{0,80}\\b(${FILE_THINGS})\\b`, 'i');
|
|
26
|
-
|
|
27
|
-
const STORY_WORDS = [
|
|
28
|
-
'story', 'stories', 'tale', 'tales', 'legend', 'legends',
|
|
29
|
-
'myth', 'myths', 'fable', 'fables', 'folklore', 'folktale',
|
|
30
|
-
'folk tale', 'fairytale', 'fairy tale',
|
|
31
|
-
].join('|');
|
|
32
|
-
const STORY_LINKS = 'about|of|regarding|recounting|called|named';
|
|
33
|
-
const TRIM_TAIL = '\\s+(?:and|in|into)\\b|\\s*[.!?]|$';
|
|
34
|
-
|
|
35
|
-
const STORY_SUBJECT_PATTERN = new RegExp(
|
|
36
|
-
`\\b(?:${STORY_WORDS})\\s+(?:${STORY_LINKS})\\s+[a-z0-9]{3,}`, 'i',
|
|
37
|
-
);
|
|
38
|
-
const STORY_ASK_PATTERN = new RegExp(
|
|
39
|
-
`\\b(?:tell|write|narrate|recite|make|create|generate|give|share|hear|listen to)\\b[^.!?\\n]{0,60}\\b(?:${STORY_WORDS})\\b`, 'i',
|
|
40
|
-
);
|
|
41
|
-
const STORY_CAPTURE = new RegExp(
|
|
42
|
-
`\\b(?:${STORY_WORDS})\\s+(?:${STORY_LINKS})\\s+([^.!?\\n]{2,}?)\\s*(?=${TRIM_TAIL})`, 'i',
|
|
43
|
-
);
|
|
44
|
-
const ABOUT_CAPTURE = /\b(?:tell me|talk|read|learn|research|hear|know|find out|look up|speak|write|give)\b[^.!?\n]{0,30}\babout\s+([^.!?\n]{2,}?)\s*(?=\s+(?:and|in|into)\b|\s*[.!?]|$)/i;
|
|
45
|
-
const PLACE_PATTERN = /\bfrom\s+[a-z][a-z\s'.-]{1,40}$/i;
|
|
46
|
-
|
|
47
|
-
const GREETINGS = new Set([
|
|
48
|
-
'hi', 'hello', 'hey', 'hii', 'heyy', 'howdy', 'yo', 'hai',
|
|
49
|
-
'halo', 'hallo', 'hola', 'hiya', 'sup', 'good morning',
|
|
50
|
-
'good afternoon', 'good evening', 'good night', 'selamat pagi',
|
|
51
|
-
'selamat siang', 'selamat sore', 'selamat malam', 'assalamualaikum',
|
|
52
|
-
]);
|
|
53
|
-
|
|
54
|
-
export function looksLikeStoryRequest(text) {
|
|
55
|
-
return STORY_ASK_PATTERN.test(text) || STORY_SUBJECT_PATTERN.test(text);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function extractSearchTopic(text) {
|
|
59
|
-
const s = String(text).trim();
|
|
60
|
-
const story = STORY_CAPTURE.exec(s);
|
|
61
|
-
if (story) return story[1].trim();
|
|
62
|
-
const about = ABOUT_CAPTURE.exec(s);
|
|
63
|
-
return about ? about[1].trim() : null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
22
|
export function detectIntent(text) {
|
|
67
23
|
const lower = text.toLowerCase().trim();
|
|
68
24
|
if (lower.startsWith('/')) return { type: 'command' };
|
|
69
25
|
|
|
70
|
-
if (GREETINGS.has(lower)) {
|
|
71
|
-
return { type: 'greeting' };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
26
|
if (SELF_PHRASES.some((p) => lower.includes(p))) {
|
|
75
27
|
return { type: 'self' };
|
|
76
28
|
}
|
|
77
29
|
|
|
78
|
-
if (STORY_SUBJECT_PATTERN.test(lower) || PLACE_PATTERN.test(lower)) {
|
|
79
|
-
return { type: 'knowledge' };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const isKnowledgePrefixed = KNOWLEDGE_PREFIXES.some(
|
|
83
|
-
(p) => lower.startsWith(p + ' ') || lower === p,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
if (!INFO_ASK.test(lower) && !isKnowledgePrefixed && FILE_REQUEST.test(lower)) {
|
|
87
|
-
return { type: 'chat' };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
30
|
const isQuestion = /\?$/.test(lower);
|
|
91
31
|
const firstWord = lower.split(/\s+/)[0] || '';
|
|
92
32
|
|
|
@@ -94,7 +34,7 @@ export function detectIntent(text) {
|
|
|
94
34
|
if (isQuestion && QUESTION_PREFIXES.includes(firstWord)) {
|
|
95
35
|
needsWeb = true;
|
|
96
36
|
}
|
|
97
|
-
if (
|
|
37
|
+
if (KNOWLEDGE_PREFIXES.some((p) => lower.startsWith(p + ' ') || lower === p)) {
|
|
98
38
|
needsWeb = true;
|
|
99
39
|
}
|
|
100
40
|
|