add support for Grok live search (#20)

This commit is contained in:
Ismail Pelaseyed authored and GitHub committed 2025-07-22 11:42:04 +02:00
1 parent 07ec1540c3
commit 1d45dd3d9e
8 files changed
+240 -144

No files matched your search

+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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": {
+17 -5
View File
@@ -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<StreamingChunk, void, unknown> {
// 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;
+61 -16
View File
@@ -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<string, any>;
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<GrokResponse> {
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<any, void, unknown> {
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}`);
}
}
}
async search(
query: string,
searchParameters?: SearchParameters
): Promise<GrokResponse> {
const searchMessage: GrokMessage = {
role: "user",
content: query,
};
const searchOptions: SearchOptions = {
search_parameters: searchParameters || { mode: "on" },
};
return this.chat([searchMessage], [], undefined, searchOptions);
}
}
+102 -101
View File
@@ -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']
}
}
}
];
required: ["updates"],
},
},
},
];
+44 -11
View File
@@ -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<boolean> => {
@@ -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,
};
}
}
+4 -4
View File
@@ -1,4 +1,4 @@
export { TextEditorTool } from './text-editor';
export { BashTool } from './bash';
export { TodoTool } from './todo-tool';
export { ConfirmationTool } from './confirmation-tool';
export { TextEditorTool } from "./text-editor";
export { BashTool } from "./bash";
export { TodoTool } from "./todo-tool";
export { ConfirmationTool } from "./confirmation-tool";
+9 -4
View File
@@ -76,6 +76,7 @@ export function ChatHistory({ entries }: ChatHistoryProps) {
</Box>
);
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 (
<Box key={index} flexDirection="column" marginTop={1}>
@@ -134,7 +137,9 @@ export function ChatHistory({ entries }: ChatHistoryProps) {
</Text>
</Box>
<Box marginLeft={2} flexDirection="column">
{shouldShowFileContent ? (
{isExecuting ? (
<Text color="cyan">⎿ Executing...</Text>
) : shouldShowFileContent ? (
<Box flexDirection="column">
<Text color="gray">⎿ File contents:</Text>
<Box marginLeft={2} flexDirection="column">
@@ -143,12 +148,12 @@ export function ChatHistory({ entries }: ChatHistoryProps) {
</Box>
) : shouldShowDiff ? (
// For diff results, show only the summary line, not the raw content
<Text color="gray">⎿ {entry.content.split('\n')[0]}</Text>
<Text color="gray">⎿ {entry.content.split("\n")[0]}</Text>
) : (
<Text color="gray">⎿ {entry.content}</Text>
)}
</Box>
{shouldShowDiff && (
{shouldShowDiff && !isExecuting && (
<Box marginLeft={4} flexDirection="column">
{renderDiff(entry.content, filePath)}
</Box>