@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/src/session.js CHANGED
@@ -1,141 +1,16 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, unlinkSync } from 'fs';
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 ROOT = join(Config.getConfigDir(), 'sessions');
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
- const path = pathFor(this.activeName());
137
- if (existsSync(path)) {
138
- const data = JSON.parse(readFileSync(path, 'utf-8') || '[]');
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
- ensureDir();
148
- writeFileSync(pathFor(this.activeName()), JSON.stringify(messages.slice(-MAX_MESSAGES), null, 2));
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
- ensureDir();
214
- writeFileSync(pathFor(this.activeName()), '[]');
30
+ if (existsSync(SESSION_FILE)) writeFileSync(SESSION_FILE, '[]');
215
31
  } catch {}
216
32
  },
217
33
 
218
34
  hasHistory() {
219
- return this.loadActive().length > 0;
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
+ };
@@ -1,6 +1,6 @@
1
1
  export const Branding = {
2
2
  APP_NAME: 'VierrataleAI',
3
- VERSION: '0.1.0-beta.14',
3
+ VERSION: '0.1.0-beta.8',
4
4
 
5
5
  colors: {
6
6
  primary: '\x1b[38;2;124;58;237m',
@@ -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 (isKnowledgePrefixed) {
37
+ if (KNOWLEDGE_PREFIXES.some((p) => lower.startsWith(p + ' ') || lower === p)) {
98
38
  needsWeb = true;
99
39
  }
100
40