feat: improve model configuration and settings management (#59)
* feat: improve model configuration and settings management - Refactor model configuration system with unified settings approach - Add support for user-level default models and project-level current models - Implement automatic saving of API key and base URL from command line arguments - Remove model descriptions for cleaner UI and easier extensibility - Fix commander.js subcommand argument parsing with fallback mechanism - Improve configuration file structure and priority handling - Add comprehensive model configuration utilities in src/utils/model-config.ts Configuration priority: project current model > user default model > system default Both main command and git subcommand now save credentials automatically * refactor: unify settings management - Add SettingsManager singleton for user/project settings - Eliminate duplicate settings code across files - Clear separation: user settings vs project settings * docs: improve README clarity and organization - Simplify setup section with quick start approach - Consolidate configuration info into unified section - Add clear explanation of user vs project settings - Document OpenAI-compatible API support - Remove duplicate model configuration sections * bump sdk --------- Co-authored-by: Ismail Pelaseyed <homanp@gmail.com>
This commit is contained in:
1 parent
346097acab
commit
5a4b2c8e25
12 files changed
+534
-251
No files matched your search
@@ -66,7 +66,7 @@ Create `~/.grok/user-settings.json`:
|
||||
|
||||
### Custom Base URL (Optional)
|
||||
|
||||
You can configure a custom Grok API endpoint (choose one method):
|
||||
By default, the CLI uses `https://api.x.ai/v1` as the Grok API endpoint. You can configure a custom endpoint if needed (choose one method):
|
||||
|
||||
**Method 1: Environment Variable**
|
||||
```bash
|
||||
@@ -75,7 +75,7 @@ export GROK_BASE_URL=https://your-custom-endpoint.com/v1
|
||||
|
||||
**Method 2: Command Line Flag**
|
||||
```bash
|
||||
grok --api-key your_api_key_here --baseurl https://your-custom-endpoint.com/v1
|
||||
grok --api-key your_api_key_here --base-url https://your-custom-endpoint.com/v1
|
||||
```
|
||||
|
||||
**Method 3: User Settings File**
|
||||
@@ -87,6 +87,89 @@ Add to `~/.grok/user-settings.json`:
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
Grok CLI uses two types of configuration files to manage settings:
|
||||
|
||||
### User-Level Settings (`~/.grok/user-settings.json`)
|
||||
|
||||
This file stores **global settings** that apply across all projects. These settings rarely change and include:
|
||||
|
||||
- **API Key**: Your Grok API key
|
||||
- **Base URL**: Custom API endpoint (if needed)
|
||||
- **Default Model**: Your preferred model (e.g., `grok-4-latest`)
|
||||
- **Available Models**: List of models you can use
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"apiKey": "your_api_key_here",
|
||||
"baseURL": "https://api.x.ai/v1",
|
||||
"defaultModel": "grok-4-latest",
|
||||
"models": [
|
||||
"grok-4-latest",
|
||||
"grok-3-latest",
|
||||
"grok-3-fast",
|
||||
"grok-3-mini-fast"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Project-Level Settings (`.grok/settings.json`)
|
||||
|
||||
This file stores **project-specific settings** in your current working directory. It includes:
|
||||
|
||||
- **Current Model**: The model currently in use for this project
|
||||
- **MCP Servers**: Model Context Protocol server configurations
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"model": "grok-3-fast",
|
||||
"mcpServers": {
|
||||
"linear": {
|
||||
"name": "linear",
|
||||
"transport": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["@linear/mcp-server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Global Defaults**: User-level settings provide your default preferences
|
||||
2. **Project Override**: Project-level settings override defaults for specific projects
|
||||
3. **Directory-Specific**: When you change directories, project settings are loaded automatically
|
||||
4. **Fallback Logic**: Project model → User default model → System default (`grok-4-latest`)
|
||||
|
||||
This means you can have different models for different projects while maintaining consistent global settings like your API key.
|
||||
|
||||
### Using Other API Providers
|
||||
|
||||
**Important**: Grok CLI uses **OpenAI-compatible APIs**. You can use any provider that implements the OpenAI chat completions standard.
|
||||
|
||||
**Popular Providers**:
|
||||
- **X.AI (Grok)**: `https://api.x.ai/v1` (default)
|
||||
- **OpenAI**: `https://api.openai.com/v1`
|
||||
- **OpenRouter**: `https://openrouter.ai/api/v1`
|
||||
- **Groq**: `https://api.groq.com/openai/v1`
|
||||
|
||||
**Example with OpenRouter**:
|
||||
```json
|
||||
{
|
||||
"apiKey": "your_openrouter_key",
|
||||
"baseURL": "https://openrouter.ai/api/v1",
|
||||
"defaultModel": "anthropic/claude-3.5-sonnet",
|
||||
"models": [
|
||||
"anthropic/claude-3.5-sonnet",
|
||||
"openai/gpt-4o",
|
||||
"meta-llama/llama-3.1-70b-instruct"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Interactive Mode
|
||||
@@ -143,11 +226,11 @@ Add to `~/.grok/user-settings.json`:
|
||||
```json
|
||||
{
|
||||
"apiKey": "your_api_key_here",
|
||||
"model": "grok-4-latest"
|
||||
"defaultModel": "grok-4-latest"
|
||||
}
|
||||
```
|
||||
|
||||
Priority order: `--model` flag > `GROK_MODEL` environment variable > user settings > default (grok-4-latest)
|
||||
**Model Priority**: `--model` flag > `GROK_MODEL` environment variable > user default model > system default (grok-4-latest)
|
||||
|
||||
### Command Line Options
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@vibe-kit/grok-cli",
|
||||
"version": "0.0.12",
|
||||
"version": "0.0.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@vibe-kit/grok-cli",
|
||||
"version": "0.0.12",
|
||||
"version": "0.0.17",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.17.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vibe-kit/grok-cli",
|
||||
"version": "0.0.17",
|
||||
"version": "0.0.18",
|
||||
"description": "An open-source AI agent that brings the power of Grok directly into your terminal.",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { ToolResult } from "../types";
|
||||
import { EventEmitter } from "events";
|
||||
import { createTokenCounter, TokenCounter } from "../utils/token-counter";
|
||||
import { loadCustomInstructions } from "../utils/custom-instructions";
|
||||
import { getSetting } from "../utils/settings";
|
||||
import { getSettingsManager } from "../utils/settings-manager";
|
||||
|
||||
export interface ChatEntry {
|
||||
type: "user" | "assistant" | "tool_result" | "tool_call";
|
||||
@@ -54,7 +54,8 @@ export class GrokAgent extends EventEmitter {
|
||||
|
||||
constructor(apiKey: string, baseURL?: string, model?: string) {
|
||||
super();
|
||||
const savedModel = getSetting("model");
|
||||
const manager = getSettingsManager();
|
||||
const savedModel = manager.getCurrentModel();
|
||||
const modelToUse = model || savedModel || "grok-4-latest";
|
||||
this.grokClient = new GrokClient(apiKey, modelToUse, baseURL);
|
||||
this.textEditor = new TextEditorTool();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useInput } from "ink";
|
||||
import { GrokAgent, ChatEntry } from "../agent/grok-agent";
|
||||
import { ConfirmationService } from "../utils/confirmation-service";
|
||||
import { updateSetting } from "../utils/settings";
|
||||
|
||||
import { filterCommandSuggestions } from "../ui/components/command-suggestions";
|
||||
import { loadModelConfig, updateCurrentModel } from "../utils/model-config";
|
||||
|
||||
interface UseInputHandlerProps {
|
||||
agent: GrokAgent;
|
||||
@@ -26,7 +27,6 @@ interface CommandSuggestion {
|
||||
|
||||
interface ModelOption {
|
||||
model: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function useInputHandler({
|
||||
@@ -62,15 +62,10 @@ export function useInputHandler({
|
||||
{ command: "/exit", description: "Exit the application" },
|
||||
];
|
||||
|
||||
const availableModels: ModelOption[] = [
|
||||
{
|
||||
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" },
|
||||
];
|
||||
// Load models from configuration with fallback to defaults
|
||||
const availableModels: ModelOption[] = useMemo(() => {
|
||||
return loadModelConfig(); // Return directly, interface already matches
|
||||
}, []);
|
||||
|
||||
const handleDirectCommand = async (input: string): Promise<boolean> => {
|
||||
const trimmedInput = input.trim();
|
||||
@@ -102,24 +97,27 @@ export function useInputHandler({
|
||||
Built-in Commands:
|
||||
/clear - Clear chat history
|
||||
/help - Show this help
|
||||
/models - Switch Grok models
|
||||
/models - Switch between available models
|
||||
/exit - Exit application
|
||||
exit, quit - Exit application
|
||||
|
||||
|
||||
Git Commands:
|
||||
/commit-and-push - AI-generated commit + push to remote
|
||||
|
||||
|
||||
Keyboard Shortcuts:
|
||||
Shift+Tab - Toggle auto-edit mode (bypass confirmations)
|
||||
|
||||
Direct Commands (executed immediately):
|
||||
ls [path] - List directory contents
|
||||
pwd - Show current directory
|
||||
pwd - Show current directory
|
||||
cd <path> - Change directory
|
||||
cat <file> - View file contents
|
||||
mkdir <dir> - Create directory
|
||||
touch <file>- Create empty file
|
||||
|
||||
Model Configuration:
|
||||
Edit ~/.grok/models.json to add custom models (Claude, GPT, Gemini, etc.)
|
||||
|
||||
For complex operations, just describe what you want in natural language.
|
||||
Examples:
|
||||
"edit package.json and add a new script"
|
||||
@@ -150,7 +148,7 @@ Examples:
|
||||
|
||||
if (modelNames.includes(modelArg)) {
|
||||
agent.setModel(modelArg);
|
||||
updateSetting("model", modelArg);
|
||||
updateCurrentModel(modelArg); // Update project current model
|
||||
const confirmEntry: ChatEntry = {
|
||||
type: "assistant",
|
||||
content: `✓ Switched to model: ${modelArg}`,
|
||||
@@ -172,6 +170,7 @@ Available models: ${modelNames.join(", ")}`,
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (trimmedInput === "/commit-and-push") {
|
||||
const userEntry: ChatEntry = {
|
||||
type: "user",
|
||||
@@ -665,7 +664,7 @@ Respond with ONLY the commit message, no additional text.`;
|
||||
if (key.tab || key.return) {
|
||||
const selectedModel = availableModels[selectedModelIndex];
|
||||
agent.setModel(selectedModel.model);
|
||||
updateSetting("model", selectedModel.model);
|
||||
updateCurrentModel(selectedModel.model); // Update project current model
|
||||
const confirmEntry: ChatEntry = {
|
||||
type: "assistant",
|
||||
content: `✓ Switched to model: ${selectedModel.model}`,
|
||||
|
||||
+41
-63
@@ -6,10 +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";
|
||||
import { getSettingsManager } from "./utils/settings-manager";
|
||||
import { ConfirmationService } from "./utils/confirmation-service";
|
||||
import { createMCPCommand } from "./commands/mcp";
|
||||
import type { ChatCompletionMessageParam } from "openai/resources/chat";
|
||||
@@ -47,27 +44,12 @@ process.on("unhandledRejection", (reason, promise) => {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Ensure user .grok directory exists with default settings
|
||||
// Ensure user settings are initialized
|
||||
function ensureUserSettingsDirectory(): void {
|
||||
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, { recursive: true });
|
||||
}
|
||||
|
||||
// Create default user-settings.json if it doesn't exist
|
||||
if (!fs.existsSync(settingsFile)) {
|
||||
const defaultSettings = {
|
||||
apiKey: "",
|
||||
baseURL: "",
|
||||
defaultModel: "grok-4-latest",
|
||||
};
|
||||
fs.writeFileSync(settingsFile, JSON.stringify(defaultSettings, null, 2));
|
||||
}
|
||||
const manager = getSettingsManager();
|
||||
// This will create default settings if they don't exist
|
||||
manager.loadUserSettings();
|
||||
} catch (error) {
|
||||
// Silently ignore errors during setup
|
||||
}
|
||||
@@ -75,50 +57,33 @@ function ensureUserSettingsDirectory(): void {
|
||||
|
||||
// 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 {
|
||||
ensureUserSettingsDirectory();
|
||||
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;
|
||||
const manager = getSettingsManager();
|
||||
return manager.getApiKey();
|
||||
}
|
||||
|
||||
// Load base URL from user settings if not in environment
|
||||
function loadBaseURL(): string | undefined {
|
||||
// First check environment variables
|
||||
let baseURL = process.env.GROK_BASE_URL;
|
||||
function loadBaseURL(): string {
|
||||
const manager = getSettingsManager();
|
||||
return manager.getBaseURL();
|
||||
}
|
||||
|
||||
if (!baseURL) {
|
||||
// Try to load from user settings file
|
||||
try {
|
||||
ensureUserSettingsDirectory();
|
||||
const homeDir = os.homedir();
|
||||
const settingsFile = path.join(homeDir, ".grok", "user-settings.json");
|
||||
// Save command line settings to user settings file
|
||||
async function saveCommandLineSettings(apiKey?: string, baseURL?: string): Promise<void> {
|
||||
try {
|
||||
const manager = getSettingsManager();
|
||||
|
||||
if (fs.existsSync(settingsFile)) {
|
||||
const settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
|
||||
baseURL = settings.baseURL;
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors, baseURL will remain undefined
|
||||
// Update with command line values
|
||||
if (apiKey) {
|
||||
manager.updateUserSetting('apiKey', apiKey);
|
||||
console.log("✅ API key saved to ~/.grok/user-settings.json");
|
||||
}
|
||||
if (baseURL) {
|
||||
manager.updateUserSetting('baseURL', baseURL);
|
||||
console.log("✅ Base URL saved to ~/.grok/user-settings.json");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("⚠️ Could not save settings to file:", error instanceof Error ? error.message : "Unknown error");
|
||||
}
|
||||
|
||||
return baseURL;
|
||||
}
|
||||
|
||||
// Load model from user settings if not in environment
|
||||
@@ -127,10 +92,10 @@ function loadModel(): string | undefined {
|
||||
let model = process.env.GROK_MODEL;
|
||||
|
||||
if (!model) {
|
||||
// Try to load from local settings file
|
||||
// Use the unified model loading from settings manager
|
||||
try {
|
||||
const settings = loadSettings();
|
||||
model = settings.model;
|
||||
const manager = getSettingsManager();
|
||||
model = manager.getCurrentModel();
|
||||
} catch (error) {
|
||||
// Ignore errors, model will remain undefined
|
||||
}
|
||||
@@ -383,6 +348,11 @@ program
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Save API key and base URL to user settings if provided via command line
|
||||
if (options.apiKey || options.baseUrl) {
|
||||
await saveCommandLineSettings(options.apiKey, options.baseUrl);
|
||||
}
|
||||
|
||||
// Headless mode: process prompt and exit
|
||||
if (options.prompt) {
|
||||
await processPromptHeadless(options.prompt, apiKey, baseURL, model);
|
||||
@@ -392,6 +362,9 @@ program
|
||||
// Interactive mode: launch UI
|
||||
const agent = new GrokAgent(apiKey, baseURL, model);
|
||||
console.log("🤖 Starting Grok CLI Conversational Assistant...\n");
|
||||
|
||||
ensureUserSettingsDirectory();
|
||||
|
||||
render(React.createElement(ChatInterface, { agent }));
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error initializing Grok CLI:", error.message);
|
||||
@@ -443,6 +416,11 @@ gitCommand
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Save API key and base URL to user settings if provided via command line
|
||||
if (options.apiKey || options.baseUrl) {
|
||||
await saveCommandLineSettings(options.apiKey, options.baseUrl);
|
||||
}
|
||||
|
||||
await handleCommitAndPushHeadless(apiKey, baseURL, model);
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error during git commit-and-push:", error.message);
|
||||
|
||||
+26
-57
@@ -1,87 +1,56 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import { getSettingsManager } from "../utils/settings-manager";
|
||||
import { MCPServerConfig } from "./client";
|
||||
|
||||
const CONFIG_DIR = path.join(process.cwd(), ".grok");
|
||||
const SETTINGS_FILE = path.join(CONFIG_DIR, "settings.json");
|
||||
|
||||
export interface Settings {
|
||||
model?: string;
|
||||
mcpServers?: Record<string, MCPServerConfig>;
|
||||
}
|
||||
|
||||
export interface MCPConfig {
|
||||
servers: MCPServerConfig[];
|
||||
}
|
||||
|
||||
function loadSettings(): Settings {
|
||||
try {
|
||||
if (!fs.existsSync(SETTINGS_FILE)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const settingsData = fs.readFileSync(SETTINGS_FILE, "utf8");
|
||||
return JSON.parse(settingsData);
|
||||
} catch (error) {
|
||||
console.warn("Failed to load settings:", error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings(settings: Settings): void {
|
||||
try {
|
||||
// Ensure config directory exists
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
||||
} catch (error) {
|
||||
console.error("Failed to save settings:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load MCP configuration from project settings
|
||||
*/
|
||||
export function loadMCPConfig(): MCPConfig {
|
||||
const settings = loadSettings();
|
||||
const servers = settings.mcpServers ? Object.values(settings.mcpServers) : [];
|
||||
const manager = getSettingsManager();
|
||||
const projectSettings = manager.loadProjectSettings();
|
||||
const servers = projectSettings.mcpServers ? Object.values(projectSettings.mcpServers) : [];
|
||||
return { servers };
|
||||
}
|
||||
|
||||
export function saveMCPConfig(config: MCPConfig): void {
|
||||
const settings = loadSettings();
|
||||
settings.mcpServers = {};
|
||||
const manager = getSettingsManager();
|
||||
const mcpServers: Record<string, MCPServerConfig> = {};
|
||||
|
||||
// Convert servers array to object keyed by name
|
||||
for (const server of config.servers) {
|
||||
settings.mcpServers[server.name] = server;
|
||||
mcpServers[server.name] = server;
|
||||
}
|
||||
|
||||
saveSettings(settings);
|
||||
manager.updateProjectSetting('mcpServers', mcpServers);
|
||||
}
|
||||
|
||||
export function addMCPServer(config: MCPServerConfig): void {
|
||||
const settings = loadSettings();
|
||||
if (!settings.mcpServers) {
|
||||
settings.mcpServers = {};
|
||||
}
|
||||
const manager = getSettingsManager();
|
||||
const projectSettings = manager.loadProjectSettings();
|
||||
const mcpServers = projectSettings.mcpServers || {};
|
||||
|
||||
settings.mcpServers[config.name] = config;
|
||||
saveSettings(settings);
|
||||
mcpServers[config.name] = config;
|
||||
manager.updateProjectSetting('mcpServers', mcpServers);
|
||||
}
|
||||
|
||||
export function removeMCPServer(serverName: string): void {
|
||||
const settings = loadSettings();
|
||||
if (settings.mcpServers) {
|
||||
delete settings.mcpServers[serverName];
|
||||
saveSettings(settings);
|
||||
const manager = getSettingsManager();
|
||||
const projectSettings = manager.loadProjectSettings();
|
||||
const mcpServers = projectSettings.mcpServers;
|
||||
|
||||
if (mcpServers) {
|
||||
delete mcpServers[serverName];
|
||||
manager.updateProjectSetting('mcpServers', mcpServers);
|
||||
}
|
||||
}
|
||||
|
||||
export function getMCPServer(serverName: string): MCPServerConfig | undefined {
|
||||
const settings = loadSettings();
|
||||
return settings.mcpServers?.[serverName];
|
||||
const manager = getSettingsManager();
|
||||
const projectSettings = manager.loadProjectSettings();
|
||||
return projectSettings.mcpServers?.[serverName];
|
||||
}
|
||||
|
||||
// Predefined server configurations
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
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";
|
||||
import { getSettingsManager } from "../../utils/settings-manager";
|
||||
|
||||
interface ApiKeyInputProps {
|
||||
onApiKeySet: (agent: GrokAgent) => void;
|
||||
}
|
||||
|
||||
interface UserSettings {
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export default function ApiKeyInput({ onApiKeySet }: ApiKeyInputProps) {
|
||||
const [input, setInput] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
@@ -60,33 +54,10 @@ export default function ApiKeyInput({ onApiKeySet }: ApiKeyInputProps) {
|
||||
// Set environment variable for current process
|
||||
process.env.GROK_API_KEY = apiKey;
|
||||
|
||||
// Save to .grok/user-settings.json
|
||||
// Save to user settings
|
||||
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 });
|
||||
|
||||
const manager = getSettingsManager();
|
||||
manager.updateUserSetting('apiKey', apiKey);
|
||||
console.log(`\n✅ API key saved to ~/.grok/user-settings.json`);
|
||||
} catch (error) {
|
||||
console.log('\n⚠️ Could not save API key to settings file');
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Box, Text } from "ink";
|
||||
|
||||
interface ModelOption {
|
||||
model: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface ModelSelectionProps {
|
||||
@@ -34,9 +33,6 @@ export function ModelSelection({
|
||||
>
|
||||
{modelOption.model}
|
||||
</Text>
|
||||
<Box marginLeft={1}>
|
||||
<Text color="gray">{modelOption.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
<Box marginTop={1}>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { getSettingsManager, UserSettings, ProjectSettings } from './settings-manager';
|
||||
|
||||
export interface ModelOption {
|
||||
model: string;
|
||||
}
|
||||
|
||||
export type ModelConfig = string;
|
||||
|
||||
// Re-export interfaces for backward compatibility
|
||||
export { UserSettings, ProjectSettings };
|
||||
|
||||
/**
|
||||
* Get the effective current model
|
||||
* Priority: project current model > user default model > system default
|
||||
*/
|
||||
export function getCurrentModel(): string {
|
||||
const manager = getSettingsManager();
|
||||
return manager.getCurrentModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load model configuration
|
||||
* Priority: user-settings.json models > default hardcoded
|
||||
*/
|
||||
export function loadModelConfig(): ModelOption[] {
|
||||
const manager = getSettingsManager();
|
||||
const models = manager.getAvailableModels();
|
||||
|
||||
return models.map(model => ({
|
||||
model: model.trim()
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default models list
|
||||
*/
|
||||
export function getDefaultModels(): string[] {
|
||||
const manager = getSettingsManager();
|
||||
return manager.getAvailableModels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current model in project settings
|
||||
*/
|
||||
export function updateCurrentModel(modelName: string): void {
|
||||
const manager = getSettingsManager();
|
||||
manager.setCurrentModel(modelName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's default model preference
|
||||
*/
|
||||
export function updateDefaultModel(modelName: string): void {
|
||||
const manager = getSettingsManager();
|
||||
manager.updateUserSetting('defaultModel', modelName);
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
/**
|
||||
* User-level settings stored in ~/.grok/user-settings.json
|
||||
* These are global settings that apply across all projects
|
||||
*/
|
||||
export interface UserSettings {
|
||||
apiKey?: string; // Grok API key
|
||||
baseURL?: string; // API base URL
|
||||
defaultModel?: string; // User's preferred default model
|
||||
models?: string[]; // Available models list
|
||||
}
|
||||
|
||||
/**
|
||||
* Project-level settings stored in .grok/settings.json
|
||||
* These are project-specific settings
|
||||
*/
|
||||
export interface ProjectSettings {
|
||||
model?: string; // Current model for this project
|
||||
mcpServers?: Record<string, any>; // MCP server configurations
|
||||
}
|
||||
|
||||
/**
|
||||
* Default values for user settings
|
||||
*/
|
||||
const DEFAULT_USER_SETTINGS: Partial<UserSettings> = {
|
||||
baseURL: "https://api.x.ai/v1",
|
||||
defaultModel: "grok-4-latest",
|
||||
models: [
|
||||
"grok-4-latest",
|
||||
"grok-3-latest",
|
||||
"grok-3-fast",
|
||||
"grok-3-mini-fast"
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Default values for project settings
|
||||
*/
|
||||
const DEFAULT_PROJECT_SETTINGS: Partial<ProjectSettings> = {
|
||||
model: "grok-4-latest"
|
||||
};
|
||||
|
||||
/**
|
||||
* Unified settings manager that handles both user-level and project-level settings
|
||||
*/
|
||||
export class SettingsManager {
|
||||
private static instance: SettingsManager;
|
||||
|
||||
private userSettingsPath: string;
|
||||
private projectSettingsPath: string;
|
||||
|
||||
private constructor() {
|
||||
// User settings path: ~/.grok/user-settings.json
|
||||
this.userSettingsPath = path.join(os.homedir(), '.grok', 'user-settings.json');
|
||||
|
||||
// Project settings path: .grok/settings.json (in current working directory)
|
||||
this.projectSettingsPath = path.join(process.cwd(), '.grok', 'settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*/
|
||||
public static getInstance(): SettingsManager {
|
||||
if (!SettingsManager.instance) {
|
||||
SettingsManager.instance = new SettingsManager();
|
||||
}
|
||||
return SettingsManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure directory exists for a given file path
|
||||
*/
|
||||
private ensureDirectoryExists(filePath: string): void {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user settings from ~/.grok/user-settings.json
|
||||
*/
|
||||
public loadUserSettings(): UserSettings {
|
||||
try {
|
||||
if (!fs.existsSync(this.userSettingsPath)) {
|
||||
// Create default user settings if file doesn't exist
|
||||
this.saveUserSettings(DEFAULT_USER_SETTINGS);
|
||||
return { ...DEFAULT_USER_SETTINGS };
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(this.userSettingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content);
|
||||
|
||||
// Merge with defaults to ensure all required fields exist
|
||||
return { ...DEFAULT_USER_SETTINGS, ...settings };
|
||||
} catch (error) {
|
||||
console.warn('Failed to load user settings:', error instanceof Error ? error.message : 'Unknown error');
|
||||
return { ...DEFAULT_USER_SETTINGS };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user settings to ~/.grok/user-settings.json
|
||||
*/
|
||||
public saveUserSettings(settings: Partial<UserSettings>): void {
|
||||
try {
|
||||
this.ensureDirectoryExists(this.userSettingsPath);
|
||||
|
||||
// Read existing settings directly to avoid recursion
|
||||
let existingSettings: UserSettings = { ...DEFAULT_USER_SETTINGS };
|
||||
if (fs.existsSync(this.userSettingsPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(this.userSettingsPath, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
existingSettings = { ...DEFAULT_USER_SETTINGS, ...parsed };
|
||||
} catch (error) {
|
||||
// If file is corrupted, use defaults
|
||||
console.warn('Corrupted user settings file, using defaults');
|
||||
}
|
||||
}
|
||||
|
||||
const mergedSettings = { ...existingSettings, ...settings };
|
||||
|
||||
fs.writeFileSync(
|
||||
this.userSettingsPath,
|
||||
JSON.stringify(mergedSettings, null, 2),
|
||||
{ mode: 0o600 } // Secure permissions for API key
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to save user settings:', error instanceof Error ? error.message : 'Unknown error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific user setting
|
||||
*/
|
||||
public updateUserSetting<K extends keyof UserSettings>(key: K, value: UserSettings[K]): void {
|
||||
const settings = { [key]: value } as Partial<UserSettings>;
|
||||
this.saveUserSettings(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific user setting
|
||||
*/
|
||||
public getUserSetting<K extends keyof UserSettings>(key: K): UserSettings[K] {
|
||||
const settings = this.loadUserSettings();
|
||||
return settings[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load project settings from .grok/settings.json
|
||||
*/
|
||||
public loadProjectSettings(): ProjectSettings {
|
||||
try {
|
||||
if (!fs.existsSync(this.projectSettingsPath)) {
|
||||
// Create default project settings if file doesn't exist
|
||||
this.saveProjectSettings(DEFAULT_PROJECT_SETTINGS);
|
||||
return { ...DEFAULT_PROJECT_SETTINGS };
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(this.projectSettingsPath, 'utf-8');
|
||||
const settings = JSON.parse(content);
|
||||
|
||||
// Merge with defaults
|
||||
return { ...DEFAULT_PROJECT_SETTINGS, ...settings };
|
||||
} catch (error) {
|
||||
console.warn('Failed to load project settings:', error instanceof Error ? error.message : 'Unknown error');
|
||||
return { ...DEFAULT_PROJECT_SETTINGS };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save project settings to .grok/settings.json
|
||||
*/
|
||||
public saveProjectSettings(settings: Partial<ProjectSettings>): void {
|
||||
try {
|
||||
this.ensureDirectoryExists(this.projectSettingsPath);
|
||||
|
||||
// Read existing settings directly to avoid recursion
|
||||
let existingSettings: ProjectSettings = { ...DEFAULT_PROJECT_SETTINGS };
|
||||
if (fs.existsSync(this.projectSettingsPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(this.projectSettingsPath, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
existingSettings = { ...DEFAULT_PROJECT_SETTINGS, ...parsed };
|
||||
} catch (error) {
|
||||
// If file is corrupted, use defaults
|
||||
console.warn('Corrupted project settings file, using defaults');
|
||||
}
|
||||
}
|
||||
|
||||
const mergedSettings = { ...existingSettings, ...settings };
|
||||
|
||||
fs.writeFileSync(
|
||||
this.projectSettingsPath,
|
||||
JSON.stringify(mergedSettings, null, 2)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to save project settings:', error instanceof Error ? error.message : 'Unknown error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific project setting
|
||||
*/
|
||||
public updateProjectSetting<K extends keyof ProjectSettings>(key: K, value: ProjectSettings[K]): void {
|
||||
const settings = { [key]: value } as Partial<ProjectSettings>;
|
||||
this.saveProjectSettings(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific project setting
|
||||
*/
|
||||
public getProjectSetting<K extends keyof ProjectSettings>(key: K): ProjectSettings[K] {
|
||||
const settings = this.loadProjectSettings();
|
||||
return settings[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current model with proper fallback logic:
|
||||
* 1. Project-specific model setting
|
||||
* 2. User's default model
|
||||
* 3. System default
|
||||
*/
|
||||
public getCurrentModel(): string {
|
||||
const projectModel = this.getProjectSetting('model');
|
||||
if (projectModel) {
|
||||
return projectModel;
|
||||
}
|
||||
|
||||
const userDefaultModel = this.getUserSetting('defaultModel');
|
||||
if (userDefaultModel) {
|
||||
return userDefaultModel;
|
||||
}
|
||||
|
||||
return DEFAULT_PROJECT_SETTINGS.model || 'grok-4-latest';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current model for the project
|
||||
*/
|
||||
public setCurrentModel(model: string): void {
|
||||
this.updateProjectSetting('model', model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available models list from user settings
|
||||
*/
|
||||
public getAvailableModels(): string[] {
|
||||
const models = this.getUserSetting('models');
|
||||
return models || DEFAULT_USER_SETTINGS.models || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key from user settings or environment
|
||||
*/
|
||||
public getApiKey(): string | undefined {
|
||||
// First check environment variable
|
||||
const envApiKey = process.env.GROK_API_KEY;
|
||||
if (envApiKey) {
|
||||
return envApiKey;
|
||||
}
|
||||
|
||||
// Then check user settings
|
||||
return this.getUserSetting('apiKey');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base URL from user settings or environment
|
||||
*/
|
||||
public getBaseURL(): string {
|
||||
// First check environment variable
|
||||
const envBaseURL = process.env.GROK_BASE_URL;
|
||||
if (envBaseURL) {
|
||||
return envBaseURL;
|
||||
}
|
||||
|
||||
// Then check user settings
|
||||
const userBaseURL = this.getUserSetting('baseURL');
|
||||
return userBaseURL || DEFAULT_USER_SETTINGS.baseURL || 'https://api.x.ai/v1';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get the singleton instance
|
||||
*/
|
||||
export function getSettingsManager(): SettingsManager {
|
||||
return SettingsManager.getInstance();
|
||||
}
|
||||
+3
-67
@@ -1,68 +1,4 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
// This file is kept for potential future convenience functions
|
||||
// All settings management should use getSettingsManager() from './settings-manager'
|
||||
|
||||
interface Settings {
|
||||
model?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const SETTINGS_DIR = ".grok";
|
||||
const SETTINGS_FILE = "settings.json";
|
||||
|
||||
function getSettingsPath(): string {
|
||||
return path.join(process.cwd(), SETTINGS_DIR, SETTINGS_FILE);
|
||||
}
|
||||
|
||||
function ensureSettingsDirectory(): void {
|
||||
const settingsDir = path.join(process.cwd(), SETTINGS_DIR);
|
||||
if (!fs.existsSync(settingsDir)) {
|
||||
fs.mkdirSync(settingsDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: Settings = {
|
||||
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");
|
||||
return JSON.parse(settingsContent);
|
||||
} catch (error) {
|
||||
console.warn("Failed to load settings:", error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateSetting<K extends keyof Settings>(
|
||||
key: K,
|
||||
value: Settings[K]
|
||||
): void {
|
||||
const currentSettings = loadSettings();
|
||||
currentSettings[key] = value;
|
||||
saveSettings(currentSettings);
|
||||
}
|
||||
|
||||
export function getSetting<K extends keyof Settings>(key: K): Settings[K] {
|
||||
const settings = loadSettings();
|
||||
return settings[key];
|
||||
}
|
||||
export { getSettingsManager, UserSettings, ProjectSettings } from './settings-manager';
|
||||
Reference in new issue
Block a user