@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/probeChat.js ADDED
@@ -0,0 +1,269 @@
1
+ import 'dotenv/config';
2
+ import { ProbeAgent } from '@probelabs/probe/agent';
3
+ import { TokenUsageDisplay } from './tokenUsageDisplay.js';
4
+ import { writeFileSync, existsSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { TelemetryConfig } from './telemetry.js';
7
+ import { trace } from '@opentelemetry/api';
8
+ import { appTracer } from './appTracer.js';
9
+
10
+ // Parse and validate allowed folders from environment variable
11
+ const allowedFolders = process.env.ALLOWED_FOLDERS
12
+ ? process.env.ALLOWED_FOLDERS.split(',').map(folder => folder.trim()).filter(Boolean)
13
+ : [];
14
+
15
+ // Validate folders exist on startup - will be handled by index.js in non-interactive mode
16
+ // This is kept for backward compatibility with direct ProbeChat usage
17
+ const validateFolders = () => {
18
+ if (allowedFolders.length > 0) {
19
+ for (const folder of allowedFolders) {
20
+ const exists = existsSync(folder);
21
+ // Only log if not in non-interactive mode or if in debug mode
22
+ if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
23
+ console.log(`- ${folder} ${exists ? '✓' : '✗ (not found)'}`);
24
+ if (!exists) {
25
+ console.warn(`Warning: Folder "${folder}" does not exist or is not accessible`);
26
+ }
27
+ }
28
+ }
29
+ } else {
30
+ // Only log if not in non-interactive mode or if in debug mode
31
+ if (process.env.PROBE_NON_INTERACTIVE !== '1' || process.env.DEBUG_CHAT === '1') {
32
+ console.warn('No folders configured via ALLOWED_FOLDERS. Tools might default to current directory or require explicit paths.');
33
+ }
34
+ }
35
+ };
36
+
37
+ // Only validate folders on startup if not in non-interactive mode
38
+ if (typeof process !== 'undefined' && !process.env.PROBE_CHAT_SKIP_FOLDER_VALIDATION) {
39
+ validateFolders();
40
+ }
41
+
42
+ /**
43
+ * Extract image URLs from message text
44
+ * @param {string} message - The message text to analyze
45
+ * @param {boolean} debug - Whether to log debug information
46
+ * @returns {Array} Array of { url: string, cleanedMessage: string }
47
+ */
48
+ function extractImageUrls(message, debug = false) {
49
+ // This function should be called within the session context, so it will inherit the trace ID
50
+ const tracer = trace.getTracer('probe-chat', '1.0.0');
51
+ return tracer.startActiveSpan('content.image.extract', (span) => {
52
+ try {
53
+ // Pattern to match image URLs and base64 data:
54
+ // 1. GitHub private-user-images URLs (always images, regardless of extension)
55
+ // 2. GitHub user-attachments/assets URLs (always images, regardless of extension)
56
+ // 3. URLs with common image extensions (PNG, JPG, JPEG, WebP, GIF)
57
+ // 4. Base64 data URLs (data:image/...)
58
+ // Updated to stop at quotes, spaces, or common HTML/XML delimiters
59
+ const imageUrlPattern = /(?:data:image\/[a-zA-Z]*;base64,[A-Za-z0-9+/=]+|https?:\/\/(?:(?:private-user-images\.githubusercontent\.com|github\.com\/user-attachments\/assets)\/[^\s"'<>]+|[^\s"'<>]+\.(?:png|jpg|jpeg|webp|gif)(?:\?[^\s"'<>]*)?))/gi;
60
+
61
+ span.setAttributes({
62
+ 'message.length': message.length,
63
+ 'debug.enabled': debug
64
+ });
65
+
66
+ if (debug) {
67
+ console.log(`[DEBUG] Scanning message for image URLs. Message length: ${message.length}`);
68
+ console.log(`[DEBUG] Image URL pattern: ${imageUrlPattern.toString()}`);
69
+ }
70
+
71
+ const urls = [];
72
+ let match;
73
+
74
+ while ((match = imageUrlPattern.exec(message)) !== null) {
75
+ urls.push(match[0]);
76
+ if (debug) {
77
+ console.log(`[DEBUG] Found image URL: ${match[0]}`);
78
+ }
79
+ }
80
+
81
+ // Clean the message by removing found URLs
82
+ let cleanedMessage = message;
83
+ urls.forEach(url => {
84
+ cleanedMessage = cleanedMessage.replace(url, '').trim();
85
+ });
86
+
87
+ // Clean up any remaining extra whitespace
88
+ cleanedMessage = cleanedMessage.replace(/\s+/g, ' ').trim();
89
+
90
+ span.setAttributes({
91
+ 'images.found': urls.length,
92
+ 'message.cleaned_length': cleanedMessage.length
93
+ });
94
+
95
+ if (debug) {
96
+ console.log(`[DEBUG] Extracted ${urls.length} image URLs`);
97
+ console.log(`[DEBUG] Cleaned message length: ${cleanedMessage.length}`);
98
+ }
99
+
100
+ return { urls, cleanedMessage };
101
+ } finally {
102
+ span.end();
103
+ }
104
+ });
105
+ }
106
+
107
+ /**
108
+ * ProbeChat class using ProbeAgent with MCP support
109
+ */
110
+ export class ProbeChat {
111
+ /**
112
+ * Create a new ProbeChat instance
113
+ * @param {Object} options - Configuration options
114
+ * @param {string} [options.sessionId] - Optional session ID
115
+ * @param {boolean} [options.isNonInteractive=false] - Suppress internal logs if true
116
+ * @param {string} [options.customPrompt] - Custom prompt to replace the default system message
117
+ * @param {string} [options.promptType] - Predefined prompt type (architect, code-review, support)
118
+ * @param {boolean} [options.allowEdit=false] - Allow the use of the 'implement' tool
119
+ * @param {string} [options.provider] - Force specific AI provider
120
+ * @param {string} [options.model] - Override model name
121
+ * @param {boolean} [options.debug] - Enable debug mode
122
+ * @param {boolean} [options.enableMcp=false] - Enable MCP tool integration
123
+ * @param {Array} [options.mcpServers] - MCP server configurations
124
+ */
125
+ constructor(options = {}) {
126
+ this.isNonInteractive = options.isNonInteractive || process.env.PROBE_NON_INTERACTIVE === '1';
127
+ this.debug = options.debug || process.env.DEBUG_CHAT === '1';
128
+
129
+ // Initialize ProbeAgent with MCP support
130
+ const agentOptions = {
131
+ ...options,
132
+ path: allowedFolders.length > 0 ? allowedFolders[0] : process.cwd(),
133
+ enableMcp: options.enableMcp || process.env.ENABLE_MCP === '1',
134
+ mcpServers: options.mcpServers
135
+ };
136
+
137
+ this.agent = new ProbeAgent(agentOptions);
138
+
139
+ // Initialize telemetry and token display
140
+ this.telemetryConfig = new TelemetryConfig();
141
+ this.tokenUsage = new TokenUsageDisplay();
142
+
143
+ if (this.debug) {
144
+ console.log(`[DEBUG] ProbeChat initialized with MCP ${agentOptions.enableMcp ? 'enabled' : 'disabled'}`);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Answer a question using the agentic flow with optional image support
150
+ * @param {string} message - The user's question
151
+ * @param {Object} [options] - Optional configuration
152
+ * @param {string} [options.schema] - JSON schema for structured output
153
+ * @param {Array} [options.images] - Array of image data (base64 strings or URLs)
154
+ * @returns {Promise<string>} - The final answer
155
+ */
156
+ async chat(message, options = {}) {
157
+ if (!message || typeof message !== 'string' || message.trim().length === 0) {
158
+ throw new Error('Message is required and must be a non-empty string');
159
+ }
160
+
161
+ // Extract images from the message text if not provided in options
162
+ let images = options.images || [];
163
+ let cleanedMessage = message;
164
+
165
+ if (!images.length) {
166
+ const extracted = extractImageUrls(message, this.debug);
167
+ images = extracted.urls;
168
+ cleanedMessage = extracted.cleanedMessage;
169
+
170
+ if (this.debug && images.length > 0) {
171
+ console.log(`[DEBUG] Extracted ${images.length} images from message`);
172
+ }
173
+ }
174
+
175
+ // Use ProbeAgent to answer the question
176
+ const result = await this.agent.answer(cleanedMessage, images, options);
177
+
178
+ // Update token usage display
179
+ this.tokenUsage.updateFromTokenCounter(this.agent.tokenCounter);
180
+
181
+ if (!this.isNonInteractive) {
182
+ this.tokenUsage.display();
183
+ }
184
+
185
+ return result;
186
+ }
187
+
188
+ /**
189
+ * Get session ID
190
+ */
191
+ getSessionId() {
192
+ return this.agent.sessionId;
193
+ }
194
+
195
+ /**
196
+ * Get usage summary for the current session
197
+ */
198
+ getUsageSummary() {
199
+ return this.agent.tokenCounter.getUsageSummary();
200
+ }
201
+
202
+ /**
203
+ * Clear conversation history
204
+ */
205
+ clearHistory() {
206
+ this.agent.clearHistory();
207
+ this.tokenUsage.clear();
208
+ }
209
+
210
+ /**
211
+ * Export conversation history
212
+ */
213
+ exportHistory() {
214
+ return this.agent.history.map(msg => ({ ...msg }));
215
+ }
216
+
217
+ /**
218
+ * Save conversation history to file
219
+ */
220
+ saveHistory(filename) {
221
+ if (!filename) {
222
+ filename = `probe-chat-history-${this.agent.sessionId}-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
223
+ }
224
+
225
+ const historyData = {
226
+ sessionId: this.agent.sessionId,
227
+ timestamp: new Date().toISOString(),
228
+ messages: this.exportHistory(),
229
+ usage: this.getUsageSummary()
230
+ };
231
+
232
+ writeFileSync(filename, JSON.stringify(historyData, null, 2));
233
+
234
+ if (!this.isNonInteractive) {
235
+ console.log(`Conversation history saved to: ${filename}`);
236
+ }
237
+
238
+ return filename;
239
+ }
240
+
241
+ /**
242
+ * Cancel current request
243
+ */
244
+ cancel() {
245
+ this.agent.cancel();
246
+ }
247
+
248
+ /**
249
+ * Clean up resources (including MCP connections)
250
+ */
251
+ async cleanup() {
252
+ try {
253
+ await this.agent.cleanup();
254
+ } catch (error) {
255
+ // Log the error but don't throw to ensure graceful cleanup
256
+ if (!this.isNonInteractive) {
257
+ console.warn('Warning during cleanup:', error.message);
258
+ }
259
+ }
260
+ }
261
+ }
262
+
263
+ // Create the default instance using environment variables
264
+ export const chat = new ProbeChat({
265
+ enableMcp: process.env.ENABLE_MCP === '1',
266
+ debug: process.env.DEBUG_CHAT === '1'
267
+ });
268
+
269
+ export default chat;