@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/index.js ADDED
@@ -0,0 +1,582 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Check for non-interactive mode flag early, before any imports
4
+ // This ensures the environment variable is set before any module code runs
5
+ if (process.argv.includes('-m') || process.argv.includes('--message')) {
6
+ process.env.PROBE_NON_INTERACTIVE = '1';
7
+ }
8
+
9
+ // Check if stdin is connected to a pipe (not a TTY)
10
+ // This allows for usage like: echo "query" | probe-chat
11
+ if (!process.stdin.isTTY) {
12
+ process.env.PROBE_NON_INTERACTIVE = '1';
13
+ process.env.PROBE_STDIN_PIPED = '1';
14
+ }
15
+
16
+ import 'dotenv/config';
17
+ import inquirer from 'inquirer';
18
+ import chalk from 'chalk';
19
+ import ora from 'ora';
20
+ import { Command } from 'commander';
21
+ import { existsSync, realpathSync, readFileSync } from 'fs';
22
+ import { resolve, dirname, join } from 'path';
23
+ import { fileURLToPath } from 'url';
24
+ import { randomUUID } from 'crypto';
25
+ import { ProbeChat } from './probeChat.js';
26
+ import { TokenUsageDisplay } from './tokenUsageDisplay.js';
27
+ import { DEFAULT_SYSTEM_MESSAGE } from '@probelabs/probe';
28
+
29
+ /**
30
+ * Main function that runs the Probe Chat CLI or web interface
31
+ */
32
+ export function main() {
33
+ // Get the directory name of the current module
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
35
+ const packageJsonPath = join(__dirname, 'package.json');
36
+
37
+ // Read package.json to get the version
38
+ let version = '1.0.0'; // Default fallback version
39
+ try {
40
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
41
+ version = packageJson.version || version;
42
+ } catch (error) {
43
+ // Non-critical, suppress in non-interactive unless debug
44
+ // console.warn(`Warning: Could not read version from package.json: ${error.message}`);
45
+ }
46
+
47
+ // Create a new instance of the program
48
+ const program = new Command();
49
+
50
+ program
51
+ .name('probe-chat')
52
+ .description('CLI chat interface for Probe code search')
53
+ .version(version)
54
+ .option('-d, --debug', 'Enable debug mode')
55
+ .option('--model-name <model>', 'Specify the model to use') // Renamed from --model
56
+ .option('-f, --force-provider <provider>', 'Force a specific provider (options: anthropic, openai, google)')
57
+ .option('-w, --web', 'Run in web interface mode')
58
+ .option('-p, --port <port>', 'Port to run web server on (default: 8080)')
59
+ .option('-m, --message <message>', 'Send a single message and exit (non-interactive mode)')
60
+ .option('-s, --session-id <sessionId>', 'Specify a session ID for the chat (optional)')
61
+ .option('--images <urls>', 'Comma-separated list of image URLs to include in the message')
62
+ .option('--json', 'Output the response as JSON in non-interactive mode')
63
+ .option('--max-iterations <number>', 'Maximum number of tool iterations allowed (default: 30)')
64
+ .option('--prompt <value>', 'Use a custom prompt (values: architect, code-review, code-review-template, support, path to a file, or arbitrary string)')
65
+ .option('--allow-edit', 'Enable the implement tool for editing files')
66
+ .option('--implement-tool-backend <backend>', 'Choose implementation tool backend (aider, claude-code)')
67
+ .option('--implement-tool-timeout <ms>', 'Implementation tool timeout in milliseconds')
68
+ .option('--implement-tool-config <path>', 'Path to implementation tool configuration file')
69
+ .option('--implement-tool-list-backends', 'List available implementation tool backends')
70
+ .option('--implement-tool-backend-info <backend>', 'Show information about a specific implementation tool backend')
71
+ .option('--trace-file [path]', 'Enable tracing to file (default: ./traces.jsonl)')
72
+ .option('--trace-remote [endpoint]', 'Enable tracing to remote endpoint (default: http://localhost:4318/v1/traces)')
73
+ .option('--trace-console', 'Enable tracing to console (for debugging)')
74
+ .argument('[path]', 'Path to the codebase to search (overrides ALLOWED_FOLDERS)')
75
+ .parse(process.argv);
76
+
77
+ const options = program.opts();
78
+ const pathArg = program.args[0];
79
+
80
+ // Parse image URLs if provided
81
+ let imageUrls = [];
82
+ if (options.images) {
83
+ imageUrls = options.images.split(',').map(url => url.trim()).filter(url => url.length > 0);
84
+ console.log(`Using ${imageUrls.length} image(s):`, imageUrls);
85
+ }
86
+
87
+ // --- Logging Configuration ---
88
+ const isPipedInput = process.env.PROBE_STDIN_PIPED === '1';
89
+ const isNonInteractive = !!options.message || isPipedInput;
90
+
91
+ // Environment variable is already set at the top of the file
92
+ // This is just for code clarity
93
+ if (isNonInteractive && process.env.PROBE_NON_INTERACTIVE !== '1') {
94
+ process.env.PROBE_NON_INTERACTIVE = '1';
95
+ }
96
+
97
+ // Raw logging for non-interactive output
98
+ const rawLog = (...args) => console.log(...args);
99
+ const rawError = (...args) => console.error(...args);
100
+
101
+ // Disable color/formatting in raw non-interactive mode
102
+ if (isNonInteractive && !options.json && !options.debug) {
103
+ chalk.level = 0;
104
+ }
105
+
106
+ // Conditional logging helpers
107
+ const logInfo = (...args) => {
108
+ if (!isNonInteractive || options.debug) {
109
+ console.log(...args);
110
+ }
111
+ };
112
+ const logWarn = (...args) => {
113
+ if (!isNonInteractive || options.debug) {
114
+ console.warn(...args);
115
+ } else if (isNonInteractive) {
116
+ // Optionally log warnings to stderr in non-interactive mode even without debug
117
+ // rawError('Warning:', ...args);
118
+ }
119
+ };
120
+ const logError = (...args) => {
121
+ // Always log errors, but use rawError in non-interactive mode
122
+ if (isNonInteractive) {
123
+ rawError('Error:', ...args); // Prefix with Error: for clarity on stderr
124
+ } else {
125
+ console.error(...args);
126
+ }
127
+ };
128
+ // --- End Logging Configuration ---
129
+
130
+ if (options.debug) {
131
+ process.env.DEBUG_CHAT = '1';
132
+ logInfo(chalk.yellow('Debug mode enabled'));
133
+ }
134
+ if (options.modelName) { // Use renamed option
135
+ process.env.MODEL_NAME = options.modelName;
136
+ logInfo(chalk.blue(`Using model: ${options.modelName}`));
137
+ }
138
+ if (options.forceProvider) {
139
+ const provider = options.forceProvider.toLowerCase();
140
+ if (!['anthropic', 'openai', 'google'].includes(provider)) {
141
+ logError(chalk.red(`Invalid provider "${provider}". Must be one of: anthropic, openai, google`));
142
+ process.exit(1);
143
+ }
144
+ process.env.FORCE_PROVIDER = provider;
145
+ logInfo(chalk.blue(`Forcing provider: ${provider}`));
146
+ }
147
+
148
+ // Set MAX_TOOL_ITERATIONS from command line if provided
149
+ if (options.maxIterations) {
150
+ const maxIterations = parseInt(options.maxIterations, 10);
151
+ if (isNaN(maxIterations) || maxIterations <= 0) {
152
+ logError(chalk.red(`Invalid max iterations value: ${options.maxIterations}. Must be a positive number.`));
153
+ process.exit(1);
154
+ }
155
+ process.env.MAX_TOOL_ITERATIONS = maxIterations.toString();
156
+ logInfo(chalk.blue(`Setting maximum tool iterations to: ${maxIterations}`));
157
+ }
158
+
159
+ // Handle --implement-tool-list-backends option
160
+ if (options.implementToolListBackends) {
161
+ (async () => {
162
+ const { listBackendNames, getBackendMetadata } = await import('./implement/backends/registry.js');
163
+ const backends = listBackendNames();
164
+
165
+ console.log('\nAvailable implementation tool backends:');
166
+ for (const backend of backends) {
167
+ const metadata = getBackendMetadata(backend);
168
+ console.log(`\n ${chalk.bold(backend)} - ${metadata.description}`);
169
+ console.log(` Version: ${metadata.version}`);
170
+ console.log(` Languages: ${metadata.capabilities.supportsLanguages.join(', ')}`);
171
+ }
172
+ process.exit(0);
173
+ })();
174
+ }
175
+
176
+ // Handle --implement-tool-backend-info option
177
+ if (options.implementToolBackendInfo) {
178
+ (async () => {
179
+ const { getBackendMetadata } = await import('./implement/backends/registry.js');
180
+ const metadata = getBackendMetadata(options.implementToolBackendInfo);
181
+
182
+ if (!metadata) {
183
+ console.error(`Backend '${options.implementToolBackendInfo}' not found`);
184
+ process.exit(1);
185
+ }
186
+
187
+ console.log(`\n${chalk.bold('Backend Information: ' + options.implementToolBackendInfo)}`);
188
+ console.log(`\nDescription: ${metadata.description}`);
189
+ console.log(`Version: ${metadata.version}`);
190
+ console.log(`\nCapabilities:`);
191
+ console.log(` Languages: ${metadata.capabilities.supportsLanguages.join(', ')}`);
192
+ console.log(` Streaming: ${metadata.capabilities.supportsStreaming ? '✓' : '✗'}`);
193
+ console.log(` Direct File Edit: ${metadata.capabilities.supportsDirectFileEdit ? '✓' : '✗'}`);
194
+ console.log(` Test Generation: ${metadata.capabilities.supportsTestGeneration ? '✓' : '✗'}`);
195
+ console.log(` Plan Generation: ${metadata.capabilities.supportsPlanGeneration ? '✓' : '✗'}`);
196
+ console.log(` Max Sessions: ${metadata.capabilities.maxConcurrentSessions}`);
197
+ console.log(`\nRequired Dependencies:`);
198
+ for (const dep of metadata.dependencies) {
199
+ console.log(` - ${dep.name} (${dep.type}): ${dep.description}`);
200
+ if (dep.installCommand) {
201
+ console.log(` Install: ${dep.installCommand}`);
202
+ }
203
+ }
204
+ process.exit(0);
205
+ })();
206
+ }
207
+
208
+ // Set ALLOW_EDIT from command line if provided
209
+ if (options.allowEdit) {
210
+ process.env.ALLOW_EDIT = '1';
211
+ logInfo(chalk.blue(`Enabling implement tool with --allow-edit flag`));
212
+ }
213
+
214
+ // Set implementation tool backend options
215
+ if (options.implementToolBackend) {
216
+ process.env.IMPLEMENT_TOOL_BACKEND = options.implementToolBackend;
217
+ logInfo(chalk.blue(`Using implementation tool backend: ${options.implementToolBackend}`));
218
+ }
219
+
220
+ if (options.implementToolTimeout) {
221
+ process.env.IMPLEMENT_TOOL_TIMEOUT = options.implementToolTimeout;
222
+ logInfo(chalk.blue(`Implementation tool timeout: ${options.implementToolTimeout}ms`));
223
+ }
224
+
225
+ if (options.implementToolConfig) {
226
+ process.env.IMPLEMENT_TOOL_CONFIG_PATH = options.implementToolConfig;
227
+ logInfo(chalk.blue(`Using implementation tool config: ${options.implementToolConfig}`));
228
+ }
229
+
230
+ // Set telemetry options from command line if provided
231
+ if (options.traceFile !== undefined) {
232
+ process.env.OTEL_ENABLE_FILE = 'true';
233
+ process.env.OTEL_FILE_PATH = options.traceFile || './traces.jsonl';
234
+ logInfo(chalk.blue(`Enabling file tracing to: ${process.env.OTEL_FILE_PATH}`));
235
+ }
236
+
237
+ if (options.traceRemote !== undefined) {
238
+ process.env.OTEL_ENABLE_REMOTE = 'true';
239
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = options.traceRemote || 'http://localhost:4318/v1/traces';
240
+ logInfo(chalk.blue(`Enabling remote tracing to: ${process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}`));
241
+ }
242
+
243
+ if (options.traceConsole) {
244
+ process.env.OTEL_ENABLE_CONSOLE = 'true';
245
+ logInfo(chalk.blue(`Enabling console tracing`));
246
+ }
247
+
248
+
249
+ // Handle custom prompt if provided
250
+ let customPrompt = null;
251
+ if (options.prompt) {
252
+ // Check if it's one of the predefined prompts
253
+ const predefinedPrompts = ['architect', 'code-review', 'code-review-template', 'support', 'engineer'];
254
+ if (predefinedPrompts.includes(options.prompt)) {
255
+ process.env.PROMPT_TYPE = options.prompt;
256
+ logInfo(chalk.blue(`Using predefined prompt: ${options.prompt}`));
257
+ } else {
258
+ // Check if it's a file path
259
+ try {
260
+ const promptPath = resolve(options.prompt);
261
+ if (existsSync(promptPath)) {
262
+ customPrompt = readFileSync(promptPath, 'utf8');
263
+ process.env.CUSTOM_PROMPT = customPrompt;
264
+ logInfo(chalk.blue(`Loaded custom prompt from file: ${promptPath}`));
265
+ } else {
266
+ // Not a predefined prompt or existing file, treat as a direct string prompt
267
+ customPrompt = options.prompt;
268
+ process.env.CUSTOM_PROMPT = customPrompt;
269
+ logInfo(chalk.blue(`Using custom prompt string`));
270
+ }
271
+ } catch (error) {
272
+ // If there's an error resolving the path, treat as a direct string prompt
273
+ customPrompt = options.prompt;
274
+ process.env.CUSTOM_PROMPT = customPrompt;
275
+ logInfo(chalk.blue(`Using custom prompt string`));
276
+ }
277
+ }
278
+ }
279
+
280
+ // Parse and validate allowed folders from environment variable
281
+ const allowedFolders = process.env.ALLOWED_FOLDERS
282
+ ? process.env.ALLOWED_FOLDERS.split(',').map(folder => folder.trim()).filter(Boolean)
283
+ : [];
284
+
285
+ // Resolve path argument to override ALLOWED_FOLDERS
286
+ if (pathArg) {
287
+ const resolvedPath = resolve(pathArg);
288
+ if (existsSync(resolvedPath)) {
289
+ const realPath = realpathSync(resolvedPath);
290
+ process.env.ALLOWED_FOLDERS = realPath;
291
+ logInfo(chalk.blue(`Using codebase path: ${realPath}`));
292
+ // Clear allowedFolders if pathArg overrides it
293
+ allowedFolders.length = 0;
294
+ allowedFolders.push(realPath);
295
+ } else {
296
+ logError(chalk.red(`Path does not exist: ${resolvedPath}`));
297
+ process.exit(1);
298
+ }
299
+ } else {
300
+ // Log allowed folders only if interactive or debug
301
+ logInfo('Configured search folders:');
302
+ for (const folder of allowedFolders) {
303
+ const exists = existsSync(folder);
304
+ logInfo(`- ${folder} ${exists ? '✓' : '✗ (not found)'}`);
305
+ if (!exists) {
306
+ logWarn(chalk.yellow(`Warning: Folder "${folder}" does not exist or is not accessible`));
307
+ }
308
+ }
309
+ if (allowedFolders.length === 0 && !isNonInteractive) { // Only warn if interactive
310
+ logWarn(chalk.yellow('No folders configured. Set ALLOWED_FOLDERS in .env file or provide a path argument.'));
311
+ }
312
+ }
313
+
314
+
315
+ // Set port for web server if specified
316
+ if (options.port) {
317
+ process.env.PORT = options.port;
318
+ }
319
+
320
+ // Check for API keys
321
+ const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
322
+ const openaiApiKey = process.env.OPENAI_API_KEY;
323
+ const googleApiKey = process.env.GOOGLE_API_KEY;
324
+ const hasApiKeys = !!(anthropicApiKey || openaiApiKey || googleApiKey);
325
+
326
+ // --- Web Mode (check before non-interactive to override) ---
327
+ if (options.web) {
328
+ if (!hasApiKeys) {
329
+ // Use logWarn for web mode warning
330
+ logWarn(chalk.yellow('Warning: No API key provided. The web interface will show instructions on how to set up API keys.'));
331
+ }
332
+ // Import and start web server
333
+ import('./webServer.js')
334
+ .then(async module => {
335
+ const { startWebServer } = module;
336
+ logInfo(`Starting web server on port ${process.env.PORT || 8080}...`);
337
+ await startWebServer(version, hasApiKeys, { allowEdit: options.allowEdit });
338
+ })
339
+ .catch(error => {
340
+ logError(chalk.red(`Error starting web server: ${error.message}`));
341
+ process.exit(1);
342
+ });
343
+ return; // Exit main function
344
+ }
345
+ // --- End Web Mode ---
346
+
347
+ // --- Non-Interactive Mode ---
348
+ if (isNonInteractive) {
349
+ if (!hasApiKeys) {
350
+ logError(chalk.red('No API key provided. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY environment variable.'));
351
+ process.exit(1);
352
+ }
353
+
354
+ let chat;
355
+ try {
356
+ // Pass session ID if provided, ProbeChat generates one otherwise
357
+ chat = new ProbeChat({
358
+ sessionId: options.sessionId,
359
+ isNonInteractive: true,
360
+ customPrompt: customPrompt,
361
+ promptType: options.prompt && ['architect', 'code-review', 'code-review-template', 'support', 'engineer'].includes(options.prompt) ? options.prompt : null,
362
+ allowEdit: options.allowEdit
363
+ });
364
+ // Model/Provider info is logged via logInfo above if debug enabled
365
+ logInfo(chalk.blue(`Using Session ID: ${chat.getSessionId()}`)); // Log the actual session ID being used
366
+ } catch (error) {
367
+ logError(chalk.red(`Initializing chat failed: ${error.message}`));
368
+ process.exit(1);
369
+ }
370
+
371
+ // Function to read from stdin
372
+ const readFromStdin = () => {
373
+ return new Promise((resolve) => {
374
+ let data = '';
375
+ process.stdin.on('data', (chunk) => {
376
+ data += chunk;
377
+ });
378
+ process.stdin.on('end', () => {
379
+ resolve(data.trim());
380
+ });
381
+ });
382
+ };
383
+
384
+ // Async function to handle the single chat request
385
+ const runNonInteractiveChat = async () => {
386
+ try {
387
+ // Get message from command line argument or stdin
388
+ let message = options.message;
389
+
390
+ // If no message argument but stdin is piped, read from stdin
391
+ if (!message && isPipedInput) {
392
+ logInfo('Reading message from stdin...'); // Log only if debug
393
+ message = await readFromStdin();
394
+ }
395
+
396
+ if (!message) {
397
+ logError('No message provided. Use --message option or pipe input to stdin.');
398
+ process.exit(1);
399
+ }
400
+
401
+ logInfo('Sending message...'); // Log only if debug
402
+ const result = await chat.chat(message, chat.getSessionId(), null, imageUrls); // Use the chat's current session ID and pass images
403
+
404
+ if (result && typeof result === 'object' && result.response !== undefined) {
405
+ if (options.json) {
406
+ const outputData = {
407
+ response: result.response,
408
+ sessionId: chat.getSessionId(),
409
+ tokenUsage: result.tokenUsage || null // Include usage if available
410
+ };
411
+ // Output JSON to stdout
412
+ rawLog(JSON.stringify(outputData, null, 2));
413
+ } else {
414
+ // Output raw response text to stdout
415
+ rawLog(result.response);
416
+ }
417
+ process.exit(0); // Success
418
+ } else if (typeof result === 'string') { // Handle simple string responses (e.g., cancellation message)
419
+ if (options.json) {
420
+ rawLog(JSON.stringify({ response: result, sessionId: chat.getSessionId(), tokenUsage: null }, null, 2));
421
+ } else {
422
+ rawLog(result);
423
+ }
424
+ process.exit(0); // Exit cleanly
425
+ }
426
+ else {
427
+ logError('Received an unexpected or empty response structure from chat.');
428
+ if (options.json) {
429
+ rawError(JSON.stringify({ error: 'Unexpected response structure', response: result, sessionId: chat.getSessionId() }, null, 2));
430
+ }
431
+ process.exit(1); // Error exit code
432
+ }
433
+ } catch (error) {
434
+ logError(`Chat request failed: ${error.message}`);
435
+ if (options.json) {
436
+ // Output JSON error to stderr
437
+ rawError(JSON.stringify({ error: error.message, sessionId: chat.getSessionId() }, null, 2));
438
+ }
439
+ process.exit(1); // Error exit code
440
+ }
441
+ };
442
+
443
+ runNonInteractiveChat();
444
+ return; // Exit main function, prevent interactive/web mode
445
+ }
446
+ // --- End Non-Interactive Mode ---
447
+
448
+
449
+ // --- Interactive CLI Mode ---
450
+ // (This block only runs if not non-interactive and not web mode)
451
+
452
+ if (!hasApiKeys) {
453
+ // Use logError and standard console.log for setup instructions
454
+ logError(chalk.red('No API key provided. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY environment variable.'));
455
+ console.log(chalk.cyan('You can find these instructions in the .env.example file:'));
456
+ console.log(chalk.cyan('1. Create a .env file by copying .env.example'));
457
+ console.log(chalk.cyan('2. Add your API key to the .env file'));
458
+ console.log(chalk.cyan('3. Restart the application'));
459
+ process.exit(1);
460
+ }
461
+
462
+ // Initialize ProbeChat for CLI mode
463
+ let chat;
464
+ try {
465
+ // Pass session ID if provided (though less common for interactive start)
466
+ chat = new ProbeChat({
467
+ sessionId: options.sessionId,
468
+ isNonInteractive: false,
469
+ customPrompt: customPrompt,
470
+ promptType: options.prompt && ['architect', 'code-review', 'code-review-template', 'support', 'engineer'].includes(options.prompt) ? options.prompt : null,
471
+ allowEdit: options.allowEdit
472
+ });
473
+
474
+ // Log model/provider info using logInfo
475
+ if (chat.apiType === 'anthropic') {
476
+ logInfo(chalk.green(`Using Anthropic API with model: ${chat.model}`));
477
+ } else if (chat.apiType === 'openai') {
478
+ logInfo(chalk.green(`Using OpenAI API with model: ${chat.model}`));
479
+ } else if (chat.apiType === 'google') {
480
+ logInfo(chalk.green(`Using Google API with model: ${chat.model}`));
481
+ }
482
+
483
+ logInfo(chalk.blue(`Session ID: ${chat.getSessionId()}`));
484
+ logInfo(chalk.cyan('Type "exit" or "quit" to end the chat'));
485
+ logInfo(chalk.cyan('Type "usage" to see token usage statistics'));
486
+ logInfo(chalk.cyan('Type "clear" to clear the chat history'));
487
+ logInfo(chalk.cyan('-------------------------------------------'));
488
+ } catch (error) {
489
+ logError(chalk.red(`Error initializing chat: ${error.message}`));
490
+ process.exit(1);
491
+ }
492
+
493
+ // Format AI response for interactive mode
494
+ function formatResponseInteractive(response) {
495
+ // Check if response is a structured object with response and tokenUsage properties
496
+ let textResponse = '';
497
+ if (response && typeof response === 'object' && 'response' in response) {
498
+ textResponse = response.response;
499
+ } else if (typeof response === 'string') {
500
+ // Fallback for legacy format or simple string response
501
+ textResponse = response;
502
+ } else {
503
+ return chalk.red('[Error: Invalid response format]');
504
+ }
505
+
506
+ // Apply formatting (e.g., highlighting tool calls)
507
+ return textResponse.replace(
508
+ /<tool_call>(.*?)<\/tool_call>/gs,
509
+ (match, toolCall) => chalk.magenta(`[Tool Call] ${toolCall}`)
510
+ );
511
+ }
512
+
513
+ // Main interactive chat loop
514
+ async function startChat() {
515
+ while (true) {
516
+ const { message } = await inquirer.prompt([
517
+ {
518
+ type: 'input',
519
+ name: 'message',
520
+ message: chalk.blue('You:'),
521
+ prefix: '',
522
+ },
523
+ ]);
524
+
525
+ if (message.toLowerCase() === 'exit' || message.toLowerCase() === 'quit') {
526
+ logInfo(chalk.yellow('Goodbye!'));
527
+ break;
528
+ } else if (message.toLowerCase() === 'usage') {
529
+ const usage = chat.getTokenUsage();
530
+ const display = new TokenUsageDisplay();
531
+ const formatted = display.format(usage);
532
+
533
+ // Use logInfo for usage details
534
+ logInfo(chalk.blue('Current:', formatted.current.total));
535
+ logInfo(chalk.blue('Context:', formatted.contextWindow));
536
+ logInfo(chalk.blue('Cache:',
537
+ `Read: ${formatted.current.cache.read},`,
538
+ `Write: ${formatted.current.cache.write},`,
539
+ `Total: ${formatted.current.cache.total}`));
540
+ logInfo(chalk.blue('Total:', formatted.total.total));
541
+
542
+ // Show context window in terminal title (only relevant for interactive)
543
+ process.stdout.write('\x1B]0;Context: ' + formatted.contextWindow + '\x07');
544
+ continue;
545
+ } else if (message.toLowerCase() === 'clear') {
546
+ const newSessionId = chat.clearHistory();
547
+ logInfo(chalk.yellow('Chat history cleared'));
548
+ logInfo(chalk.blue(`New session ID: ${newSessionId}`));
549
+ continue;
550
+ }
551
+
552
+ const spinner = ora('Thinking...').start(); // Spinner is ok for interactive mode
553
+ try {
554
+ const result = await chat.chat(message, null, null, imageUrls); // Uses internal session ID and pass images
555
+ spinner.stop();
556
+
557
+ logInfo(chalk.green('Assistant:'));
558
+ console.log(formatResponseInteractive(result)); // Use standard console.log for the actual response content
559
+ console.log(); // Add a newline for readability
560
+
561
+ // Update terminal title with context window size if available
562
+ if (result && typeof result === 'object' && result.tokenUsage && result.tokenUsage.contextWindow) {
563
+ process.stdout.write('\x1B]0;Context: ' + result.tokenUsage.contextWindow + '\x07');
564
+ }
565
+ } catch (error) {
566
+ spinner.stop();
567
+ logError(chalk.red(`Error: ${error.message}`)); // Use logError
568
+ }
569
+ }
570
+ }
571
+
572
+ startChat().catch((error) => {
573
+ logError(chalk.red(`Fatal error in interactive chat: ${error.message}`));
574
+ process.exit(1);
575
+ });
576
+ // --- End Interactive CLI Mode ---
577
+ }
578
+
579
+ // If this file is run directly, call main()
580
+ if (import.meta.url.startsWith('file:') && process.argv[1] === fileURLToPath(import.meta.url)) {
581
+ main();
582
+ }
package/logo.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@probelabs/probe-chat",
3
+ "version": "0.6.0-rc100",
4
+ "description": "CLI and web interface for Probe code search (formerly @probelabs/probe-web and @probelabs/probe-chat)",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "probe-chat": "./bin/probe-chat.js",
9
+ "probe-web": "./bin/probe-chat.js"
10
+ },
11
+ "scripts": {
12
+ "start": "node index.js",
13
+ "web": "node index.js --web",
14
+ "build": "node build.js",
15
+ "build:prod": "cross-env NODE_ENV=production node build.js",
16
+ "test": "cross-env NODE_ENV=test node --test test/integration/chatFlows.test.js test/integration/toolCalling.test.js",
17
+ "test:chat": "cross-env NODE_ENV=test node --test test/integration/chatFlows.test.js",
18
+ "test:tools": "cross-env NODE_ENV=test node --test test/integration/toolCalling.test.js",
19
+ "prepublishOnly": "chmod +x ./bin/probe-chat.js"
20
+ },
21
+ "keywords": [
22
+ "probe",
23
+ "code-search",
24
+ "chat",
25
+ "ai",
26
+ "cli",
27
+ "web"
28
+ ],
29
+ "author": "Leonid Bugaev",
30
+ "license": "Apache-2.0",
31
+ "dependencies": {
32
+ "@ai-sdk/anthropic": "^2.0.8",
33
+ "@ai-sdk/google": "^2.0.14",
34
+ "@ai-sdk/openai": "^2.0.10",
35
+ "@probelabs/probe": "latest",
36
+ "@opentelemetry/api": "^1.9.0",
37
+ "@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
38
+ "@opentelemetry/resources": "^2.0.1",
39
+ "@opentelemetry/sdk-node": "^0.203.0",
40
+ "@opentelemetry/semantic-conventions": "^1.36.0",
41
+ "@modelcontextprotocol/sdk": "^1.0.0",
42
+ "ai": "^5.0.0",
43
+ "chalk": "^5.3.0",
44
+ "commander": "^11.1.0",
45
+ "dotenv": "^16.4.7",
46
+ "glob": "^10.3.10",
47
+ "inquirer": "^9.2.12",
48
+ "ora": "^7.0.1",
49
+ "tiktoken": "^1.0.20",
50
+ "zod": "^3.24.2"
51
+ },
52
+ "devDependencies": {
53
+ "cross-env": "^7.0.3",
54
+ "esbuild": "^0.21.5"
55
+ },
56
+ "peerDependencies": {
57
+ "aider-chat": ">=0.20.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "aider-chat": {
61
+ "optional": true
62
+ }
63
+ },
64
+ "engines": {
65
+ "node": ">=18.0.0"
66
+ },
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "git+https://github.com/probelabs/probe.git"
70
+ },
71
+ "bugs": {
72
+ "url": "https://github.com/probelabs/probe/issues"
73
+ },
74
+ "homepage": "https://github.com/probelabs/probe#readme",
75
+ "publishConfig": {
76
+ "access": "public"
77
+ },
78
+ "files": [
79
+ "bin/",
80
+ "storage/",
81
+ "implement/",
82
+ "test/",
83
+ "index.js",
84
+ "probeChat.js",
85
+ "tokenCounter.js",
86
+ "tokenUsageDisplay.js",
87
+ "tools.js",
88
+ "webServer.js",
89
+ "auth.js",
90
+ "probeTool.js",
91
+ "cancelRequest.js",
92
+ "telemetry.js",
93
+ "fileSpanExporter.js",
94
+ "appTracer.js",
95
+ "index.html",
96
+ "logo.png",
97
+ "README.md",
98
+ "TRACING.md",
99
+ "LICENSE"
100
+ ]
101
+ }