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:
z23ccandIsmail Pelaseyed authored and GitHub committed 2025-07-31 10:07:41 +02:00
1 parent 346097acab
commit 5a4b2c8e25
12 files changed
+534 -251

No files matched your search

+87 -4
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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": {
+3 -2
View File
@@ -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();
+17 -18
View File
@@ -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
View File
@@ -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
View File
@@ -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
+4 -33
View File
@@ -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');
-4
View 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}>
+56
View File
@@ -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);
}
+294
View File
@@ -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
View File
@@ -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';