diff --git a/README.md b/README.md index ec19fd6..b09d01c 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,9 @@ This mode is particularly useful for: ### Model Selection -You can specify which AI model to use with the `--model` parameter: +You can specify which AI model to use with the `--model` parameter or `GROK_MODEL` environment variable: +**Method 1: Command Line Flag** ```bash # Use Grok models grok --model grok-4-latest @@ -131,6 +132,23 @@ grok --model gemini-2.5-pro --base-url https://api-endpoint.com/v1 grok --model claude-sonnet-4-20250514 --base-url https://api-endpoint.com/v1 ``` +**Method 2: Environment Variable** +```bash +export GROK_MODEL=grok-4-latest +grok +``` + +**Method 3: User Settings File** +Add to `~/.grok/user-settings.json`: +```json +{ + "apiKey": "your_api_key_here", + "model": "grok-4-latest" +} +``` + +Priority order: `--model` flag > `GROK_MODEL` environment variable > user settings > default (grok-4-latest) + ### Command Line Options ```bash @@ -141,7 +159,7 @@ Options: -d, --directory set working directory -k, --api-key Grok API key (or set GROK_API_KEY env var) -u, --base-url Grok API base URL (or set GROK_BASE_URL env var) - -m, --model AI model to use (e.g., grok-4-latest, grok-3-latest) + -m, --model AI model to use (e.g., grok-4-latest, grok-3-latest) (or set GROK_MODEL env var) -p, --prompt process a single prompt and exit (headless mode) -h, --help display help for command ``` diff --git a/package.json b/package.json index 20ee5c1..784913b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vibe-kit/grok-cli", - "version": "0.0.14", + "version": "0.0.15", "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 f5d9423..6fd049b 100644 --- a/src/agent/grok-agent.ts +++ b/src/agent/grok-agent.ts @@ -54,8 +54,7 @@ export class GrokAgent extends EventEmitter { constructor(apiKey: string, baseURL?: string, model?: string) { super(); - // Use saved model if no model is explicitly provided - const savedModel = getSetting("selectedModel"); + const savedModel = getSetting("model"); const modelToUse = model || savedModel || "grok-4-latest"; this.grokClient = new GrokClient(apiKey, modelToUse, baseURL); this.textEditor = new TextEditorTool(); diff --git a/src/hooks/use-input-handler.ts b/src/hooks/use-input-handler.ts index 0ae90e2..c0ff353 100644 --- a/src/hooks/use-input-handler.ts +++ b/src/hooks/use-input-handler.ts @@ -150,7 +150,7 @@ Examples: if (modelNames.includes(modelArg)) { agent.setModel(modelArg); - updateSetting("selectedModel", modelArg); + updateSetting("model", modelArg); const confirmEntry: ChatEntry = { type: "assistant", content: `✓ Switched to model: ${modelArg}`, @@ -663,7 +663,7 @@ Respond with ONLY the commit message, no additional text.`; if (key.tab || key.return) { const selectedModel = availableModels[selectedModelIndex]; agent.setModel(selectedModel.model); - updateSetting("selectedModel", selectedModel.model); + updateSetting("model", selectedModel.model); const confirmEntry: ChatEntry = { type: "assistant", content: `✓ Switched to model: ${selectedModel.model}`, diff --git a/src/index.ts b/src/index.ts index a89dcf6..d78471f 100755 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { program } from "commander"; import * as dotenv from "dotenv"; import { GrokAgent } from "./agent/grok-agent"; import ChatInterface from "./ui/components/chat-interface"; +import { getSetting, loadSettings } from "./utils/settings"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; @@ -16,32 +17,32 @@ import { createMCPCommand } from "./commands/mcp"; dotenv.config(); // Add proper signal handling for terminal cleanup -process.on('SIGINT', () => { +process.on("SIGINT", () => { // Restore terminal to normal mode before exit if (process.stdin.isTTY) { process.stdin.setRawMode(false); } - console.log('\nGracefully shutting down...'); + console.log("\nGracefully shutting down..."); process.exit(0); }); -process.on('SIGTERM', () => { +process.on("SIGTERM", () => { // Restore terminal to normal mode before exit if (process.stdin.isTTY) { process.stdin.setRawMode(false); } - console.log('\nGracefully shutting down...'); + console.log("\nGracefully shutting down..."); process.exit(0); }); // Handle uncaught exceptions to prevent hanging -process.on('uncaughtException', (error) => { - console.error('Uncaught exception:', error); +process.on("uncaughtException", (error) => { + console.error("Uncaught exception:", error); process.exit(1); }); -process.on('unhandledRejection', (reason, promise) => { - console.error('Unhandled rejection at:', promise, 'reason:', reason); +process.on("unhandledRejection", (reason, promise) => { + console.error("Unhandled rejection at:", promise, "reason:", reason); process.exit(1); }); @@ -62,7 +63,7 @@ function ensureUserSettingsDirectory(): void { const defaultSettings = { apiKey: "", baseURL: "", - defaultModel: "grok-4-latest" + defaultModel: "grok-4-latest", }; fs.writeFileSync(settingsFile, JSON.stringify(defaultSettings, null, 2)); } @@ -119,6 +120,24 @@ function loadBaseURL(): string | undefined { return baseURL; } +// Load model from user settings if not in environment +function loadModel(): string | undefined { + // First check environment variables + let model = process.env.GROK_MODEL; + + if (!model) { + // Try to load from local settings file + try { + const settings = loadSettings(); + model = settings.model; + } catch (error) { + // Ignore errors, model will remain undefined + } + } + + return model; +} + // Handle commit-and-push command in headless mode async function handleCommitAndPushHeadless( apiKey: string, @@ -136,8 +155,10 @@ async function handleCommitAndPushHeadless( console.log("> /commit-and-push\n"); // First check if there are any changes at all - const initialStatusResult = await agent.executeBashCommand("git status --porcelain"); - + const initialStatusResult = await agent.executeBashCommand( + "git status --porcelain" + ); + if (!initialStatusResult.success || !initialStatusResult.output?.trim()) { console.log("❌ No changes to commit. Working directory is clean."); process.exit(1); @@ -147,9 +168,11 @@ async function handleCommitAndPushHeadless( // Add all changes const addResult = await agent.executeBashCommand("git add ."); - + if (!addResult.success) { - console.log(`❌ git add: ${addResult.error || 'Failed to stage changes'}`); + console.log( + `❌ git add: ${addResult.error || "Failed to stage changes"}` + ); process.exit(1); } @@ -171,10 +194,10 @@ Follow conventional commit format (feat:, fix:, docs:, etc.) and keep it under 7 Respond with ONLY the commit message, no additional text.`; console.log("🤖 Generating commit message..."); - + const commitMessageEntries = await agent.processUserMessage(commitPrompt); let commitMessage = ""; - + // Extract the commit message from the AI response for (const entry of commitMessageEntries) { if (entry.type === "assistant" && entry.content.trim()) { @@ -189,7 +212,7 @@ Respond with ONLY the commit message, no additional text.`; } // Clean the commit message - const cleanCommitMessage = commitMessage.replace(/^["']|["']$/g, ''); + const cleanCommitMessage = commitMessage.replace(/^["']|["']$/g, ""); console.log(`✅ Generated commit message: "${cleanCommitMessage}"`); // Execute the commit @@ -197,28 +220,38 @@ Respond with ONLY the commit message, no additional text.`; const commitResult = await agent.executeBashCommand(commitCommand); if (commitResult.success) { - console.log(`✅ git commit: ${commitResult.output?.split('\n')[0] || 'Commit successful'}`); - + console.log( + `✅ git commit: ${ + commitResult.output?.split("\n")[0] || "Commit successful" + }` + ); + // If commit was successful, push to remote // First try regular push, if it fails try with upstream setup let pushResult = await agent.executeBashCommand("git push"); - - if (!pushResult.success && pushResult.error?.includes("no upstream branch")) { + + if ( + !pushResult.success && + pushResult.error?.includes("no upstream branch") + ) { console.log("🔄 Setting upstream and pushing..."); pushResult = await agent.executeBashCommand("git push -u origin HEAD"); } - + if (pushResult.success) { - console.log(`✅ git push: ${pushResult.output?.split('\n')[0] || 'Push successful'}`); + console.log( + `✅ git push: ${ + pushResult.output?.split("\n")[0] || "Push successful" + }` + ); } else { - console.log(`❌ git push: ${pushResult.error || 'Push failed'}`); + console.log(`❌ git push: ${pushResult.error || "Push failed"}`); process.exit(1); } } else { - console.log(`❌ git commit: ${commitResult.error || 'Commit failed'}`); + console.log(`❌ git commit: ${commitResult.error || "Commit failed"}`); process.exit(1); } - } catch (error: any) { console.error("❌ Error during commit and push:", error.message); process.exit(1); @@ -306,7 +339,7 @@ program ) .option( "-m, --model ", - "AI model to use (e.g., gemini-2.5-pro, grok-4-latest)" + "AI model to use (e.g., gemini-2.5-pro, grok-4-latest) (or set GROK_MODEL env var)" ) .option( "-p, --prompt ", @@ -329,7 +362,7 @@ program // Get API key from options, environment, or user settings const apiKey = options.apiKey || loadApiKey(); const baseURL = options.baseUrl || loadBaseURL(); - const model = options.model; + const model = options.model || loadModel(); if (!apiKey) { console.error( @@ -370,7 +403,7 @@ gitCommand ) .option( "-m, --model ", - "AI model to use (e.g., gemini-2.5-pro, grok-4-latest)" + "AI model to use (e.g., gemini-2.5-pro, grok-4-latest) (or set GROK_MODEL env var)" ) .action(async (options) => { if (options.directory) { @@ -389,7 +422,7 @@ gitCommand // Get API key from options, environment, or user settings const apiKey = options.apiKey || loadApiKey(); const baseURL = options.baseUrl || loadBaseURL(); - const model = options.model; + const model = options.model || loadModel(); if (!apiKey) { console.error( diff --git a/src/mcp/config.ts b/src/mcp/config.ts index d4466c4..f12ab3c 100644 --- a/src/mcp/config.ts +++ b/src/mcp/config.ts @@ -7,7 +7,7 @@ const CONFIG_DIR = path.join(process.cwd(), ".grok"); const SETTINGS_FILE = path.join(CONFIG_DIR, "settings.json"); export interface Settings { - selectedModel?: string; + model?: string; mcpServers?: Record; } @@ -52,12 +52,12 @@ export function loadMCPConfig(): MCPConfig { export function saveMCPConfig(config: MCPConfig): void { const settings = loadSettings(); settings.mcpServers = {}; - + // Convert servers array to object keyed by name for (const server of config.servers) { settings.mcpServers[server.name] = server; } - + saveSettings(settings); } @@ -66,7 +66,7 @@ export function addMCPServer(config: MCPServerConfig): void { if (!settings.mcpServers) { settings.mcpServers = {}; } - + settings.mcpServers[config.name] = config; saveSettings(settings); } diff --git a/src/utils/settings.ts b/src/utils/settings.ts index 7cc07d4..283a990 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -1,13 +1,13 @@ -import fs from 'fs'; -import path from 'path'; +import fs from "fs"; +import path from "path"; interface Settings { - selectedModel?: string; + model?: string; [key: string]: any; } -const SETTINGS_DIR = '.grok'; -const SETTINGS_FILE = 'settings.json'; +const SETTINGS_DIR = ".grok"; +const SETTINGS_FILE = "settings.json"; function getSettingsPath(): string { return path.join(process.cwd(), SETTINGS_DIR, SETTINGS_FILE); @@ -21,23 +21,23 @@ function ensureSettingsDirectory(): void { } const DEFAULT_SETTINGS: Settings = { - selectedModel: 'grok-4-latest' + model: "grok-4-latest", }; export function loadSettings(): Settings { try { ensureSettingsDirectory(); const settingsPath = getSettingsPath(); - + if (!fs.existsSync(settingsPath)) { saveSettings(DEFAULT_SETTINGS); return DEFAULT_SETTINGS; } - - const settingsContent = fs.readFileSync(settingsPath, 'utf-8'); + + const settingsContent = fs.readFileSync(settingsPath, "utf-8"); return JSON.parse(settingsContent); } catch (error) { - console.warn('Failed to load settings:', error); + console.warn("Failed to load settings:", error); return {}; } } @@ -46,14 +46,17 @@ export function saveSettings(settings: Settings): void { try { ensureSettingsDirectory(); const settingsPath = getSettingsPath(); - + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); } catch (error) { - console.error('Failed to save settings:', error); + console.error("Failed to save settings:", error); } } -export function updateSetting(key: K, value: Settings[K]): void { +export function updateSetting( + key: K, + value: Settings[K] +): void { const currentSettings = loadSettings(); currentSettings[key] = value; saveSettings(currentSettings); @@ -62,4 +65,4 @@ export function updateSetting(key: K, value: Settings[ export function getSetting(key: K): Settings[K] { const settings = loadSettings(); return settings[key]; -} \ No newline at end of file +}