@probelabs/probe-chat 0.6.0-rc100

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.
@@ -0,0 +1,476 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
4
+
5
+ /**
6
+ * JSON file-based storage for chat history
7
+ * Each session is stored as a separate JSON file in ~/.probe/sessions/
8
+ * Uses file modification time for sorting sessions
9
+ */
10
+ export class JsonChatStorage {
11
+ constructor(options = {}) {
12
+ this.webMode = options.webMode || false;
13
+ this.verbose = options.verbose || false;
14
+ this.baseDir = this.getChatHistoryDir();
15
+ this.sessionsDir = join(this.baseDir, 'sessions');
16
+ this.fallbackToMemory = false;
17
+
18
+ // In-memory fallback storage
19
+ this.memorySessions = new Map();
20
+ this.memoryMessages = new Map(); // sessionId -> messages[]
21
+ }
22
+
23
+ /**
24
+ * Get the appropriate directory for storing chat history
25
+ */
26
+ getChatHistoryDir() {
27
+ if (process.platform === 'win32') {
28
+ // Windows: Use LocalAppData
29
+ const localAppData = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local');
30
+ return join(localAppData, 'probe');
31
+ } else {
32
+ // Mac/Linux: Use ~/.probe
33
+ return join(homedir(), '.probe');
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Ensure the chat history directory exists
39
+ */
40
+ ensureChatHistoryDir() {
41
+ try {
42
+ if (!existsSync(this.baseDir)) {
43
+ mkdirSync(this.baseDir, { recursive: true });
44
+ }
45
+ if (!existsSync(this.sessionsDir)) {
46
+ mkdirSync(this.sessionsDir, { recursive: true });
47
+ }
48
+ return true;
49
+ } catch (error) {
50
+ console.warn(`Failed to create chat history directory ${this.baseDir}:`, error.message);
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Get the file path for a session
57
+ */
58
+ getSessionFilePath(sessionId) {
59
+ return join(this.sessionsDir, `${sessionId}.json`);
60
+ }
61
+
62
+ /**
63
+ * Initialize storage - JSON files if in web mode and directory is accessible
64
+ */
65
+ async initialize() {
66
+ if (!this.webMode) {
67
+ this.fallbackToMemory = true;
68
+ if (this.verbose) {
69
+ console.log('Using in-memory storage (CLI mode)');
70
+ }
71
+ return true;
72
+ }
73
+
74
+ try {
75
+ if (!this.ensureChatHistoryDir()) {
76
+ this.fallbackToMemory = true;
77
+ if (this.verbose) {
78
+ console.log('Cannot create history directory, using in-memory storage');
79
+ }
80
+ return true;
81
+ }
82
+
83
+ if (this.verbose) {
84
+ console.log(`JSON file storage initialized at: ${this.sessionsDir}`);
85
+ }
86
+
87
+ return true;
88
+ } catch (error) {
89
+ console.warn('Failed to initialize JSON storage, falling back to memory:', error.message);
90
+ this.fallbackToMemory = true;
91
+ return true;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Save or update session data
97
+ */
98
+ async saveSession(sessionData) {
99
+ const { id, createdAt, lastActivity, firstMessagePreview, metadata = {} } = sessionData;
100
+
101
+ if (this.fallbackToMemory) {
102
+ this.memorySessions.set(id, {
103
+ id,
104
+ created_at: createdAt,
105
+ last_activity: lastActivity,
106
+ first_message_preview: firstMessagePreview,
107
+ metadata
108
+ });
109
+ return true;
110
+ }
111
+
112
+ try {
113
+ const filePath = this.getSessionFilePath(id);
114
+
115
+ // Read existing session data if it exists
116
+ let existingData = {
117
+ id,
118
+ created_at: createdAt,
119
+ last_activity: lastActivity,
120
+ first_message_preview: firstMessagePreview,
121
+ metadata,
122
+ messages: []
123
+ };
124
+
125
+ if (existsSync(filePath)) {
126
+ try {
127
+ const fileContent = readFileSync(filePath, 'utf8');
128
+ const existing = JSON.parse(fileContent);
129
+ existingData = {
130
+ ...existing,
131
+ last_activity: lastActivity,
132
+ first_message_preview: firstMessagePreview || existing.first_message_preview,
133
+ metadata: { ...existing.metadata, ...metadata }
134
+ };
135
+ } catch (error) {
136
+ console.warn(`Failed to read existing session file ${filePath}:`, error.message);
137
+ }
138
+ }
139
+
140
+ writeFileSync(filePath, JSON.stringify(existingData, null, 2));
141
+ return true;
142
+ } catch (error) {
143
+ console.error('Failed to save session:', error);
144
+ return false;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Update session activity timestamp
150
+ */
151
+ async updateSessionActivity(sessionId, timestamp = Date.now()) {
152
+ if (this.fallbackToMemory) {
153
+ const session = this.memorySessions.get(sessionId);
154
+ if (session) {
155
+ session.last_activity = timestamp;
156
+ }
157
+ return true;
158
+ }
159
+
160
+ try {
161
+ const filePath = this.getSessionFilePath(sessionId);
162
+ if (existsSync(filePath)) {
163
+ const fileContent = readFileSync(filePath, 'utf8');
164
+ const sessionData = JSON.parse(fileContent);
165
+ sessionData.last_activity = timestamp;
166
+ writeFileSync(filePath, JSON.stringify(sessionData, null, 2));
167
+ }
168
+ return true;
169
+ } catch (error) {
170
+ console.error('Failed to update session activity:', error);
171
+ return false;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Save a message to the session
177
+ */
178
+ async saveMessage(sessionId, messageData) {
179
+ const {
180
+ role,
181
+ content,
182
+ timestamp = Date.now(),
183
+ displayType,
184
+ visible = 1,
185
+ images = [],
186
+ metadata = {}
187
+ } = messageData;
188
+
189
+ const message = {
190
+ role,
191
+ content,
192
+ timestamp,
193
+ display_type: displayType,
194
+ visible,
195
+ images,
196
+ metadata
197
+ };
198
+
199
+ if (this.fallbackToMemory) {
200
+ if (!this.memoryMessages.has(sessionId)) {
201
+ this.memoryMessages.set(sessionId, []);
202
+ }
203
+ this.memoryMessages.get(sessionId).push(message);
204
+ return true;
205
+ }
206
+
207
+ try {
208
+ const filePath = this.getSessionFilePath(sessionId);
209
+ let sessionData = {
210
+ id: sessionId,
211
+ created_at: timestamp,
212
+ last_activity: timestamp,
213
+ first_message_preview: null,
214
+ metadata: {},
215
+ messages: []
216
+ };
217
+
218
+ // Read existing session data
219
+ if (existsSync(filePath)) {
220
+ try {
221
+ const fileContent = readFileSync(filePath, 'utf8');
222
+ sessionData = JSON.parse(fileContent);
223
+ } catch (error) {
224
+ console.warn(`Failed to read session file ${filePath}:`, error.message);
225
+ }
226
+ }
227
+
228
+ // Add message to session
229
+ sessionData.messages.push(message);
230
+ sessionData.last_activity = timestamp;
231
+
232
+ // Update first message preview if this is the first user message
233
+ if (role === 'user' && !sessionData.first_message_preview) {
234
+ const preview = content.length > 100 ? content.substring(0, 100) + '...' : content;
235
+ sessionData.first_message_preview = preview;
236
+ }
237
+
238
+ writeFileSync(filePath, JSON.stringify(sessionData, null, 2));
239
+ return true;
240
+ } catch (error) {
241
+ console.error('Failed to save message:', error);
242
+ return false;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Get session history (display messages only)
248
+ */
249
+ async getSessionHistory(sessionId, limit = 100) {
250
+ if (this.fallbackToMemory) {
251
+ const messages = this.memoryMessages.get(sessionId) || [];
252
+ return messages
253
+ .filter(msg => msg.visible)
254
+ .slice(0, limit);
255
+ }
256
+
257
+ try {
258
+ const filePath = this.getSessionFilePath(sessionId);
259
+ if (!existsSync(filePath)) {
260
+ return [];
261
+ }
262
+
263
+ const fileContent = readFileSync(filePath, 'utf8');
264
+ const sessionData = JSON.parse(fileContent);
265
+
266
+ return (sessionData.messages || [])
267
+ .filter(msg => msg.visible)
268
+ .slice(0, limit);
269
+ } catch (error) {
270
+ console.error('Failed to get session history:', error);
271
+ return [];
272
+ }
273
+ }
274
+
275
+ /**
276
+ * List recent sessions using file modification dates
277
+ */
278
+ async listSessions(limit = 50, offset = 0) {
279
+ if (this.fallbackToMemory) {
280
+ const sessions = Array.from(this.memorySessions.values())
281
+ .sort((a, b) => b.last_activity - a.last_activity)
282
+ .slice(offset, offset + limit);
283
+ return sessions;
284
+ }
285
+
286
+ try {
287
+ if (!existsSync(this.sessionsDir)) {
288
+ return [];
289
+ }
290
+
291
+ // Get all JSON files in sessions directory
292
+ const files = readdirSync(this.sessionsDir)
293
+ .filter(file => file.endsWith('.json'))
294
+ .map(file => {
295
+ const filePath = join(this.sessionsDir, file);
296
+ const stat = statSync(filePath);
297
+ return {
298
+ file,
299
+ filePath,
300
+ mtime: stat.mtime.getTime(),
301
+ sessionId: file.replace('.json', '')
302
+ };
303
+ })
304
+ .sort((a, b) => b.mtime - a.mtime) // Sort by modification time (newest first)
305
+ .slice(offset, offset + limit);
306
+
307
+ const sessions = [];
308
+ for (const fileInfo of files) {
309
+ try {
310
+ const fileContent = readFileSync(fileInfo.filePath, 'utf8');
311
+ const sessionData = JSON.parse(fileContent);
312
+ sessions.push({
313
+ id: sessionData.id,
314
+ created_at: sessionData.created_at,
315
+ last_activity: sessionData.last_activity || fileInfo.mtime,
316
+ first_message_preview: sessionData.first_message_preview,
317
+ metadata: sessionData.metadata || {}
318
+ });
319
+ } catch (error) {
320
+ console.warn(`Failed to read session file ${fileInfo.filePath}:`, error.message);
321
+ }
322
+ }
323
+
324
+ return sessions;
325
+ } catch (error) {
326
+ console.error('Failed to list sessions:', error);
327
+ return [];
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Delete a session and its file
333
+ */
334
+ async deleteSession(sessionId) {
335
+ if (this.fallbackToMemory) {
336
+ this.memorySessions.delete(sessionId);
337
+ this.memoryMessages.delete(sessionId);
338
+ return true;
339
+ }
340
+
341
+ try {
342
+ const filePath = this.getSessionFilePath(sessionId);
343
+ if (existsSync(filePath)) {
344
+ unlinkSync(filePath);
345
+ }
346
+ return true;
347
+ } catch (error) {
348
+ console.error('Failed to delete session:', error);
349
+ return false;
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Prune old sessions (older than specified days)
355
+ */
356
+ async pruneOldSessions(olderThanDays = 30) {
357
+ const cutoffTime = Date.now() - (olderThanDays * 24 * 60 * 60 * 1000);
358
+
359
+ if (this.fallbackToMemory) {
360
+ let pruned = 0;
361
+ for (const [sessionId, session] of this.memorySessions.entries()) {
362
+ if (session.last_activity < cutoffTime) {
363
+ this.memorySessions.delete(sessionId);
364
+ this.memoryMessages.delete(sessionId);
365
+ pruned++;
366
+ }
367
+ }
368
+ return pruned;
369
+ }
370
+
371
+ try {
372
+ if (!existsSync(this.sessionsDir)) {
373
+ return 0;
374
+ }
375
+
376
+ const files = readdirSync(this.sessionsDir).filter(file => file.endsWith('.json'));
377
+ let pruned = 0;
378
+
379
+ for (const file of files) {
380
+ const filePath = join(this.sessionsDir, file);
381
+ const stat = statSync(filePath);
382
+
383
+ if (stat.mtime.getTime() < cutoffTime) {
384
+ unlinkSync(filePath);
385
+ pruned++;
386
+ }
387
+ }
388
+
389
+ return pruned;
390
+ } catch (error) {
391
+ console.error('Failed to prune old sessions:', error);
392
+ return 0;
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Get storage statistics
398
+ */
399
+ async getStats() {
400
+ if (this.fallbackToMemory) {
401
+ let messageCount = 0;
402
+ let visibleMessageCount = 0;
403
+
404
+ for (const messages of this.memoryMessages.values()) {
405
+ messageCount += messages.length;
406
+ visibleMessageCount += messages.filter(msg => msg.visible).length;
407
+ }
408
+
409
+ return {
410
+ session_count: this.memorySessions.size,
411
+ message_count: messageCount,
412
+ visible_message_count: visibleMessageCount,
413
+ storage_type: 'memory'
414
+ };
415
+ }
416
+
417
+ try {
418
+ if (!existsSync(this.sessionsDir)) {
419
+ return {
420
+ session_count: 0,
421
+ message_count: 0,
422
+ visible_message_count: 0,
423
+ storage_type: 'json_files'
424
+ };
425
+ }
426
+
427
+ const files = readdirSync(this.sessionsDir).filter(file => file.endsWith('.json'));
428
+ let messageCount = 0;
429
+ let visibleMessageCount = 0;
430
+
431
+ for (const file of files) {
432
+ try {
433
+ const filePath = join(this.sessionsDir, file);
434
+ const fileContent = readFileSync(filePath, 'utf8');
435
+ const sessionData = JSON.parse(fileContent);
436
+
437
+ if (sessionData.messages) {
438
+ messageCount += sessionData.messages.length;
439
+ visibleMessageCount += sessionData.messages.filter(msg => msg.visible).length;
440
+ }
441
+ } catch (error) {
442
+ // Skip corrupted files
443
+ }
444
+ }
445
+
446
+ return {
447
+ session_count: files.length,
448
+ message_count: messageCount,
449
+ visible_message_count: visibleMessageCount,
450
+ storage_type: 'json_files'
451
+ };
452
+ } catch (error) {
453
+ console.error('Failed to get storage stats:', error);
454
+ return {
455
+ session_count: 0,
456
+ message_count: 0,
457
+ visible_message_count: 0,
458
+ storage_type: 'error'
459
+ };
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Check if using persistent storage
465
+ */
466
+ isPersistent() {
467
+ return !this.fallbackToMemory;
468
+ }
469
+
470
+ /**
471
+ * Close storage (no-op for JSON files)
472
+ */
473
+ async close() {
474
+ // No cleanup needed for JSON files
475
+ }
476
+ }