Files
grok-cli/src/utils/settings-manager.ts
T

379 lines
11 KiB
TypeScript

import * as fs from "fs";
import * as path from "path";
import * as os from "os";
/**
* Current settings version - increment this when adding new models or changing settings structure
* This triggers automatic migration for existing users
*/
const SETTINGS_VERSION = 2;
/**
* 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
settingsVersion?: number; // Version for migration tracking
}
/**
* 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-code-fast-1",
models: [
// Grok 4.1 Fast models (2M context, latest - November 2025)
"grok-4-1-fast-reasoning",
"grok-4-1-fast-non-reasoning",
// Grok 4 Fast models (2M context)
"grok-4-fast-reasoning",
"grok-4-fast-non-reasoning",
// Grok 4 flagship (256K context)
"grok-4",
"grok-4-latest",
// Grok Code (optimized for coding, 256K context)
"grok-code-fast-1",
// Grok 3 models (131K context)
"grok-3",
"grok-3-latest",
"grok-3-fast",
"grok-3-mini",
"grok-3-mini-fast",
],
};
/**
* Default values for project settings
*/
const DEFAULT_PROJECT_SETTINGS: Partial<ProjectSettings> = {
model: "grok-code-fast-1",
};
/**
* 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
const newSettings = { ...DEFAULT_USER_SETTINGS, settingsVersion: SETTINGS_VERSION };
this.saveUserSettings(newSettings);
return newSettings;
}
const content = fs.readFileSync(this.userSettingsPath, "utf-8");
const settings = JSON.parse(content);
// Check if migration is needed
const currentVersion = settings.settingsVersion || 1;
if (currentVersion < SETTINGS_VERSION) {
const migratedSettings = this.migrateSettings(settings, currentVersion);
this.saveUserSettings(migratedSettings);
return migratedSettings;
}
// 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 };
}
}
/**
* Migrate settings from an older version to the current version
*/
private migrateSettings(settings: UserSettings, fromVersion: number): UserSettings {
let migrated = { ...settings };
// Migration from version 1 to 2: Add new Grok 4.1 and Grok 4 Fast models
if (fromVersion < 2) {
const defaultModels = DEFAULT_USER_SETTINGS.models || [];
const existingModels = new Set(migrated.models || []);
// Add any new models that don't exist in user's current list
const newModels = defaultModels.filter(model => !existingModels.has(model));
// Prepend new models to the list (newest models first)
migrated.models = [...newModels, ...(migrated.models || [])];
}
// Add future migrations here:
// if (fromVersion < 3) { ... }
migrated.settingsVersion = SETTINGS_VERSION;
return migrated;
}
/**
* 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-code-fast-1";
}
/**
* 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();
}