Terminal-based AI coding agent with interactive TUI for autonomous code generation.

Features:
  - Interactive TUI with React/Ink
  - Autonomous agent with tool calls (bash, read, write, edit, glob, grep)
  - Permission system with pattern-based rules
  - Session management with auto-compaction
  - Dual providers: GitHub Copilot and Ollama
  - MCP server integration
  - Todo panel and theme system
  - Streaming responses
  - GitHub-compatible project context
This commit is contained in:
2026-01-27 23:33:06 -05:00
commit 0062e5d9d9
521 changed files with 66418 additions and 0 deletions

168
src/services/config.ts Normal file
View File

@@ -0,0 +1,168 @@
/**
* 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();
/**
* Load configuration from file
*/
export const loadConfig = async (): Promise<void> => {
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();
}
};
/**
* 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,
};
};