From eb3fd06c546a05dc8acd51fa19d28ff69a613c95 Mon Sep 17 00:00:00 2001 From: Ismail Pelaseyed Date: Mon, 21 Jul 2025 13:27:59 +0200 Subject: [PATCH] add support for setting api key in TUI (#11) --- src/index.ts | 42 +++++--- src/ui/components/api-key-input.tsx | 138 +++++++++++++++++++++++++++ src/ui/components/chat-interface.tsx | 22 ++++- 3 files changed, 186 insertions(+), 16 deletions(-) create mode 100644 src/ui/components/api-key-input.tsx diff --git a/src/index.ts b/src/index.ts index ba3c776..e2dae80 100755 --- a/src/index.ts +++ b/src/index.ts @@ -6,10 +6,36 @@ import { program } from "commander"; import * as dotenv from "dotenv"; import { GrokAgent } from "./agent/grok-agent"; import ChatInterface from "./ui/components/chat-interface"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; // Load environment variables dotenv.config(); +// Load API key from user settings if not in environment +function loadApiKey(): string | undefined { + // First check environment variables + let apiKey = process.env.GROK_API_KEY; + + if (!apiKey) { + // Try to load from user settings file + try { + const homeDir = os.homedir(); + const settingsFile = path.join(homeDir, '.grok', 'user-settings.json'); + + if (fs.existsSync(settingsFile)) { + const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); + apiKey = settings.apiKey; + } + } catch (error) { + // Ignore errors, apiKey will remain undefined + } + } + + return apiKey; +} + program .name("grok") .description( @@ -19,18 +45,6 @@ program .option("-d, --directory ", "set working directory", process.cwd()) .option("-k, --api-key ", "Grok API key (or set GROK_API_KEY env var)") .action((options) => { - // Get API key from options or environment - const apiKey = options.apiKey || process.env.GROK_API_KEY; - - if (!apiKey) { - console.error("āŒ Error: Grok API key is required"); - console.error( - " Set GROK_API_KEY environment variable or use --api-key option" - ); - console.error(" Copy .env.example to .env and add your API key"); - process.exit(1); - } - if (options.directory) { try { process.chdir(options.directory); @@ -44,7 +58,9 @@ program } try { - const agent = new GrokAgent(apiKey); + // Get API key from options, environment, or user settings + const apiKey = options.apiKey || loadApiKey(); + const agent = apiKey ? new GrokAgent(apiKey) : undefined; console.log("šŸ¤– Starting Grok CLI Conversational Assistant...\n"); diff --git a/src/ui/components/api-key-input.tsx b/src/ui/components/api-key-input.tsx new file mode 100644 index 0000000..b647366 --- /dev/null +++ b/src/ui/components/api-key-input.tsx @@ -0,0 +1,138 @@ +import React, { useState } from "react"; +import { Box, Text, useInput, useApp } from "ink"; +import { GrokAgent } from "../../agent/grok-agent"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; + +interface ApiKeyInputProps { + onApiKeySet: (agent: GrokAgent) => void; +} + +interface UserSettings { + apiKey?: string; +} + +export default function ApiKeyInput({ onApiKeySet }: ApiKeyInputProps) { + const [input, setInput] = useState(""); + const [error, setError] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const { exit } = useApp(); + + useInput((inputChar, key) => { + if (isSubmitting) return; + + if (key.ctrl && inputChar === "c") { + exit(); + return; + } + + if (key.return) { + handleSubmit(); + return; + } + + + if (key.backspace || key.delete) { + setInput((prev) => prev.slice(0, -1)); + setError(""); + return; + } + + if (inputChar && !key.ctrl && !key.meta) { + setInput((prev) => prev + inputChar); + setError(""); + } + }); + + + const handleSubmit = async () => { + if (!input.trim()) { + setError("API key cannot be empty"); + return; + } + + setIsSubmitting(true); + try { + const apiKey = input.trim(); + const agent = new GrokAgent(apiKey); + + // Set environment variable for current process + process.env.GROK_API_KEY = apiKey; + + // Save to .grok/user-settings.json + try { + const homeDir = os.homedir(); + const grokDir = path.join(homeDir, '.grok'); + const settingsFile = path.join(grokDir, 'user-settings.json'); + + // Create .grok directory if it doesn't exist + if (!fs.existsSync(grokDir)) { + fs.mkdirSync(grokDir, { mode: 0o700 }); + } + + // Load existing settings or create new + let settings: UserSettings = {}; + if (fs.existsSync(settingsFile)) { + try { + settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); + } catch { + settings = {}; + } + } + + // Update API key + settings.apiKey = apiKey; + + // Save settings + fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2), { mode: 0o600 }); + + console.log(`\nāœ… API key saved to ~/.grok/user-settings.json`); + } catch (error) { + console.log('\nāš ļø Could not save API key to settings file'); + console.log('API key set for current session only'); + } + + onApiKeySet(agent); + } catch (error: any) { + setError("Invalid API key format"); + setIsSubmitting(false); + } + }; + + const displayText = input.length > 0 ? + (isSubmitting ? "*".repeat(input.length) : "*".repeat(input.length) + "ā–ˆ") : + (isSubmitting ? " " : "ā–ˆ"); + + return ( + + šŸ”‘ Grok API Key Required + + Please enter your Grok API key to continue: + + + + āÆ + {displayText} + + + {error ? ( + + āŒ {error} + + ) : null} + + + • Press Enter to submit + • Press Ctrl+C to exit + Note: API key will be saved to ~/.grok/user-settings.json + + + {isSubmitting ? ( + + šŸ”„ Validating API key... + + ) : null} + + ); +} \ No newline at end of file diff --git a/src/ui/components/chat-interface.tsx b/src/ui/components/chat-interface.tsx index 4505ada..8a984b1 100644 --- a/src/ui/components/chat-interface.tsx +++ b/src/ui/components/chat-interface.tsx @@ -9,13 +9,15 @@ import { ChatHistory } from "./chat-history"; import { ChatInput } from "./chat-input"; import ConfirmationDialog from "./confirmation-dialog"; import { ConfirmationService, ConfirmationOptions } from "../../utils/confirmation-service"; +import ApiKeyInput from "./api-key-input"; import cfonts from "cfonts"; interface ChatInterfaceProps { - agent: GrokAgent; + agent?: GrokAgent; } -export default function ChatInterface({ agent }: ChatInterfaceProps) { +// Main chat component that handles input when agent is available +function ChatInterfaceWithAgent({ agent }: { agent: GrokAgent }) { const [chatHistory, setChatHistory] = useState([]); const [isProcessing, setIsProcessing] = useState(false); const [processingTime, setProcessingTime] = useState(0); @@ -87,7 +89,6 @@ export default function ChatInterface({ agent }: ChatInterfaceProps) { }; }, [confirmationService]); - useEffect(() => { if (!isProcessing && !isStreaming) { setProcessingTime(0); @@ -181,3 +182,18 @@ export default function ChatInterface({ agent }: ChatInterfaceProps) { ); } + +// Main component that handles API key input or chat interface +export default function ChatInterface({ agent }: ChatInterfaceProps) { + const [currentAgent, setCurrentAgent] = useState(agent || null); + + const handleApiKeySet = (newAgent: GrokAgent) => { + setCurrentAgent(newAgent); + }; + + if (!currentAgent) { + return ; + } + + return ; +}