@probelabs/probe 0.6.0-rc166 → 0.6.0-rc167

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,595 @@
1
+ /**
2
+ * Enhanced Claude Code Engine with session management and better streaming
3
+ */
4
+
5
+ import { spawn } from 'child_process';
6
+ import { randomBytes } from 'crypto';
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import { EventEmitter } from 'events';
11
+ import { BuiltInMCPServer } from '../mcp/built-in-server.js';
12
+
13
+ /**
14
+ * Session manager for Claude Code conversations
15
+ */
16
+ class ClaudeSession {
17
+ constructor(id, debug = false) {
18
+ this.id = id;
19
+ this.conversationId = null;
20
+ this.messageCount = 0;
21
+ this.debug = debug;
22
+ }
23
+
24
+ /**
25
+ * Update session with Claude's conversation ID
26
+ */
27
+ setConversationId(convId) {
28
+ this.conversationId = convId;
29
+ if (this.debug) {
30
+ console.log(`[Session ${this.id}] Conversation ID: ${convId}`);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Get resume arguments for continuing conversation
36
+ */
37
+ getResumeArgs() {
38
+ if (this.conversationId && this.messageCount > 0) {
39
+ return ['--resume', this.conversationId];
40
+ }
41
+ return [];
42
+ }
43
+
44
+ incrementMessageCount() {
45
+ this.messageCount++;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Enhanced Claude Code Engine
51
+ */
52
+ export async function createEnhancedClaudeCLIEngine(options = {}) {
53
+ const { agent, systemPrompt, customPrompt, debug, sessionId, allowedTools } = options;
54
+
55
+ // Create or reuse session
56
+ const session = new ClaudeSession(
57
+ sessionId || randomBytes(8).toString('hex'),
58
+ debug
59
+ );
60
+
61
+ // Start built-in MCP server with ephemeral port
62
+ let mcpServer = null;
63
+ let mcpConfigPath = null;
64
+
65
+ if (agent) {
66
+ mcpServer = new BuiltInMCPServer(agent, {
67
+ port: 0, // Ephemeral port
68
+ host: '127.0.0.1',
69
+ debug: debug
70
+ });
71
+
72
+ const { host, port } = await mcpServer.start();
73
+
74
+ if (debug) {
75
+ console.log('[DEBUG] Built-in MCP server started');
76
+ console.log('[DEBUG] MCP URL:', `http://${host}:${port}/mcp`);
77
+ }
78
+
79
+ // Create MCP config for Claude Code to use
80
+ // Note: Claude Code currently requires spawning a process, not HTTP transport
81
+ // Keep built-in server running but also provide process-based config for CLI
82
+ mcpConfigPath = path.join(os.tmpdir(), `probe-mcp-${session.id}.json`);
83
+ const mcpConfig = {
84
+ mcpServers: {
85
+ probe: {
86
+ command: 'node',
87
+ args: [path.join(process.cwd(), 'mcp-probe-server.js')],
88
+ env: {
89
+ PROBE_WORKSPACE: process.cwd(),
90
+ DEBUG: debug ? 'true' : 'false'
91
+ }
92
+ }
93
+ }
94
+ };
95
+
96
+ await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
97
+ }
98
+
99
+ if (debug) {
100
+ console.log('[DEBUG] Enhanced Claude Code Engine');
101
+ console.log('[DEBUG] Session:', session.id);
102
+ console.log('[DEBUG] MCP Config:', mcpConfigPath);
103
+ }
104
+
105
+ // Combine prompts
106
+ const fullSystemPrompt = combinePrompts(systemPrompt, customPrompt, agent);
107
+
108
+ return {
109
+ sessionId: session.id,
110
+ session,
111
+
112
+ /**
113
+ * Query Claude with advanced streaming
114
+ */
115
+ async *query(prompt, opts = {}) {
116
+ const emitter = new EventEmitter();
117
+ let buffer = '';
118
+ let processEnded = false;
119
+ let currentToolCall = null;
120
+ let isSchemaMode = false;
121
+
122
+ // Check if this is a schema reminder or validation request
123
+ // In these cases, we treat Claude Code as a black box - just get the response
124
+ if (opts.schema || prompt.includes('JSON schema') || prompt.includes('mermaid diagram')) {
125
+ isSchemaMode = true;
126
+ if (debug) {
127
+ console.log('[DEBUG] Schema/validation mode - treating as black box');
128
+ }
129
+ }
130
+
131
+ // For schema mode, append the schema requirement to the prompt
132
+ let finalPrompt = prompt;
133
+ if (opts.schema && isSchemaMode) {
134
+ finalPrompt = `${prompt}\n\nPlease provide your response in the following JSON format:\n${opts.schema}`;
135
+ }
136
+
137
+ // Build command arguments
138
+ const args = buildClaudeArgs({
139
+ systemPrompt: fullSystemPrompt,
140
+ mcpConfigPath,
141
+ session,
142
+ debug,
143
+ prompt: finalPrompt, // Use finalPrompt which may include schema
144
+ allowedTools: allowedTools || opts.allowedTools // Support tool filtering
145
+ });
146
+
147
+ if (debug) {
148
+ console.log('[DEBUG] Executing: claude', args.join(' '));
149
+ }
150
+
151
+ // CRITICAL: Claude Code requires echo pipe to work when spawned from Node.js
152
+ // Without it, the process hangs indefinitely waiting for stdin
153
+ // This has been tested extensively - see CLAUDE_CLI_ECHO_REQUIREMENT.md
154
+ // DO NOT REMOVE THE echo "" | PREFIX
155
+ // SECURITY: Shell argument escaping using POSIX single-quote method
156
+ // Single quotes in POSIX shells protect ALL metacharacters (;|&$`><*?) except single quote itself
157
+ // The pattern 'text'\''more' correctly handles embedded quotes by:
158
+ // 1. Closing the current quote with '
159
+ // 2. Adding an escaped quote \'
160
+ // 3. Opening a new quote with '
161
+ // This is the standard POSIX-compliant method and is completely safe against injection
162
+ const shellCmd = `echo "" | claude ${args.map(arg => {
163
+ // Validate arg is a string (paranoid check)
164
+ if (typeof arg !== 'string') {
165
+ throw new TypeError(`Invalid argument type: expected string, got ${typeof arg}`);
166
+ }
167
+ // Escape single quotes using POSIX method: ' -> '\''
168
+ const escaped = arg.replace(/'/g, "'\\''");
169
+ // Wrap in single quotes for complete shell metacharacter protection
170
+ return `'${escaped}'`;
171
+ }).join(' ')}`;
172
+
173
+ if (debug) {
174
+ console.log('[DEBUG] Shell command length:', shellCmd.length);
175
+ // Don't log full command if too long (e.g. system prompt)
176
+ if (shellCmd.length < 500) {
177
+ console.log('[DEBUG] Shell command:', shellCmd);
178
+ } else {
179
+ console.log('[DEBUG] Shell command (truncated):', shellCmd.substring(0, 200) + '...');
180
+ }
181
+ }
182
+
183
+ // Initialize tool collector for batch emission
184
+ const toolCollector = [];
185
+
186
+ // Spawn using shell wrapper with echo pipe
187
+ const proc = spawn('sh', ['-c', shellCmd], {
188
+ env: { ...process.env, FORCE_COLOR: '0' },
189
+ stdio: ['ignore', 'pipe', 'pipe'] // Ignore stdin since echo handles it
190
+ });
191
+
192
+ // Handle stdout
193
+ proc.stdout.on('data', (data) => {
194
+ buffer += data.toString();
195
+ processJsonBuffer(buffer, emitter, session, debug, toolCollector);
196
+
197
+ // Keep only incomplete line in buffer
198
+ const lines = buffer.split('\n');
199
+ buffer = lines[lines.length - 1] || '';
200
+ });
201
+
202
+ // Handle stderr
203
+ proc.stderr.on('data', (data) => {
204
+ const stderr = data.toString();
205
+ if (debug) {
206
+ console.error('[STDERR]', stderr);
207
+ }
208
+
209
+ // Check for important errors
210
+ if (stderr.includes('command not found')) {
211
+ emitter.emit('error', new Error('Claude Code not found. Please install it first.'));
212
+ }
213
+ });
214
+
215
+ // Handle process end
216
+ proc.on('close', (code) => {
217
+ processEnded = true;
218
+ if (code !== 0 && debug) {
219
+ console.log(`[DEBUG] Process exited with code ${code}`);
220
+ }
221
+
222
+ // Process any remaining buffer
223
+ if (buffer.trim()) {
224
+ processJsonBuffer(buffer, emitter, session, debug, toolCollector);
225
+ }
226
+
227
+ // Emit collected tool events as batch
228
+ if (toolCollector.length > 0) {
229
+ emitter.emit('toolBatch', {
230
+ tools: toolCollector,
231
+ timestamp: new Date().toISOString()
232
+ });
233
+
234
+ if (debug) {
235
+ console.log(`[DEBUG] Emitting batch of ${toolCollector.length} tool events`);
236
+ }
237
+ }
238
+
239
+ emitter.emit('end');
240
+ });
241
+
242
+ proc.on('error', (error) => {
243
+ emitter.emit('error', error);
244
+ });
245
+
246
+ // Stream generator
247
+ const messageQueue = [];
248
+ let resolver = null;
249
+
250
+ emitter.on('message', (msg) => {
251
+ messageQueue.push(msg);
252
+ if (resolver) {
253
+ resolver();
254
+ resolver = null;
255
+ }
256
+ });
257
+
258
+ emitter.on('toolBatch', (batch) => {
259
+ messageQueue.push({ type: 'toolBatch', ...batch });
260
+ if (resolver) {
261
+ resolver();
262
+ resolver = null;
263
+ }
264
+ });
265
+
266
+ emitter.on('end', () => {
267
+ processEnded = true;
268
+ if (resolver) {
269
+ resolver();
270
+ resolver = null;
271
+ }
272
+ });
273
+
274
+ emitter.on('error', (error) => {
275
+ messageQueue.push({ type: 'error', error });
276
+ if (resolver) {
277
+ resolver();
278
+ resolver = null;
279
+ }
280
+ });
281
+
282
+ // Process messages
283
+ while (!processEnded || messageQueue.length > 0) {
284
+ if (messageQueue.length > 0) {
285
+ const msg = messageQueue.shift();
286
+
287
+ if (msg.type === 'text') {
288
+ yield { type: 'text', content: msg.content };
289
+ } else if (msg.type === 'tool_use') {
290
+ // Start tool execution
291
+ currentToolCall = msg;
292
+ yield {
293
+ type: 'text',
294
+ content: `\n🔧 Using ${msg.name}: ${JSON.stringify(msg.input)}\n`
295
+ };
296
+
297
+ // Execute tool
298
+ const result = await executeProbleTool(agent, msg.name, msg.input);
299
+ yield { type: 'text', content: `${result}\n` };
300
+ } else if (msg.type === 'toolBatch') {
301
+ // Pass through the tool batch for ProbeAgent to emit
302
+ yield { type: 'toolBatch', tools: msg.tools, timestamp: msg.timestamp };
303
+ } else if (msg.type === 'session_update') {
304
+ // Session was updated with conversation ID
305
+ if (debug) {
306
+ console.log('[DEBUG] Session updated:', msg.conversationId);
307
+ }
308
+ } else if (msg.type === 'error') {
309
+ yield { type: 'error', error: msg.error };
310
+ break;
311
+ }
312
+ } else if (!processEnded) {
313
+ // Wait for more messages
314
+ await new Promise(resolve => {
315
+ resolver = resolve;
316
+ });
317
+ }
318
+ }
319
+
320
+ // Increment message count
321
+ session.incrementMessageCount();
322
+
323
+ // Return session info for potential resume
324
+ yield {
325
+ type: 'metadata',
326
+ data: {
327
+ sessionId: session.id,
328
+ conversationId: session.conversationId,
329
+ messageCount: session.messageCount
330
+ }
331
+ };
332
+ },
333
+
334
+ /**
335
+ * Get session info
336
+ */
337
+ getSession() {
338
+ return {
339
+ id: session.id,
340
+ conversationId: session.conversationId,
341
+ messageCount: session.messageCount
342
+ };
343
+ },
344
+
345
+ /**
346
+ * Clean up - MUST be called to stop MCP server and clean resources
347
+ */
348
+ async close() {
349
+ try {
350
+ // Stop built-in MCP server
351
+ if (mcpServer) {
352
+ await mcpServer.stop();
353
+ if (debug) {
354
+ console.log('[DEBUG] Built-in MCP server stopped');
355
+ }
356
+ }
357
+
358
+ // Remove temporary MCP config file
359
+ if (mcpConfigPath) {
360
+ await fs.unlink(mcpConfigPath).catch(() => {});
361
+ if (debug) {
362
+ console.log('[DEBUG] MCP config file removed');
363
+ }
364
+ }
365
+
366
+ if (debug) {
367
+ console.log('[DEBUG] Engine closed, session:', session.id);
368
+ }
369
+ } catch (error) {
370
+ if (debug) {
371
+ console.error('[DEBUG] Error during cleanup:', error.message);
372
+ }
373
+ }
374
+ }
375
+ };
376
+ }
377
+
378
+ /**
379
+ * Process JSON buffer and emit messages
380
+ */
381
+ function processJsonBuffer(buffer, emitter, session, debug, toolCollector = null) {
382
+ const lines = buffer.split('\n');
383
+
384
+ for (const line of lines) {
385
+ if (!line.trim()) continue;
386
+
387
+ try {
388
+ const parsed = JSON.parse(line);
389
+
390
+ // Claude Code might return an array of messages
391
+ const messages = Array.isArray(parsed) ? parsed : [parsed];
392
+
393
+ for (const msg of messages) {
394
+
395
+ switch (msg.type) {
396
+ case 'result':
397
+ // Claude Code returns a complete result object
398
+ if (msg.result) {
399
+ emitter.emit('message', { type: 'text', content: msg.result });
400
+ }
401
+ if (msg.session_id) {
402
+ session.setConversationId(msg.session_id);
403
+ emitter.emit('message', { type: 'session_update', conversationId: msg.session_id });
404
+ }
405
+ break;
406
+
407
+ case 'conversation':
408
+ session.setConversationId(msg.id);
409
+ emitter.emit('message', { type: 'session_update', conversationId: msg.id });
410
+ break;
411
+
412
+ case 'text':
413
+ if (msg.text) {
414
+ emitter.emit('message', { type: 'text', content: msg.text });
415
+ }
416
+ break;
417
+
418
+ case 'assistant':
419
+ // Claude Code emits assistant messages when using internal agents/tools
420
+ if (msg.message && msg.message.content) {
421
+ // Extract text from the content array
422
+ for (const content of msg.message.content) {
423
+ if (content.type === 'text' && content.text) {
424
+ emitter.emit('message', { type: 'text', content: content.text });
425
+ } else if (content.type === 'tool_use') {
426
+ // Collect tool call for batch emission
427
+ if (toolCollector) {
428
+ toolCollector.push({
429
+ timestamp: new Date().toISOString(),
430
+ name: content.name,
431
+ args: content.input || {},
432
+ id: content.id,
433
+ status: 'started'
434
+ });
435
+ }
436
+ // Internal tool use - already handled by Claude Code
437
+ if (debug) {
438
+ console.log('[DEBUG] Assistant internal tool use:', content.name);
439
+ }
440
+ }
441
+ }
442
+ }
443
+ break;
444
+
445
+ case 'tool_use':
446
+ // Collect tool call for batch emission
447
+ if (toolCollector) {
448
+ toolCollector.push({
449
+ timestamp: new Date().toISOString(),
450
+ name: msg.name,
451
+ args: msg.input || {},
452
+ id: msg.id,
453
+ status: 'started'
454
+ });
455
+ }
456
+ emitter.emit('message', {
457
+ type: 'tool_use',
458
+ id: msg.id,
459
+ name: msg.name,
460
+ input: msg.input
461
+ });
462
+ break;
463
+
464
+ case 'tool_result':
465
+ // Mark tool as completed in collector
466
+ if (toolCollector && msg.tool_use_id) {
467
+ // Find the matching tool call and update its status
468
+ const toolCall = toolCollector.find(t => t.id === msg.tool_use_id);
469
+ if (toolCall) {
470
+ toolCall.status = 'completed';
471
+ toolCall.resultPreview = msg.content ?
472
+ (typeof msg.content === 'string' ?
473
+ msg.content.substring(0, 200) :
474
+ JSON.stringify(msg.content).substring(0, 200)) + '...' :
475
+ 'No Result';
476
+ }
477
+ }
478
+ // Tool results are handled internally
479
+ if (debug) {
480
+ console.log('[DEBUG] Tool result:', msg);
481
+ }
482
+ break;
483
+
484
+ case 'error':
485
+ emitter.emit('error', new Error(msg.message || 'Unknown error'));
486
+ break;
487
+
488
+ default:
489
+ if (debug) {
490
+ console.log('[DEBUG] Unknown message type:', msg.type);
491
+ console.log('[DEBUG] Full message:', JSON.stringify(msg).substring(0, 200));
492
+ }
493
+ }
494
+ } // Close inner for loop for messages array
495
+ } catch (e) {
496
+ // Not valid JSON, might be partial
497
+ if (debug && line.trim()) {
498
+ console.log('[DEBUG] Non-JSON output:', line);
499
+ }
500
+ }
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Build claude command arguments
506
+ */
507
+ function buildClaudeArgs({ systemPrompt, mcpConfigPath, session, debug, prompt, allowedTools }) {
508
+ const args = [
509
+ '-p', // Short form of --print
510
+ prompt, // The prompt text goes right after -p
511
+ '--output-format', 'json'
512
+ ];
513
+
514
+ // Add session resume if available
515
+ const resumeArgs = session.getResumeArgs();
516
+ if (resumeArgs.length > 0) {
517
+ args.push(...resumeArgs);
518
+ }
519
+
520
+ // Add system prompt
521
+ if (systemPrompt) {
522
+ args.push('--system-prompt', systemPrompt);
523
+ }
524
+
525
+ // Add MCP config
526
+ args.push('--mcp-config', mcpConfigPath);
527
+
528
+ // Add allowed tools filter if specified
529
+ // If no filter specified, allow all probe tools
530
+ if (allowedTools && Array.isArray(allowedTools) && allowedTools.length > 0) {
531
+ // Convert tool names to MCP format: mcp__probe__<toolname>
532
+ const mcpTools = allowedTools.map(tool =>
533
+ tool.startsWith('mcp__') ? tool : `mcp__probe__${tool}`
534
+ ).join(',');
535
+ args.push('--allowedTools', mcpTools);
536
+ } else {
537
+ // Default: allow all probe tools
538
+ args.push('--allowedTools', 'mcp__probe__*');
539
+ }
540
+
541
+ // Add debug flag
542
+ if (debug) {
543
+ args.push('--verbose');
544
+ }
545
+
546
+ return args;
547
+ }
548
+
549
+ /**
550
+ * Execute Probe tool through agent
551
+ */
552
+ async function executeProbleTool(agent, toolName, params) {
553
+ if (!agent || !agent.toolImplementations) {
554
+ return 'Tool execution not available';
555
+ }
556
+
557
+ // Remove MCP prefix: mcp__probe__<toolname> -> <toolname>
558
+ const name = toolName.replace(/^mcp__probe__/, '');
559
+ const tool = agent.toolImplementations[name];
560
+
561
+ if (!tool) {
562
+ return `Unknown tool: ${name}`;
563
+ }
564
+
565
+ try {
566
+ const result = await tool.execute(params);
567
+ return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
568
+ } catch (error) {
569
+ return `Tool error: ${error.message}`;
570
+ }
571
+ }
572
+
573
+ // Old createEnhancedMCPConfig function removed - now using built-in MCP server
574
+
575
+ /**
576
+ * Combine prompts intelligently
577
+ */
578
+ function combinePrompts(systemPrompt, customPrompt, agent) {
579
+ // For Claude Code, the systemPrompt already contains all necessary instructions
580
+ // from getClaudeNativeSystemPrompt(), so we don't need to add a base prompt
581
+
582
+ // If only customPrompt is provided (no systemPrompt), use it as the main prompt
583
+ if (!systemPrompt && customPrompt) {
584
+ return customPrompt;
585
+ }
586
+
587
+ // If systemPrompt is provided, it's already complete from getClaudeNativeSystemPrompt
588
+ // Just add customPrompt if available
589
+ if (systemPrompt && customPrompt) {
590
+ return systemPrompt + '\n\n## Additional Instructions\n' + customPrompt;
591
+ }
592
+
593
+ // Return systemPrompt as-is if no customPrompt
594
+ return systemPrompt || '';
595
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Enhanced Vercel AI SDK Engine with proper tool and prompt support
3
+ */
4
+
5
+ import { streamText } from 'ai';
6
+
7
+ /**
8
+ * Create an enhanced Vercel AI SDK engine with full tool support
9
+ * @param {Object} agent - The ProbeAgent instance
10
+ * @returns {Object} Engine interface
11
+ */
12
+ export function createEnhancedVercelEngine(agent) {
13
+ return {
14
+ /**
15
+ * Query the model using existing Vercel AI SDK implementation
16
+ * @param {string} prompt - The prompt to send
17
+ * @param {Object} options - Additional options
18
+ * @returns {AsyncIterable} Response stream
19
+ */
20
+ async *query(prompt, options = {}) {
21
+ // Get the system message with tools embedded (existing behavior)
22
+ const systemMessage = await agent.getSystemMessage();
23
+
24
+ // Build messages array with system prompt
25
+ const messages = [
26
+ { role: 'system', content: systemMessage },
27
+ ...agent.history,
28
+ { role: 'user', content: prompt }
29
+ ];
30
+
31
+ // Use existing streamText with retry and fallback
32
+ const result = await agent.streamTextWithRetryAndFallback({
33
+ model: agent.provider(agent.model),
34
+ messages,
35
+ maxTokens: options.maxTokens || agent.maxResponseTokens,
36
+ temperature: options.temperature || 0.3,
37
+ // Note: Vercel AI SDK doesn't use structured tools for XML format
38
+ // The tools are embedded in the system prompt
39
+ experimental_telemetry: options.telemetry
40
+ });
41
+
42
+ // Stream the response
43
+ let fullContent = '';
44
+ for await (const chunk of result.textStream) {
45
+ fullContent += chunk;
46
+ yield { type: 'text', content: chunk };
47
+ }
48
+
49
+ // Parse XML tool calls from the response if any
50
+ // This maintains compatibility with existing XML tool format
51
+ const toolCalls = agent.parseXmlToolCalls ? agent.parseXmlToolCalls(fullContent) : null;
52
+ if (toolCalls && toolCalls.length > 0) {
53
+ yield { type: 'tool_calls', toolCalls };
54
+ }
55
+
56
+ // Handle finish reason
57
+ if (result.finishReason) {
58
+ yield { type: 'finish', reason: result.finishReason };
59
+ }
60
+ },
61
+
62
+ /**
63
+ * Get available tools for this engine
64
+ */
65
+ getTools() {
66
+ return agent.toolImplementations || {};
67
+ },
68
+
69
+ /**
70
+ * Get system prompt for this engine
71
+ */
72
+ async getSystemPrompt() {
73
+ return agent.getSystemMessage();
74
+ },
75
+
76
+ /**
77
+ * Optional cleanup
78
+ */
79
+ async close() {
80
+ // Nothing to cleanup for Vercel AI
81
+ }
82
+ };
83
+ }