UNPKG

@vibe-kit/grok-cli

Version:

An open-source AI agent that brings the power of Grok directly into your terminal.

584 lines (572 loc) 27.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GrokAgent = void 0; const client_1 = require("../grok/client"); const tools_1 = require("../grok/tools"); const config_1 = require("../mcp/config"); const tools_2 = require("../tools"); const events_1 = require("events"); const token_counter_1 = require("../utils/token-counter"); const custom_instructions_1 = require("../utils/custom-instructions"); const settings_manager_1 = require("../utils/settings-manager"); class GrokAgent extends events_1.EventEmitter { constructor(apiKey, baseURL, model) { super(); this.chatHistory = []; this.messages = []; this.abortController = null; this.mcpInitialized = false; const manager = (0, settings_manager_1.getSettingsManager)(); const savedModel = manager.getCurrentModel(); const modelToUse = model || savedModel || "grok-4-latest"; this.grokClient = new client_1.GrokClient(apiKey, modelToUse, baseURL); this.textEditor = new tools_2.TextEditorTool(); this.bash = new tools_2.BashTool(); this.todoTool = new tools_2.TodoTool(); this.confirmationTool = new tools_2.ConfirmationTool(); this.search = new tools_2.SearchTool(); this.tokenCounter = (0, token_counter_1.createTokenCounter)(modelToUse); // Initialize MCP servers if configured this.initializeMCP(); // Load custom instructions const customInstructions = (0, custom_instructions_1.loadCustomInstructions)(); const customInstructionsSection = customInstructions ? `\n\nCUSTOM INSTRUCTIONS:\n${customInstructions}\n\nThe above custom instructions should be followed alongside the standard instructions below.` : ""; // Initialize with system message this.messages.push({ role: "system", content: `You are Grok CLI, an AI assistant that helps with file editing, coding tasks, and system operations.${customInstructionsSection} You have access to these tools: - view_file: View file contents or directory listings - create_file: Create new files with content (ONLY use this for files that don't exist yet) - str_replace_editor: Replace text in existing files (ALWAYS use this to edit or update existing files) - bash: Execute bash commands (use for searching, file discovery, navigation, and system operations) - search: Unified search tool for finding text content or files (similar to Cursor's search functionality) - create_todo_list: Create a visual todo list for planning and tracking tasks - update_todo_list: Update existing todos in your todo list REAL-TIME INFORMATION: You have access to real-time web search and X (Twitter) data. When users ask for current information, latest news, or recent events, you automatically have access to up-to-date information from the web and social media. IMPORTANT TOOL USAGE RULES: - NEVER use create_file on files that already exist - this will overwrite them completely - ALWAYS use str_replace_editor to modify existing files, even for small changes - Before editing a file, use view_file to see its current contents - Use create_file ONLY when creating entirely new files that don't exist SEARCHING AND EXPLORATION: - Use search for fast, powerful text search across files or finding files by name (unified search tool) - Examples: search for text content like "import.*react", search for files like "component.tsx" - Use bash with commands like 'find', 'grep', 'rg', 'ls' for complex file operations and navigation - view_file is best for reading specific files you already know exist When a user asks you to edit, update, modify, or change an existing file: 1. First use view_file to see the current contents 2. Then use str_replace_editor to make the specific changes 3. Never use create_file for existing files When a user asks you to create a new file that doesn't exist: 1. Use create_file with the full content TASK PLANNING WITH TODO LISTS: - For complex requests with multiple steps, ALWAYS create a todo list first to plan your approach - Use create_todo_list to break down tasks into manageable items with priorities - Mark tasks as 'in_progress' when you start working on them (only one at a time) - Mark tasks as 'completed' immediately when finished - Use update_todo_list to track your progress throughout the task - Todo lists provide visual feedback with colors: ✅ Green (completed), 🔄 Cyan (in progress), ⏳ Yellow (pending) - Always create todos with priorities: 'high' (🔴), 'medium' (🟡), 'low' (🟢) USER CONFIRMATION SYSTEM: File operations (create_file, str_replace_editor) and bash commands will automatically request user confirmation before execution. The confirmation system will show users the actual content or command before they decide. Users can choose to approve individual operations or approve all operations of that type for the session. If a user rejects an operation, the tool will return an error and you should not proceed with that specific operation. Be helpful, direct, and efficient. Always explain what you're doing and show the results. IMPORTANT RESPONSE GUIDELINES: - After using tools, do NOT respond with pleasantries like "Thanks for..." or "Great!" - Only provide necessary explanations or next steps if relevant to the task - Keep responses concise and focused on the actual work being done - If a tool execution completes the user's request, you can remain silent or give a brief confirmation Current working directory: ${process.cwd()}`, }); } async initializeMCP() { try { const config = (0, config_1.loadMCPConfig)(); if (config.servers.length > 0) { console.log(`Found ${config.servers.length} MCP server(s) - connecting now...`); await (0, tools_1.initializeMCPServers)(); console.log(`Successfully connected to MCP servers`); } this.mcpInitialized = true; } catch (error) { console.warn("Failed to initialize MCP servers:", error); this.mcpInitialized = true; // Don't block if MCP fails } } async waitForMCPInitialization() { while (!this.mcpInitialized) { await new Promise((resolve) => setTimeout(resolve, 100)); } } isGrokModel() { const currentModel = this.grokClient.getCurrentModel(); return currentModel.toLowerCase().includes("grok"); } async processUserMessage(message) { // Wait for MCP initialization before processing await this.waitForMCPInitialization(); // Add user message to conversation const userEntry = { type: "user", content: message, timestamp: new Date(), }; this.chatHistory.push(userEntry); this.messages.push({ role: "user", content: message }); const newEntries = [userEntry]; const maxToolRounds = 10; // Prevent infinite loops let toolRounds = 0; try { const tools = await (0, tools_1.getAllGrokTools)(); let currentResponse = await this.grokClient.chat(this.messages, tools, undefined, this.isGrokModel() ? { search_parameters: { mode: "auto" } } : undefined); // Agent loop - continue until no more tool calls or max rounds reached while (toolRounds < maxToolRounds) { const assistantMessage = currentResponse.choices[0]?.message; if (!assistantMessage) { throw new Error("No response from Grok"); } // Handle tool calls if (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) { toolRounds++; // Add assistant message with tool calls const assistantEntry = { type: "assistant", content: assistantMessage.content || "Using tools to help you...", timestamp: new Date(), toolCalls: assistantMessage.tool_calls, }; this.chatHistory.push(assistantEntry); newEntries.push(assistantEntry); // Add assistant message to conversation this.messages.push({ role: "assistant", content: assistantMessage.content || "", tool_calls: assistantMessage.tool_calls, }); // Create initial tool call entries to show tools are being executed assistantMessage.tool_calls.forEach((toolCall) => { const toolCallEntry = { type: "tool_call", content: "Executing...", timestamp: new Date(), toolCall: toolCall, }; this.chatHistory.push(toolCallEntry); newEntries.push(toolCallEntry); }); // Execute tool calls and update the entries for (const toolCall of assistantMessage.tool_calls) { const result = await this.executeTool(toolCall); // Update the existing tool_call entry with the result const entryIndex = this.chatHistory.findIndex((entry) => entry.type === "tool_call" && entry.toolCall?.id === toolCall.id); if (entryIndex !== -1) { const updatedEntry = { ...this.chatHistory[entryIndex], type: "tool_result", content: result.success ? result.output || "Success" : result.error || "Error occurred", toolResult: result, }; this.chatHistory[entryIndex] = updatedEntry; // Also update in newEntries for return value const newEntryIndex = newEntries.findIndex((entry) => entry.type === "tool_call" && entry.toolCall?.id === toolCall.id); if (newEntryIndex !== -1) { newEntries[newEntryIndex] = updatedEntry; } } // Add tool result to messages with proper format (needed for AI context) this.messages.push({ role: "tool", content: result.success ? result.output || "Success" : result.error || "Error", tool_call_id: toolCall.id, }); } // Get next response - this might contain more tool calls currentResponse = await this.grokClient.chat(this.messages, tools, undefined, this.isGrokModel() ? { search_parameters: { mode: "auto" } } : undefined); } else { // No more tool calls, add final response const finalEntry = { type: "assistant", content: assistantMessage.content || "I understand, but I don't have a specific response.", timestamp: new Date(), }; this.chatHistory.push(finalEntry); this.messages.push({ role: "assistant", content: assistantMessage.content || "", }); newEntries.push(finalEntry); break; // Exit the loop } } if (toolRounds >= maxToolRounds) { const warningEntry = { type: "assistant", content: "Maximum tool execution rounds reached. Stopping to prevent infinite loops.", timestamp: new Date(), }; this.chatHistory.push(warningEntry); newEntries.push(warningEntry); } return newEntries; } catch (error) { const errorEntry = { type: "assistant", content: `Sorry, I encountered an error: ${error.message}`, timestamp: new Date(), }; this.chatHistory.push(errorEntry); return [userEntry, errorEntry]; } } messageReducer(previous, item) { const reduce = (acc, delta) => { acc = { ...acc }; for (const [key, value] of Object.entries(delta)) { if (acc[key] === undefined || acc[key] === null) { acc[key] = value; // Clean up index properties from tool calls if (Array.isArray(acc[key])) { for (const arr of acc[key]) { delete arr.index; } } } else if (typeof acc[key] === "string" && typeof value === "string") { acc[key] += value; } else if (Array.isArray(acc[key]) && Array.isArray(value)) { const accArray = acc[key]; for (let i = 0; i < value.length; i++) { if (!accArray[i]) accArray[i] = {}; accArray[i] = reduce(accArray[i], value[i]); } } else if (typeof acc[key] === "object" && typeof value === "object") { acc[key] = reduce(acc[key], value); } } return acc; }; return reduce(previous, item.choices[0]?.delta || {}); } async *processUserMessageStream(message) { // Create new abort controller for this request this.abortController = new AbortController(); // Add user message to conversation const userEntry = { type: "user", content: message, timestamp: new Date(), }; this.chatHistory.push(userEntry); this.messages.push({ role: "user", content: message }); // Calculate input tokens let inputTokens = this.tokenCounter.countMessageTokens(this.messages); yield { type: "token_count", tokenCount: inputTokens, }; const maxToolRounds = 30; // Prevent infinite loops let toolRounds = 0; let totalOutputTokens = 0; try { // Agent loop - continue until no more tool calls or max rounds reached while (toolRounds < maxToolRounds) { // Check if operation was cancelled if (this.abortController?.signal.aborted) { yield { type: "content", content: "\n\n[Operation cancelled by user]", }; yield { type: "done" }; return; } // Stream response and accumulate const tools = await (0, tools_1.getAllGrokTools)(); const stream = this.grokClient.chatStream(this.messages, tools, undefined, this.isGrokModel() ? { search_parameters: { mode: "auto" } } : undefined); let accumulatedMessage = {}; let accumulatedContent = ""; let toolCallsYielded = false; for await (const chunk of stream) { // Check for cancellation in the streaming loop if (this.abortController?.signal.aborted) { yield { type: "content", content: "\n\n[Operation cancelled by user]", }; yield { type: "done" }; return; } if (!chunk.choices?.[0]) continue; // Accumulate the message using reducer accumulatedMessage = this.messageReducer(accumulatedMessage, chunk); // Check for tool calls - yield when we have complete tool calls with function names if (!toolCallsYielded && accumulatedMessage.tool_calls?.length > 0) { // Check if we have at least one complete tool call with a function name const hasCompleteTool = accumulatedMessage.tool_calls.some((tc) => tc.function?.name); if (hasCompleteTool) { yield { type: "tool_calls", toolCalls: accumulatedMessage.tool_calls, }; toolCallsYielded = true; } } // Stream content as it comes if (chunk.choices[0].delta?.content) { accumulatedContent += chunk.choices[0].delta.content; // Update token count in real-time including accumulated content and any tool calls const currentOutputTokens = this.tokenCounter.estimateStreamingTokens(accumulatedContent) + (accumulatedMessage.tool_calls ? this.tokenCounter.countTokens(JSON.stringify(accumulatedMessage.tool_calls)) : 0); totalOutputTokens = currentOutputTokens; yield { type: "content", content: chunk.choices[0].delta.content, }; // Emit token count update yield { type: "token_count", tokenCount: inputTokens + totalOutputTokens, }; } } // Add assistant entry to history const assistantEntry = { type: "assistant", content: accumulatedMessage.content || "Using tools to help you...", timestamp: new Date(), toolCalls: accumulatedMessage.tool_calls || undefined, }; this.chatHistory.push(assistantEntry); // Add accumulated message to conversation this.messages.push({ role: "assistant", content: accumulatedMessage.content || "", tool_calls: accumulatedMessage.tool_calls, }); // Handle tool calls if present if (accumulatedMessage.tool_calls?.length > 0) { toolRounds++; // Only yield tool_calls if we haven't already yielded them during streaming if (!toolCallsYielded) { yield { type: "tool_calls", toolCalls: accumulatedMessage.tool_calls, }; } // Execute tools for (const toolCall of accumulatedMessage.tool_calls) { // Check for cancellation before executing each tool if (this.abortController?.signal.aborted) { yield { type: "content", content: "\n\n[Operation cancelled by user]", }; yield { type: "done" }; return; } const result = await this.executeTool(toolCall); const toolResultEntry = { type: "tool_result", content: result.success ? result.output || "Success" : result.error || "Error occurred", timestamp: new Date(), toolCall: toolCall, toolResult: result, }; this.chatHistory.push(toolResultEntry); yield { type: "tool_result", toolCall, toolResult: result, }; // Add tool result with proper format (needed for AI context) this.messages.push({ role: "tool", content: result.success ? result.output || "Success" : result.error || "Error", tool_call_id: toolCall.id, }); } // Update token count after processing all tool calls to include tool results inputTokens = this.tokenCounter.countMessageTokens(this.messages); yield { type: "token_count", tokenCount: inputTokens + totalOutputTokens, }; // Continue the loop to get the next response (which might have more tool calls) } else { // No tool calls, we're done break; } } if (toolRounds >= maxToolRounds) { yield { type: "content", content: "\n\nMaximum tool execution rounds reached. Stopping to prevent infinite loops.", }; } yield { type: "done" }; } catch (error) { // Check if this was a cancellation if (this.abortController?.signal.aborted) { yield { type: "content", content: "\n\n[Operation cancelled by user]", }; yield { type: "done" }; return; } const errorEntry = { type: "assistant", content: `Sorry, I encountered an error: ${error.message}`, timestamp: new Date(), }; this.chatHistory.push(errorEntry); yield { type: "content", content: errorEntry.content, }; yield { type: "done" }; } finally { // Clean up abort controller this.abortController = null; } } async executeTool(toolCall) { try { const args = JSON.parse(toolCall.function.arguments); switch (toolCall.function.name) { case "view_file": const range = args.start_line && args.end_line ? [args.start_line, args.end_line] : undefined; return await this.textEditor.view(args.path, range); case "create_file": return await this.textEditor.create(args.path, args.content); case "str_replace_editor": return await this.textEditor.strReplace(args.path, args.old_str, args.new_str, args.replace_all); case "bash": return await this.bash.execute(args.command); case "create_todo_list": return await this.todoTool.createTodoList(args.todos); case "update_todo_list": return await this.todoTool.updateTodoList(args.updates); case "search": return await this.search.search(args.query, { searchType: args.search_type, includePattern: args.include_pattern, excludePattern: args.exclude_pattern, caseSensitive: args.case_sensitive, wholeWord: args.whole_word, regex: args.regex, maxResults: args.max_results, fileTypes: args.file_types, includeHidden: args.include_hidden, }); default: // Check if this is an MCP tool if (toolCall.function.name.startsWith("mcp__")) { return await this.executeMCPTool(toolCall); } return { success: false, error: `Unknown tool: ${toolCall.function.name}`, }; } } catch (error) { return { success: false, error: `Tool execution error: ${error.message}`, }; } } async executeMCPTool(toolCall) { try { const args = JSON.parse(toolCall.function.arguments); const mcpManager = (0, tools_1.getMCPManager)(); const result = await mcpManager.callTool(toolCall.function.name, args); if (result.isError) { return { success: false, error: result.content[0]?.text || "MCP tool error", }; } // Extract content from result const output = result.content .map((item) => { if (item.type === "text") { return item.text; } else if (item.type === "resource") { return `Resource: ${item.resource?.uri || "Unknown"}`; } return String(item); }) .join("\n"); return { success: true, output: output || "Success", }; } catch (error) { return { success: false, error: `MCP tool execution error: ${error.message}`, }; } } getChatHistory() { return [...this.chatHistory]; } getCurrentDirectory() { return this.bash.getCurrentDirectory(); } async executeBashCommand(command) { return await this.bash.execute(command); } getCurrentModel() { return this.grokClient.getCurrentModel(); } setModel(model) { this.grokClient.setModel(model); // Update token counter for new model this.tokenCounter.dispose(); this.tokenCounter = (0, token_counter_1.createTokenCounter)(model); } abortCurrentOperation() { if (this.abortController) { this.abortController.abort(); } } } exports.GrokAgent = GrokAgent; //# sourceMappingURL=grok-agent.js.map