Features: - Add BRAIN_DISABLED feature flag to hide all Brain functionality - When enabled, hides Brain banner, status indicator, menu, and commands - Flag location: src/constants/brain.ts Fixes: - Fix Ollama 400 error by properly formatting tool_calls in messages - Update OllamaMessage type to include tool_calls field - Fix Brain menu keyboard not working (add missing modes to isMenuOpen) UI Changes: - Remove "^Tab toggle mode" hint from status bar - Remove "ctrl+t to hide todos" hint from status bar Files modified: - src/constants/brain.ts (add BRAIN_DISABLED flag) - src/types/ollama.ts (add tool_calls to OllamaMessage) - src/providers/ollama/chat.ts (format tool_calls in messages) - src/tui-solid/components/header.tsx (hide Brain UI when disabled) - src/tui-solid/components/status-bar.tsx (remove hints) - src/tui-solid/components/command-menu.tsx (filter brain command) - src/tui-solid/components/input-area.tsx (fix isMenuOpen modes) - src/tui-solid/routes/session.tsx (skip brain menu when disabled) - src/services/brain.ts (early return when disabled) - src/services/chat-tui/initialize.ts (skip brain init when disabled)
187 lines
4.0 KiB
TypeScript
187 lines
4.0 KiB
TypeScript
/**
|
|
* Configuration management for CodeTyper CLI
|
|
*/
|
|
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import type { Config, Provider } from "@/types/index";
|
|
import { DIRS, FILES } from "@constants/paths";
|
|
|
|
/**
|
|
* Default configuration values
|
|
*/
|
|
const getDefaults = (): Config => ({
|
|
provider: "copilot",
|
|
model: "auto",
|
|
theme: "default",
|
|
maxIterations: 20,
|
|
timeout: 30000,
|
|
cascadeEnabled: true,
|
|
protectedPaths: [
|
|
".git",
|
|
"node_modules",
|
|
".env",
|
|
".env.local",
|
|
".env.production",
|
|
"dist",
|
|
"build",
|
|
".next",
|
|
"__pycache__",
|
|
"venv",
|
|
".venv",
|
|
],
|
|
});
|
|
|
|
/**
|
|
* Environment variable mapping for providers
|
|
*/
|
|
const PROVIDER_ENV_VARS: Record<Provider, string> = {
|
|
copilot: "GITHUB_COPILOT_TOKEN",
|
|
ollama: "OLLAMA_HOST",
|
|
};
|
|
|
|
/**
|
|
* Config state (singleton pattern using closure)
|
|
*/
|
|
let configState: Config = getDefaults();
|
|
let configLoaded = false;
|
|
let configLoadPromise: Promise<void> | null = null;
|
|
|
|
/**
|
|
* Load configuration from file (with caching)
|
|
*/
|
|
export const loadConfig = async (): Promise<void> => {
|
|
// Return cached config if already loaded
|
|
if (configLoaded) {
|
|
return;
|
|
}
|
|
|
|
// If loading is in progress, wait for it
|
|
if (configLoadPromise) {
|
|
return configLoadPromise;
|
|
}
|
|
|
|
// Start loading
|
|
configLoadPromise = (async () => {
|
|
try {
|
|
const data = await fs.readFile(FILES.config, "utf-8");
|
|
const loaded = JSON.parse(data);
|
|
|
|
// Clean up deprecated keys
|
|
delete loaded.models;
|
|
|
|
configState = { ...getDefaults(), ...loaded };
|
|
} catch {
|
|
// Config file doesn't exist or is invalid, use defaults
|
|
configState = getDefaults();
|
|
}
|
|
configLoaded = true;
|
|
})();
|
|
|
|
return configLoadPromise;
|
|
};
|
|
|
|
/**
|
|
* Save configuration to file
|
|
*/
|
|
export const saveConfig = async (): Promise<void> => {
|
|
try {
|
|
await fs.mkdir(DIRS.config, { recursive: true });
|
|
await fs.writeFile(
|
|
FILES.config,
|
|
JSON.stringify(configState, null, 2),
|
|
"utf-8",
|
|
);
|
|
} catch (error) {
|
|
throw new Error(`Failed to save config: ${error}`);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get configuration value
|
|
*/
|
|
export const getConfigValue = <K extends keyof Config>(key: K): Config[K] => {
|
|
return configState[key];
|
|
};
|
|
|
|
/**
|
|
* Set configuration value
|
|
*/
|
|
export const setConfigValue = <K extends keyof Config>(
|
|
key: K,
|
|
value: Config[K],
|
|
): void => {
|
|
configState[key] = value;
|
|
};
|
|
|
|
/**
|
|
* Get full configuration
|
|
*/
|
|
export const getAllConfig = (): Config => ({ ...configState });
|
|
|
|
/**
|
|
* Get API key for provider from environment
|
|
*/
|
|
export const getApiKey = (provider?: Provider): string | undefined => {
|
|
const targetProvider = provider ?? configState.provider;
|
|
const envVar = PROVIDER_ENV_VARS[targetProvider];
|
|
return envVar ? process.env[envVar] : undefined;
|
|
};
|
|
|
|
/**
|
|
* Get configured model
|
|
*/
|
|
export const getModel = (): string => {
|
|
return configState.model ?? "auto";
|
|
};
|
|
|
|
/**
|
|
* Get config file path
|
|
*/
|
|
export const getConfigPath = (): string => FILES.config;
|
|
|
|
/**
|
|
* Check if a path is protected
|
|
*/
|
|
export const isProtectedPath = (filePath: string): boolean => {
|
|
const normalizedPath = path.normalize(filePath);
|
|
return configState.protectedPaths.some((protectedPath) =>
|
|
normalizedPath.includes(protectedPath),
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Reset configuration to defaults
|
|
*/
|
|
export const resetConfig = (): void => {
|
|
configState = getDefaults();
|
|
};
|
|
|
|
/**
|
|
* Initialize and get config (convenience function)
|
|
*/
|
|
export const getConfig = async (): Promise<{
|
|
get: <K extends keyof Config>(key: K) => Config[K];
|
|
set: <K extends keyof Config>(key: K, value: Config[K]) => void;
|
|
getAll: () => Config;
|
|
save: () => Promise<void>;
|
|
getApiKey: (provider?: Provider) => string | undefined;
|
|
getModel: () => string;
|
|
getConfigPath: () => string;
|
|
isProtectedPath: (filePath: string) => boolean;
|
|
reset: () => void;
|
|
}> => {
|
|
await loadConfig();
|
|
return {
|
|
get: getConfigValue,
|
|
set: setConfigValue,
|
|
getAll: getAllConfig,
|
|
save: saveConfig,
|
|
getApiKey,
|
|
getModel,
|
|
getConfigPath,
|
|
isProtectedPath,
|
|
reset: resetConfig,
|
|
};
|
|
};
|