Files
codetyper.cli/src/services/config.ts
Carlos Gutierrez c839fc4d68 Add BRAIN_DISABLED flag and fix Ollama tool call formatting
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)
2026-02-02 13:25:38 -05:00

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,
};
};