UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

250 lines 9.91 kB
import { config as loadEnv } from 'dotenv'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { loadAllMCPConfigs, loadAllProviderConfigs, } from '../config/mcp-config-loader.js'; import { getConfigPath } from '../config/paths.js'; import { loadPreferences } from '../config/preferences.js'; import { defaultTheme, getThemeColors } from '../config/themes.js'; import { logError } from '../utils/message-queue.js'; // Load .env file from working directory (shell environment takes precedence) // Suppress dotenv console output by temporarily redirecting stdout const envPath = join(process.cwd(), '.env'); if (existsSync(envPath)) { const originalWrite = process.stdout.write.bind(process.stdout); process.stdout.write = () => true; try { loadEnv({ path: envPath }); } finally { process.stdout.write = originalWrite; } } // Hold a map of what config files are where export const confDirMap = {}; // Find the closest config file for the requested configuration file export function getClosestConfigFile(fileName) { try { const configDir = getConfigPath(); // If NANOCODER_CONFIG_DIR is explicitly set, skip cwd and home checks // and use only the config directory (important for tests and explicit overrides) const isExplicitConfigDir = Boolean(process.env.NANOCODER_CONFIG_DIR); if (!isExplicitConfigDir) { // First, lets check for a working directory config const cwdPath = join(process.cwd(), fileName); // nosemgrep if (existsSync(cwdPath)) { // nosemgrep confDirMap[fileName] = cwdPath; // nosemgrep return cwdPath; // nosemgrep } } // Last, lets look for an user level config. // If the file doesn't exist, create it const configPath = join(configDir, fileName); // nosemgrep if (!existsSync(configPath)) { // nosemgrep createDefaultConfFile(configDir, fileName); } confDirMap[fileName] = configPath; // nosemgrep return configPath; // nosemgrep } catch (error) { logError(`Failed to load ${fileName}: ${String(error)}`); } // The code should never hit this, but it makes the TS compiler happy. return fileName; } function createDefaultConfFile(filePath, fileName) { try { // If we cant find any, lets assume this is the first user run, create the // correct file and direct the user to configure them correctly, const configFilePath = join(filePath, fileName); // nosemgrep if (!existsSync(configFilePath)) { // nosemgrep // Maybe add a better sample config? const sampleConfig = {}; mkdirSync(filePath, { recursive: true }); writeFileSync(configFilePath, // nosemgrep JSON.stringify(sampleConfig, null, 2), 'utf-8'); } } catch (error) { logError(`Failed to write ${filePath}: ${String(error)}`); } } // Try to load auto-compact config from a specific path // Returns the config if found and valid, null otherwise function tryLoadAutoCompactFromPath(configPath, defaults) { if (!existsSync(configPath)) { return null; } try { const rawData = readFileSync(configPath, 'utf-8'); const config = JSON.parse(rawData); const autoCompact = config.nanocoder?.autoCompact; if (autoCompact && typeof autoCompact === 'object') { return { enabled: autoCompact.enabled !== undefined ? Boolean(autoCompact.enabled) : defaults.enabled, threshold: validateThreshold(autoCompact.threshold ?? defaults.threshold), mode: validateMode(autoCompact.mode ?? defaults.mode), notifyUser: autoCompact.notifyUser !== undefined ? Boolean(autoCompact.notifyUser) : defaults.notifyUser, }; } } catch (error) { logError(`Failed to load auto-compact config from ${configPath}: ${String(error)}`); } return null; } // Load auto-compact configuration and Returns default config if not specified function loadAutoCompactConfig() { const defaults = { enabled: true, threshold: 60, mode: 'conservative', notifyUser: true, }; // Try to load from project-level config first const projectConfigPath = join(process.cwd(), 'agents.config.json'); const projectConfig = tryLoadAutoCompactFromPath(projectConfigPath, defaults); if (projectConfig) { return projectConfig; } // Try global config const configDir = getConfigPath(); const globalConfigPath = join(configDir, 'agents.config.json'); const globalConfig = tryLoadAutoCompactFromPath(globalConfigPath, defaults); if (globalConfig) { return globalConfig; } return defaults; } // Validate and clamp threshold to valid range (50-95) function validateThreshold(threshold) { const num = typeof threshold === 'number' ? threshold : 60; return Math.max(50, Math.min(95, Math.round(num))); } // Validate compression mode function validateMode(mode) { if (mode === 'default' || mode === 'aggressive' || mode === 'conservative') { return mode; } return 'conservative'; } // Try to load session config from a specific path // Returns the config if found and valid, null otherwise function tryLoadSessionsFromPath(configPath, defaults) { if (!existsSync(configPath)) { return null; } try { const rawData = readFileSync(configPath, 'utf-8'); const config = JSON.parse(rawData); const sessions = config.nanocoder?.sessions; if (sessions && typeof sessions === 'object') { const normalizeSessionNumber = (value, min, fallback) => { if (typeof value === 'number' && Number.isFinite(value)) { return Math.max(min, value); } return fallback; }; return { autoSave: sessions.autoSave !== undefined ? Boolean(sessions.autoSave) : defaults.autoSave, saveInterval: normalizeSessionNumber(sessions.saveInterval, 1000, // Minimum 1 second defaults.saveInterval ?? 30000), maxSessions: normalizeSessionNumber(sessions.maxSessions, 1, defaults.maxSessions ?? 100), maxMessages: normalizeSessionNumber(sessions.maxMessages, 1, defaults.maxMessages ?? 1000), retentionDays: normalizeSessionNumber(sessions.retentionDays, 1, defaults.retentionDays ?? 30), directory: sessions.directory || defaults.directory, }; } } catch (error) { logError(`Failed to load session config from ${configPath}: ${String(error)}`); } return null; } // Load session configuration and Returns default config if not specified function loadSessionConfig() { const defaults = { autoSave: true, saveInterval: 30000, // 30 seconds maxSessions: 100, maxMessages: 1000, retentionDays: 30, directory: '', }; // Try to load from project-level config first const projectConfigPath = join(process.cwd(), 'agents.config.json'); const projectConfig = tryLoadSessionsFromPath(projectConfigPath, defaults); if (projectConfig) { return projectConfig; } // Try global config const configDir = getConfigPath(); const globalConfigPath = join(configDir, 'agents.config.json'); const globalConfig = tryLoadSessionsFromPath(globalConfigPath, defaults); if (globalConfig) { return globalConfig; } return defaults; } // Function to load app configuration from agents.config.json if it exists function loadAppConfig() { // Load providers from the new hierarchical configuration system const providers = loadAllProviderConfigs(); // Load MCP servers from the new hierarchical configuration system const mcpServersWithSource = loadAllMCPConfigs(); const mcpServers = mcpServersWithSource.map(item => item.server); // Load auto-compact configuration const autoCompact = loadAutoCompactConfig(); // Load session configuration const sessions = loadSessionConfig(); return { providers, mcpServers, autoCompact, sessions, }; } let _appConfig = null; /** * Lazy-loaded app config to avoid circular dependencies during module initialization * @public */ export function getAppConfig() { if (!_appConfig) { _appConfig = loadAppConfig(); } return _appConfig; } // Function to reload the app configuration (useful after config file changes) export function reloadAppConfig() { _appConfig = loadAppConfig(); } // Function to clear the cached app configuration (useful for testing) export function clearAppConfig() { _appConfig = null; } let cachedColors = null; export function getColors() { if (!cachedColors) { const preferences = loadPreferences(); const selectedTheme = preferences.selectedTheme || defaultTheme; cachedColors = getThemeColors(selectedTheme); } return cachedColors; } // Get the package root directory (where this module is installed) const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Go up from dist/config to package root, then to source/app/prompts/main-prompt.md // This works because source/app/prompts/main-prompt.md is included in the package.json files array export const promptPath = join(__dirname, '../../source/app/prompts/main-prompt.md'); //# sourceMappingURL=index.js.map