@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.
- package/README.md +338 -0
- package/TRACING.md +226 -0
- package/appTracer.js +947 -0
- package/auth.js +76 -0
- package/bin/probe-chat.js +13 -0
- package/cancelRequest.js +84 -0
- package/fileSpanExporter.js +183 -0
- package/implement/README.md +228 -0
- package/implement/backends/AiderBackend.js +750 -0
- package/implement/backends/BaseBackend.js +276 -0
- package/implement/backends/ClaudeCodeBackend.js +767 -0
- package/implement/backends/MockBackend.js +237 -0
- package/implement/backends/registry.js +85 -0
- package/implement/core/BackendManager.js +567 -0
- package/implement/core/ImplementTool.js +354 -0
- package/implement/core/config.js +428 -0
- package/implement/core/timeouts.js +58 -0
- package/implement/core/utils.js +496 -0
- package/implement/types/BackendTypes.js +126 -0
- package/index.html +3751 -0
- package/index.js +582 -0
- package/logo.png +0 -0
- package/package.json +101 -0
- package/probeChat.js +269 -0
- package/probeTool.js +714 -0
- package/storage/JsonChatStorage.js +476 -0
- package/telemetry.js +287 -0
- package/test/integration/chatFlows.test.js +320 -0
- package/test/integration/toolCalling.test.js +471 -0
- package/test/mocks/mockLLMProvider.js +269 -0
- package/test/test-backends.js +90 -0
- package/test/testUtils.js +530 -0
- package/test/unit/backendTimeout.test.js +161 -0
- package/test/verify-tests.js +118 -0
- package/tokenCounter.js +419 -0
- package/tokenUsageDisplay.js +134 -0
- package/tools.js +186 -0
- package/webServer.js +1103 -0
|
@@ -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
|
+
}
|