diff --git a/package-lock.json b/package-lock.json index da11427..217a29a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vibe-kit/grok-cli", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vibe-kit/grok-cli", - "version": "0.0.1", + "version": "0.0.2", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/package.json b/package.json index dacf1a5..1c2c564 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-kit/grok-cli", - "version": "0.0.2", + "version": "0.0.3", "description": "An open-source AI agent that brings the power of Grok directly into your terminal.", "main": "dist/index.js", "bin": { diff --git a/src/agent/grok-agent.ts b/src/agent/grok-agent.ts index c35dae0..e840dcc 100644 --- a/src/agent/grok-agent.ts +++ b/src/agent/grok-agent.ts @@ -7,7 +7,7 @@ import { createTokenCounter, TokenCounter } from "../utils/token-counter"; import { loadCustomInstructions } from "../utils/custom-instructions"; export interface ChatEntry { - type: "user" | "assistant" | "tool_result"; + type: "user" | "assistant" | "tool_result" | "tool_call"; content: string; timestamp: Date; toolCalls?: GrokToolCall[]; @@ -64,6 +64,9 @@ You have access to these tools: - 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 @@ -127,7 +130,9 @@ Current working directory: ${process.cwd()}`, try { let currentResponse = await this.grokClient.chat( this.messages, - GROK_TOOLS + GROK_TOOLS, + undefined, + { search_parameters: { mode: "auto" } } ); // Agent loop - continue until no more tool calls or max rounds reached @@ -191,7 +196,9 @@ Current working directory: ${process.cwd()}`, // Get next response - this might contain more tool calls currentResponse = await this.grokClient.chat( this.messages, - GROK_TOOLS + GROK_TOOLS, + undefined, + { search_parameters: { mode: "auto" } } ); } else { // No more tool calls, add final response @@ -270,7 +277,7 @@ Current working directory: ${process.cwd()}`, ): AsyncGenerator { // Create new abort controller for this request this.abortController = new AbortController(); - + // Add user message to conversation const userEntry: ChatEntry = { type: "user", @@ -307,7 +314,12 @@ Current working directory: ${process.cwd()}`, } // Stream response and accumulate - const stream = this.grokClient.chatStream(this.messages, GROK_TOOLS); + const stream = this.grokClient.chatStream( + this.messages, + GROK_TOOLS, + undefined, + { search_parameters: { mode: "auto" } } + ); let accumulatedMessage: any = {}; let accumulatedContent = ""; let toolCallsYielded = false; diff --git a/src/grok/client.ts b/src/grok/client.ts index 307ca14..88e776d 100644 --- a/src/grok/client.ts +++ b/src/grok/client.ts @@ -1,15 +1,15 @@ -import OpenAI from 'openai'; -import type { ChatCompletionMessageParam } from 'openai/resources/chat'; +import OpenAI from "openai"; +import type { ChatCompletionMessageParam } from "openai/resources/chat"; export type GrokMessage = ChatCompletionMessageParam; export interface GrokTool { - type: 'function'; + type: "function"; function: { name: string; description: string; parameters: { - type: 'object'; + type: "object"; properties: Record; required: string[]; }; @@ -18,13 +18,22 @@ export interface GrokTool { export interface GrokToolCall { id: string; - type: 'function'; + type: "function"; function: { name: string; arguments: string; }; } +export interface SearchParameters { + mode?: "auto" | "on" | "off"; + // sources removed - let API use default sources to avoid format issues +} + +export interface SearchOptions { + search_parameters?: SearchParameters; +} + export interface GrokResponse { choices: Array<{ message: { @@ -38,12 +47,12 @@ export interface GrokResponse { export class GrokClient { private client: OpenAI; - private currentModel: string = 'grok-3-latest'; + private currentModel: string = "grok-3-latest"; constructor(apiKey: string, model?: string, baseURL?: string) { this.client = new OpenAI({ apiKey, - baseURL: baseURL || process.env.GROK_BASE_URL || 'https://api.x.ai/v1', + baseURL: baseURL || process.env.GROK_BASE_URL || "https://api.x.ai/v1", timeout: 360000, }); if (model) { @@ -62,17 +71,27 @@ export class GrokClient { async chat( messages: GrokMessage[], tools?: GrokTool[], - model?: string + model?: string, + searchOptions?: SearchOptions ): Promise { try { - const response = await this.client.chat.completions.create({ + const requestPayload: any = { model: model || this.currentModel, messages, tools: tools || [], - tool_choice: tools ? 'auto' : undefined, + tool_choice: tools && tools.length > 0 ? "auto" : undefined, temperature: 0.7, max_tokens: 4000, - }); + }; + + // Add search parameters if specified + if (searchOptions?.search_parameters) { + requestPayload.search_parameters = searchOptions.search_parameters; + } + + const response = await this.client.chat.completions.create( + requestPayload + ); return response as GrokResponse; } catch (error: any) { @@ -83,18 +102,28 @@ export class GrokClient { async *chatStream( messages: GrokMessage[], tools?: GrokTool[], - model?: string + model?: string, + searchOptions?: SearchOptions ): AsyncGenerator { try { - const stream = await this.client.chat.completions.create({ + const requestPayload: any = { model: model || this.currentModel, messages, tools: tools || [], - tool_choice: tools ? 'auto' : undefined, + tool_choice: tools && tools.length > 0 ? "auto" : undefined, temperature: 0.7, max_tokens: 4000, stream: true, - }); + }; + + // Add search parameters if specified + if (searchOptions?.search_parameters) { + requestPayload.search_parameters = searchOptions.search_parameters; + } + + const stream = (await this.client.chat.completions.create( + requestPayload + )) as any; for await (const chunk of stream) { yield chunk; @@ -103,4 +132,20 @@ export class GrokClient { throw new Error(`Grok API error: ${error.message}`); } } -} \ No newline at end of file + + async search( + query: string, + searchParameters?: SearchParameters + ): Promise { + const searchMessage: GrokMessage = { + role: "user", + content: query, + }; + + const searchOptions: SearchOptions = { + search_parameters: searchParameters || { mode: "on" }, + }; + + return this.chat([searchMessage], [], undefined, searchOptions); + } +} diff --git a/src/grok/tools.ts b/src/grok/tools.ts index 684d798..975fcd4 100644 --- a/src/grok/tools.ts +++ b/src/grok/tools.ts @@ -1,174 +1,175 @@ -import { GrokTool } from './client'; +import { GrokTool } from "./client"; export const GROK_TOOLS: GrokTool[] = [ { - type: 'function', + type: "function", function: { - name: 'view_file', - description: 'View contents of a file or list directory contents', + name: "view_file", + description: "View contents of a file or list directory contents", parameters: { - type: 'object', + type: "object", properties: { path: { - type: 'string', - description: 'Path to file or directory to view' + type: "string", + description: "Path to file or directory to view", }, start_line: { - type: 'number', - description: 'Starting line number for partial file view (optional)' + type: "number", + description: + "Starting line number for partial file view (optional)", }, end_line: { - type: 'number', - description: 'Ending line number for partial file view (optional)' - } + type: "number", + description: "Ending line number for partial file view (optional)", + }, }, - required: ['path'] - } - } + required: ["path"], + }, + }, }, { - type: 'function', + type: "function", function: { - name: 'create_file', - description: 'Create a new file with specified content', + name: "create_file", + description: "Create a new file with specified content", parameters: { - type: 'object', + type: "object", properties: { path: { - type: 'string', - description: 'Path where the file should be created' + type: "string", + description: "Path where the file should be created", }, content: { - type: 'string', - description: 'Content to write to the file' - } + type: "string", + description: "Content to write to the file", + }, }, - required: ['path', 'content'] - } - } + required: ["path", "content"], + }, + }, }, { - type: 'function', + type: "function", function: { - name: 'str_replace_editor', - description: 'Replace specific text in a file', + name: "str_replace_editor", + description: "Replace specific text in a file", parameters: { - type: 'object', + type: "object", properties: { path: { - type: 'string', - description: 'Path to the file to edit' + type: "string", + description: "Path to the file to edit", }, old_str: { - type: 'string', - description: 'Text to replace (must match exactly)' + type: "string", + description: "Text to replace (must match exactly)", }, new_str: { - type: 'string', - description: 'Text to replace with' - } + type: "string", + description: "Text to replace with", + }, }, - required: ['path', 'old_str', 'new_str'] - } - } + required: ["path", "old_str", "new_str"], + }, + }, }, { - type: 'function', + type: "function", function: { - name: 'bash', - description: 'Execute a bash command', + name: "bash", + description: "Execute a bash command", parameters: { - type: 'object', + type: "object", properties: { command: { - type: 'string', - description: 'The bash command to execute' - } + type: "string", + description: "The bash command to execute", + }, }, - required: ['command'] - } - } + required: ["command"], + }, + }, }, { - type: 'function', + type: "function", function: { - name: 'create_todo_list', - description: 'Create a new todo list for planning and tracking tasks', + name: "create_todo_list", + description: "Create a new todo list for planning and tracking tasks", parameters: { - type: 'object', + type: "object", properties: { todos: { - type: 'array', - description: 'Array of todo items', + type: "array", + description: "Array of todo items", items: { - type: 'object', + type: "object", properties: { id: { - type: 'string', - description: 'Unique identifier for the todo item' + type: "string", + description: "Unique identifier for the todo item", }, content: { - type: 'string', - description: 'Description of the todo item' + type: "string", + description: "Description of the todo item", }, status: { - type: 'string', - enum: ['pending', 'in_progress', 'completed'], - description: 'Current status of the todo item' + type: "string", + enum: ["pending", "in_progress", "completed"], + description: "Current status of the todo item", }, priority: { - type: 'string', - enum: ['high', 'medium', 'low'], - description: 'Priority level of the todo item' - } + type: "string", + enum: ["high", "medium", "low"], + description: "Priority level of the todo item", + }, }, - required: ['id', 'content', 'status', 'priority'] - } - } + required: ["id", "content", "status", "priority"], + }, + }, }, - required: ['todos'] - } - } + required: ["todos"], + }, + }, }, { - type: 'function', + type: "function", function: { - name: 'update_todo_list', - description: 'Update existing todos in the todo list', + name: "update_todo_list", + description: "Update existing todos in the todo list", parameters: { - type: 'object', + type: "object", properties: { updates: { - type: 'array', - description: 'Array of todo updates', + type: "array", + description: "Array of todo updates", items: { - type: 'object', + type: "object", properties: { id: { - type: 'string', - description: 'ID of the todo item to update' + type: "string", + description: "ID of the todo item to update", }, status: { - type: 'string', - enum: ['pending', 'in_progress', 'completed'], - description: 'New status for the todo item' + type: "string", + enum: ["pending", "in_progress", "completed"], + description: "New status for the todo item", }, content: { - type: 'string', - description: 'New content for the todo item' + type: "string", + description: "New content for the todo item", }, priority: { - type: 'string', - enum: ['high', 'medium', 'low'], - description: 'New priority for the todo item' - } + type: "string", + enum: ["high", "medium", "low"], + description: "New priority for the todo item", + }, }, - required: ['id'] - } - } + required: ["id"], + }, + }, }, - required: ['updates'] - } - } - } -]; \ No newline at end of file + required: ["updates"], + }, + }, + }, +]; diff --git a/src/hooks/use-input-handler.ts b/src/hooks/use-input-handler.ts index 1c9063c..3628441 100644 --- a/src/hooks/use-input-handler.ts +++ b/src/hooks/use-input-handler.ts @@ -55,10 +55,13 @@ export function useInputHandler({ ]; const availableModels: ModelOption[] = [ - { model: "grok-4-latest", description: "Latest Grok-4 model (most capable)" }, + { + model: "grok-4-latest", + description: "Latest Grok-4 model (most capable)", + }, { model: "grok-3-latest", description: "Latest Grok-3 model" }, { model: "grok-3-fast", description: "Fast Grok-3 variant" }, - { model: "grok-3-mini-fast", description: "Fastest Grok-3 variant" } + { model: "grok-3-mini-fast", description: "Fastest Grok-3 variant" }, ]; const handleDirectCommand = async (input: string): Promise => { @@ -67,18 +70,18 @@ export function useInputHandler({ if (trimmedInput === "/clear") { // Reset chat history setChatHistory([]); - + // Reset processing states setIsProcessing(false); setIsStreaming(false); setTokenCount(0); setProcessingTime(0); processingStartTime.current = 0; - + // Reset confirmation service session flags const confirmationService = ConfirmationService.getInstance(); confirmationService.resetSession(); - + setInput(""); return true; } @@ -124,7 +127,7 @@ Examples: if (trimmedInput.startsWith("/models ")) { const modelArg = trimmedInput.split(" ")[1]; - const modelNames = availableModels.map(m => m.model); + const modelNames = availableModels.map((m) => m.model); if (modelNames.includes(modelArg)) { agent.setModel(modelArg); @@ -150,7 +153,18 @@ Available models: ${modelNames.join(", ")}`, } const directBashCommands = [ - "ls", "pwd", "cd", "cat", "mkdir", "touch", "echo", "grep", "find", "cp", "mv", "rm", + "ls", + "pwd", + "cd", + "cat", + "mkdir", + "touch", + "echo", + "grep", + "find", + "cp", + "mv", + "rm", ]; const firstWord = trimmedInput.split(" ")[0]; @@ -249,10 +263,27 @@ Available models: ${modelNames.join(", ")}`, // Stop streaming for the current assistant message setChatHistory((prev) => prev.map((entry) => - entry.isStreaming ? { ...entry, isStreaming: false, toolCalls: chunk.toolCalls } : entry + entry.isStreaming + ? { + ...entry, + isStreaming: false, + toolCalls: chunk.toolCalls, + } + : entry ) ); streamingEntry = null; + + // Add individual tool call entries to show tools are being executed + chunk.toolCalls.forEach((toolCall) => { + const toolCallEntry: ChatEntry = { + type: "tool_call", + content: "Executing...", + timestamp: new Date(), + toolCall: toolCall, + }; + setChatHistory((prev) => [...prev, toolCallEntry]); + }); } break; @@ -309,7 +340,7 @@ Available models: ${modelNames.join(", ")}`, if (isConfirmationActive) { return; } - + if (key.ctrl && inputChar === "c") { exit(); return; @@ -345,7 +376,9 @@ Available models: ${modelNames.join(", ")}`, return; } if (key.downArrow) { - setSelectedCommandIndex((prev) => (prev + 1) % commandSuggestions.length); + setSelectedCommandIndex( + (prev) => (prev + 1) % commandSuggestions.length + ); return; } if (key.tab || key.return) { @@ -444,4 +477,4 @@ Available models: ${modelNames.join(", ")}`, availableModels, agent, }; -} \ No newline at end of file +} diff --git a/src/tools/index.ts b/src/tools/index.ts index be4cc9b..b7406b0 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,4 +1,4 @@ -export { TextEditorTool } from './text-editor'; -export { BashTool } from './bash'; -export { TodoTool } from './todo-tool'; -export { ConfirmationTool } from './confirmation-tool'; \ No newline at end of file +export { TextEditorTool } from "./text-editor"; +export { BashTool } from "./bash"; +export { TodoTool } from "./todo-tool"; +export { ConfirmationTool } from "./confirmation-tool"; diff --git a/src/ui/components/chat-history.tsx b/src/ui/components/chat-history.tsx index bda12ce..77e5d82 100644 --- a/src/ui/components/chat-history.tsx +++ b/src/ui/components/chat-history.tsx @@ -76,6 +76,7 @@ export function ChatHistory({ entries }: ChatHistoryProps) { ); + case "tool_call": case "tool_result": const getToolActionName = (toolName: string) => { switch (toolName) { @@ -87,6 +88,7 @@ export function ChatHistory({ entries }: ChatHistoryProps) { return "Create"; case "bash": return "Bash"; + case "create_todo_list": return "Created Todo"; case "update_todo_list": @@ -107,6 +109,7 @@ export function ChatHistory({ entries }: ChatHistoryProps) { ) { return ""; } + return args.path || args.file_path || args.command || "unknown"; } catch { return "unknown"; @@ -119,10 +122,10 @@ export function ChatHistory({ entries }: ChatHistoryProps) { const actionName = getToolActionName(toolName); const filePath = getToolFilePath(entry.toolCall); - const shouldShowDiff = toolName === "str_replace_editor" || toolName === "create_file"; const shouldShowFileContent = toolName === "view_file"; + const isExecuting = entry.type === "tool_call"; return ( @@ -134,7 +137,9 @@ export function ChatHistory({ entries }: ChatHistoryProps) { - {shouldShowFileContent ? ( + {isExecuting ? ( + ⎿ Executing... + ) : shouldShowFileContent ? ( ⎿ File contents: @@ -143,12 +148,12 @@ export function ChatHistory({ entries }: ChatHistoryProps) { ) : shouldShowDiff ? ( // For diff results, show only the summary line, not the raw content - ⎿ {entry.content.split('\n')[0]} + ⎿ {entry.content.split("\n")[0]} ) : ( ⎿ {entry.content} )} - {shouldShowDiff && ( + {shouldShowDiff && !isExecuting && ( {renderDiff(entry.content, filePath)}