@probelabs/probe 0.6.0-rc167 → 0.6.0-rc168

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.
@@ -11,6 +11,8 @@ export interface ProbeAgentOptions {
11
11
  sessionId?: string;
12
12
  /** Custom system prompt to replace the default system message */
13
13
  customPrompt?: string;
14
+ /** Alias for customPrompt. More intuitive naming for system prompts. */
15
+ systemPrompt?: string;
14
16
  /** Predefined prompt type (persona) */
15
17
  promptType?: 'code-explorer' | 'engineer' | 'code-review' | 'support' | 'architect';
16
18
  /** Allow the use of the 'implement' tool for code editing */
@@ -35,6 +37,10 @@ export interface ProbeAgentOptions {
35
37
  mcpConfig?: any;
36
38
  /** @deprecated Use mcpConfig instead */
37
39
  mcpServers?: any[];
40
+ /** List of allowed tool names. Use ['*'] for all tools (default), [] or null for no tools (raw AI mode), or specific tool names like ['search', 'query', 'extract']. Supports exclusion with '!' prefix (e.g., ['*', '!bash']). */
41
+ allowedTools?: string[] | null;
42
+ /** Convenience flag to disable all tools (equivalent to allowedTools: []). Takes precedence over allowedTools if set. */
43
+ disableTools?: boolean;
38
44
  /** Retry configuration for handling transient API failures */
39
45
  retry?: RetryOptions;
40
46
  /** Fallback configuration for multi-provider support */
@@ -227,4 +233,4 @@ export interface ProbeAgentEvents {
227
233
  }
228
234
 
229
235
  // Default export
230
- export { ProbeAgent as default };
236
+ export { ProbeAgent as default };
@@ -55,6 +55,7 @@ import {
55
55
  validateAndFixMermaidResponse
56
56
  } from './schemaUtils.js';
57
57
  import { removeThinkingTags } from './xmlParsingUtils.js';
58
+ import { predefinedPrompts } from './shared/prompts.js';
58
59
  import {
59
60
  MCPXmlBridge,
60
61
  parseHybridXmlToolCall,
@@ -89,6 +90,7 @@ export class ProbeAgent {
89
90
  * @param {Object} options - Configuration options
90
91
  * @param {string} [options.sessionId] - Optional session ID
91
92
  * @param {string} [options.customPrompt] - Custom prompt to replace the default system message
93
+ * @param {string} [options.systemPrompt] - Alias for customPrompt; takes precedence when both are provided
92
94
  * @param {string} [options.promptType] - Predefined prompt type (architect, code-review, support)
93
95
  * @param {boolean} [options.allowEdit=false] - Allow the use of the 'implement' tool
94
96
  * @param {boolean} [options.enableDelegate=false] - Enable the delegate tool for task distribution to subagents
@@ -125,7 +127,8 @@ export class ProbeAgent {
125
127
  constructor(options = {}) {
126
128
  // Basic configuration
127
129
  this.sessionId = options.sessionId || randomUUID();
128
- this.customPrompt = options.customPrompt || null;
130
+ // Support systemPrompt alias (overrides customPrompt when both are provided)
131
+ this.customPrompt = options.systemPrompt || options.customPrompt || null;
129
132
  this.promptType = options.promptType || 'code-explorer';
130
133
  this.allowEdit = !!options.allowEdit;
131
134
  this.enableDelegate = !!options.enableDelegate;
@@ -304,12 +307,14 @@ export class ProbeAgent {
304
307
  * This method initializes MCP and merges MCP tools into the tool list, and loads history from storage
305
308
  */
306
309
  async initialize() {
307
- // Check if we need to auto-detect claude-code provider
310
+ // Check if we need to auto-detect claude-code or codex provider
308
311
  // This happens when no API keys are set and no provider is specified
309
- if (!this.provider && !this.clientApiProvider && this.apiType !== 'claude-code') {
312
+ if (!this.provider && !this.clientApiProvider && this.apiType !== 'claude-code' && this.apiType !== 'codex') {
310
313
  // Check if initializeModel marked as uninitialized (no API keys)
311
314
  if (this.apiType === 'uninitialized') {
312
315
  const claudeAvailable = await this.isClaudeCommandAvailable();
316
+ const codexAvailable = await this.isCodexCommandAvailable();
317
+
313
318
  if (claudeAvailable) {
314
319
  if (this.debug) {
315
320
  console.log('[DEBUG] No API keys found, but claude command detected');
@@ -320,11 +325,22 @@ export class ProbeAgent {
320
325
  this.provider = null;
321
326
  this.model = this.clientApiModel || 'claude-3-5-sonnet-20241022';
322
327
  this.apiType = 'claude-code';
328
+ } else if (codexAvailable) {
329
+ if (this.debug) {
330
+ console.log('[DEBUG] No API keys found, but codex command detected');
331
+ console.log('[DEBUG] Auto-switching to codex provider');
332
+ }
333
+ // Set provider to codex
334
+ this.clientApiProvider = 'codex';
335
+ this.provider = null;
336
+ this.model = this.clientApiModel || 'gpt-4o';
337
+ this.apiType = 'codex';
323
338
  } else {
324
- // Neither API keys nor claude command available
325
- throw new Error('No API key provided and claude command not found. Please either:\n' +
339
+ // Neither API keys nor CLI commands available
340
+ throw new Error('No API key provided and neither claude nor codex command found. Please either:\n' +
326
341
  '1. Set an API key: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, or AWS credentials\n' +
327
- '2. Install claude command from https://docs.claude.com/en/docs/claude-code');
342
+ '2. Install claude command from https://docs.claude.com/en/docs/claude-code\n' +
343
+ '3. Install codex command from https://openai.com/codex');
328
344
  }
329
345
  }
330
346
  }
@@ -482,13 +498,34 @@ export class ProbeAgent {
482
498
 
483
499
  /**
484
500
  * Check if claude command is available on the system
501
+ * Uses execFile instead of exec to avoid shell injection risks
485
502
  * @returns {Promise<boolean>} True if claude command is available
486
503
  * @private
487
504
  */
488
505
  async isClaudeCommandAvailable() {
489
506
  try {
490
- const { execSync } = await import('child_process');
491
- execSync('claude --version', { stdio: 'ignore' });
507
+ const { execFile } = await import('child_process');
508
+ const { promisify } = await import('util');
509
+ const execFileAsync = promisify(execFile);
510
+ await execFileAsync('claude', ['--version'], { timeout: 5000 });
511
+ return true;
512
+ } catch (error) {
513
+ return false;
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Check if codex command is available on the system
519
+ * Uses execFile instead of exec to avoid shell injection risks
520
+ * @returns {Promise<boolean>} True if codex command is available
521
+ * @private
522
+ */
523
+ async isCodexCommandAvailable() {
524
+ try {
525
+ const { execFile } = await import('child_process');
526
+ const { promisify } = await import('util');
527
+ const execFileAsync = promisify(execFile);
528
+ await execFileAsync('codex', ['--version'], { timeout: 5000 });
492
529
  return true;
493
530
  } catch (error) {
494
531
  return false;
@@ -521,6 +558,25 @@ export class ProbeAgent {
521
558
  return;
522
559
  }
523
560
 
561
+ // Skip API key requirement for Codex CLI (uses built-in access in Codex CLI)
562
+ if (this.clientApiProvider === 'codex' || process.env.USE_CODEX === 'true') {
563
+ // Codex CLI engine will be initialized lazily in getEngine()
564
+ // Set minimal defaults for compatibility
565
+ this.provider = null;
566
+ // Only set model if explicitly provided, otherwise let Codex use account default
567
+ this.model = modelName || null;
568
+ this.apiType = 'codex';
569
+ if (this.debug) {
570
+ console.log('[DEBUG] Codex CLI engine selected - will use built-in access if available');
571
+ if (this.model) {
572
+ console.log(`[DEBUG] Using model: ${this.model}`);
573
+ } else {
574
+ console.log('[DEBUG] Using Codex account default model');
575
+ }
576
+ }
577
+ return;
578
+ }
579
+
524
580
  // Get API keys from environment variables
525
581
  // Support both ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN (used by Z.AI)
526
582
  const anthropicApiKey = process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN;
@@ -724,6 +780,60 @@ export class ProbeAgent {
724
780
  }
725
781
  }
726
782
 
783
+ // Check if we should use Codex engine
784
+ if (this.clientApiProvider === 'codex' || process.env.USE_CODEX === 'true') {
785
+ try {
786
+ const engine = await this.getEngine();
787
+ if (engine && engine.query) {
788
+ // Convert Vercel AI SDK format to engine format
789
+ // Extract the ORIGINAL user message as the main prompt (skip any warning messages)
790
+ // Look for user messages that are NOT the warning message
791
+ const userMessages = options.messages.filter(m =>
792
+ m.role === 'user' &&
793
+ !m.content.includes('WARNING: You have reached the maximum tool iterations limit')
794
+ );
795
+ const lastUserMessage = userMessages[userMessages.length - 1];
796
+ const prompt = lastUserMessage ? lastUserMessage.content : '';
797
+
798
+ // Pass system message and other options
799
+ const engineOptions = {
800
+ maxTokens: options.maxTokens,
801
+ temperature: options.temperature,
802
+ messages: options.messages,
803
+ systemPrompt: options.messages.find(m => m.role === 'system')?.content
804
+ };
805
+
806
+ // Get the engine's query result (async generator)
807
+ const engineStream = engine.query(prompt, engineOptions);
808
+
809
+ // Create a text stream that extracts text from engine messages
810
+ async function* createTextStream() {
811
+ for await (const message of engineStream) {
812
+ if (message.type === 'text' && message.content) {
813
+ yield message.content;
814
+ } else if (typeof message === 'string') {
815
+ // If engine returns plain strings, pass them through
816
+ yield message;
817
+ }
818
+ // Ignore other message types for the text stream
819
+ }
820
+ }
821
+
822
+ // Wrap the engine result to match streamText interface
823
+ return {
824
+ textStream: createTextStream(),
825
+ usage: Promise.resolve({}), // Engine should handle its own usage tracking
826
+ // Add other streamText-compatible properties as needed
827
+ };
828
+ }
829
+ } catch (error) {
830
+ if (this.debug) {
831
+ console.log(`[DEBUG] Failed to use Codex engine, falling back to Vercel:`, error.message);
832
+ }
833
+ // Fall through to use Vercel engine as fallback
834
+ }
835
+ }
836
+
727
837
  // Initialize retry manager if not already created
728
838
  if (!this.retryManager) {
729
839
  this.retryManager = new RetryManager({
@@ -908,6 +1018,39 @@ export class ProbeAgent {
908
1018
  this.clientApiProvider = null;
909
1019
  }
910
1020
  }
1021
+
1022
+ // Try Codex CLI engine if requested
1023
+ if (this.clientApiProvider === 'codex' || process.env.USE_CODEX === 'true') {
1024
+ try {
1025
+ const { createCodexEngine } = await import('./engines/codex.js');
1026
+
1027
+ // For Codex CLI, use a cleaner system prompt without XML formatting
1028
+ // since it has native MCP support for tools
1029
+ const systemPrompt = this.customPrompt || this.getCodexNativeSystemPrompt();
1030
+
1031
+ this.engine = await createCodexEngine({
1032
+ agent: this, // Pass reference to ProbeAgent for tool access
1033
+ systemPrompt: systemPrompt,
1034
+ customPrompt: this.customPrompt,
1035
+ sessionId: this.options?.sessionId,
1036
+ debug: this.debug,
1037
+ allowedTools: this.allowedTools, // Pass tool filtering configuration
1038
+ model: this.model // Pass model name (e.g., gpt-4o, o3, etc.)
1039
+ });
1040
+ if (this.debug) {
1041
+ console.log('[DEBUG] Using Codex CLI engine with Probe tools');
1042
+ if (this.customPrompt) {
1043
+ console.log('[DEBUG] Using custom prompt/persona');
1044
+ }
1045
+ }
1046
+ return this.engine;
1047
+ } catch (error) {
1048
+ console.warn('[WARNING] Failed to load Codex CLI engine:', error.message);
1049
+ console.warn('[WARNING] Falling back to Vercel AI SDK');
1050
+ this.clientApiProvider = null;
1051
+ }
1052
+ }
1053
+
911
1054
  // Default to enhanced Vercel AI SDK (wraps existing logic)
912
1055
  const { createEnhancedVercelEngine } = await import('./engines/enhanced-vercel.js');
913
1056
  this.engine = createEnhancedVercelEngine(this);
@@ -917,6 +1060,35 @@ export class ProbeAgent {
917
1060
  return this.engine;
918
1061
  }
919
1062
 
1063
+ /**
1064
+ * Get session information including thread ID for resumability
1065
+ * @returns {Object} Session info with sessionId, threadId, messageCount
1066
+ */
1067
+ getSessionInfo() {
1068
+ if (this.engine && this.engine.getSession) {
1069
+ return this.engine.getSession();
1070
+ }
1071
+ return {
1072
+ id: this.sessionId,
1073
+ threadId: null,
1074
+ messageCount: 0
1075
+ };
1076
+ }
1077
+
1078
+ /**
1079
+ * Close the agent and clean up resources (e.g., MCP servers)
1080
+ * @returns {Promise<void>}
1081
+ */
1082
+ async close() {
1083
+ if (this.engine && this.engine.close) {
1084
+ await this.engine.close();
1085
+ }
1086
+ if (this.mcpBridge) {
1087
+ // Clean up MCP bridge if needed
1088
+ this.mcpBridge = null;
1089
+ }
1090
+ }
1091
+
920
1092
  /**
921
1093
  * Process assistant response content and detect/load image references
922
1094
  * @param {string} content - The assistant's response content
@@ -1311,11 +1483,64 @@ export class ProbeAgent {
1311
1483
  let systemPrompt = '';
1312
1484
 
1313
1485
  // Add persona/role if configured
1314
- if (this.predefinedPrompt) {
1315
- const personaPrompt = getPromptByType(this.predefinedPrompt);
1316
- if (personaPrompt?.system) {
1317
- systemPrompt += personaPrompt.system + '\n\n';
1318
- }
1486
+ if (this.customPrompt) {
1487
+ systemPrompt += this.customPrompt + '\n\n';
1488
+ } else if (this.promptType && predefinedPrompts[this.promptType]) {
1489
+ systemPrompt += predefinedPrompts[this.promptType] + '\n\n';
1490
+ } else {
1491
+ // Use default code-explorer prompt
1492
+ systemPrompt += predefinedPrompts['code-explorer'] + '\n\n';
1493
+ }
1494
+
1495
+ // Add high-level instructions about when to use tools
1496
+ systemPrompt += `You have access to powerful code search and analysis tools through MCP:
1497
+ - search: Find code patterns using semantic search
1498
+ - extract: Extract specific code sections with context
1499
+ - query: Use AST patterns for structural code matching
1500
+ - listFiles: Browse directory contents
1501
+ - searchFiles: Find files by name patterns`;
1502
+
1503
+ if (this.enableBash) {
1504
+ systemPrompt += `\n- bash: Execute bash commands for system operations`;
1505
+ }
1506
+
1507
+ systemPrompt += `\n
1508
+ When exploring code:
1509
+ 1. Start with search to find relevant code patterns
1510
+ 2. Use extract to get detailed context when needed
1511
+ 3. Prefer focused, specific searches over broad queries
1512
+ 4. Combine multiple tools to build complete understanding`;
1513
+
1514
+ // Add workspace context
1515
+ if (this.allowedFolders && this.allowedFolders.length > 0) {
1516
+ systemPrompt += `\n\nWorkspace: ${this.allowedFolders.join(', ')}`;
1517
+ }
1518
+
1519
+ // Add repository structure if available
1520
+ if (this.fileList) {
1521
+ systemPrompt += `\n\n# Repository Structure\n`;
1522
+ systemPrompt += `You are working with a repository located at: ${this.allowedFolders[0]}\n\n`;
1523
+ systemPrompt += `Here's an overview of the repository structure (showing up to 100 most relevant files):\n\n`;
1524
+ systemPrompt += '```\n' + this.fileList + '\n```\n';
1525
+ }
1526
+
1527
+ return systemPrompt;
1528
+ }
1529
+
1530
+ /**
1531
+ * Get system prompt for Codex CLI (similar to Claude but optimized for Codex)
1532
+ */
1533
+ getCodexNativeSystemPrompt() {
1534
+ let systemPrompt = '';
1535
+
1536
+ // Add persona/role if configured
1537
+ if (this.customPrompt) {
1538
+ systemPrompt += this.customPrompt + '\n\n';
1539
+ } else if (this.promptType && predefinedPrompts[this.promptType]) {
1540
+ systemPrompt += predefinedPrompts[this.promptType] + '\n\n';
1541
+ } else {
1542
+ // Use default code-explorer prompt
1543
+ systemPrompt += predefinedPrompts['code-explorer'] + '\n\n';
1319
1544
  }
1320
1545
 
1321
1546
  // Add high-level instructions about when to use tools
@@ -1747,8 +1972,9 @@ When troubleshooting:
1747
1972
  const baseMaxIterations = this.maxIterations || MAX_TOOL_ITERATIONS;
1748
1973
  const maxIterations = options.schema ? baseMaxIterations + 4 : baseMaxIterations;
1749
1974
 
1750
- // Check if we're using Claude Code engine which handles its own agentic loop
1975
+ // Check if we're using CLI-based engines which handle their own agentic loop
1751
1976
  const isClaudeCode = this.clientApiProvider === 'claude-code' || process.env.USE_CLAUDE_CODE === 'true';
1977
+ const isCodex = this.clientApiProvider === 'codex' || process.env.USE_CODEX === 'true';
1752
1978
 
1753
1979
  if (isClaudeCode) {
1754
1980
  // For Claude Code, bypass the tool loop entirely - it handles its own internal dialogue
@@ -1820,6 +2046,77 @@ When troubleshooting:
1820
2046
  }
1821
2047
  }
1822
2048
 
2049
+ // Handle Codex engine (same pattern as Claude Code)
2050
+ if (isCodex) {
2051
+ // For Codex, bypass the tool loop entirely - it handles its own internal dialogue
2052
+ if (this.debug) {
2053
+ console.log(`[DEBUG] Using Codex engine - bypassing tool loop (black box mode)`);
2054
+ console.log(`[DEBUG] Sending question directly to Codex: ${message.substring(0, 100)}...`);
2055
+ }
2056
+
2057
+ // Send the message directly to Codex and collect the response
2058
+ try {
2059
+ const engine = await this.getEngine();
2060
+ if (engine && engine.query) {
2061
+ let assistantResponseContent = '';
2062
+ let toolBatch = null;
2063
+
2064
+ // Query Codex directly with the message and schema
2065
+ for await (const chunk of engine.query(message, options)) {
2066
+ if (chunk.type === 'text' && chunk.content) {
2067
+ assistantResponseContent += chunk.content;
2068
+ if (options.onStream) {
2069
+ options.onStream(chunk.content);
2070
+ }
2071
+ } else if (chunk.type === 'toolBatch' && chunk.tools) {
2072
+ // Store tool batch for processing after response
2073
+ toolBatch = chunk.tools;
2074
+ if (this.debug) {
2075
+ console.log(`[DEBUG] Received batch of ${chunk.tools.length} tool events from Codex`);
2076
+ }
2077
+ } else if (chunk.type === 'error') {
2078
+ throw chunk.error;
2079
+ }
2080
+ }
2081
+
2082
+ // Emit tool events after response is complete (batch mode)
2083
+ if (toolBatch && toolBatch.length > 0 && this.events) {
2084
+ if (this.debug) {
2085
+ console.log(`[DEBUG] Emitting ${toolBatch.length} tool events from Codex batch`);
2086
+ }
2087
+ for (const toolEvent of toolBatch) {
2088
+ this.events.emit('toolCall', toolEvent);
2089
+ }
2090
+ }
2091
+
2092
+ // Update history with the exchange
2093
+ this.history.push(userMessage);
2094
+ this.history.push({
2095
+ role: 'assistant',
2096
+ content: assistantResponseContent
2097
+ });
2098
+
2099
+ // Store conversation history
2100
+ // TODO: storeConversationHistory is not yet implemented for Codex
2101
+ // await this.storeConversationHistory(this.history, oldHistoryLength);
2102
+
2103
+ // Emit completion hook
2104
+ await this.hooks.emit(HOOK_TYPES.COMPLETION, {
2105
+ sessionId: this.sessionId,
2106
+ prompt: message,
2107
+ response: assistantResponseContent
2108
+ });
2109
+
2110
+ return assistantResponseContent;
2111
+ }
2112
+ } catch (error) {
2113
+ if (this.debug) {
2114
+ console.error('[DEBUG] Codex error:', error);
2115
+ }
2116
+ throw error;
2117
+ }
2118
+ }
2119
+
1823
2120
  if (this.debug) {
1824
2121
  console.log(`[DEBUG] Starting agentic flow for question: ${message.substring(0, 100)}...`);
1825
2122
  if (options.schema) {
@@ -1827,7 +2124,7 @@ When troubleshooting:
1827
2124
  }
1828
2125
  }
1829
2126
 
1830
- // Tool iteration loop (only for non-Claude Code engines)
2127
+ // Tool iteration loop (only for non-CLI engines like Vercel/Anthropic/OpenAI)
1831
2128
  while (currentIteration < maxIterations && !completionAttempted) {
1832
2129
  currentIteration++;
1833
2130
  if (this.cancelled) throw new Error('Request was cancelled by the user');