""" Configuration management module """ import os import sys import shutil from pathlib import Path from typing import Optional class ConfigManager: def __init__(self): self.config_dir = self._get_config_dir() self.env_file = self.config_dir / '.env' self.templates_dir = self.config_dir / 'templates' self.rules_dir = self.config_dir / 'rules' self.prompts_dir = self.config_dir / 'prompts' def _get_config_dir(self) -> Path: """Get platform-specific config directory""" if sys.platform == 'win32': # Windows: %USERPROFILE%\.config\madomeda base = Path(os.environ.get('USERPROFILE', os.path.expanduser('~'))) config_dir = base / '.config' / 'madomeda' elif sys.platform == 'darwin': # macOS: ~/.config/madomeda config_dir = Path.home() / '.config' / 'madomeda' else: # Linux: ~/.config/madomeda config_dir = Path.home() / '.config' / 'madomeda' return config_dir def ensure_config_exists(self): """Ensure configuration directory exists with default files""" if not self.config_dir.exists(): self.config_dir.mkdir(parents=True, exist_ok=True) self._create_default_config() # Ensure subdirectories exist self.templates_dir.mkdir(exist_ok=True) self.rules_dir.mkdir(exist_ok=True) self.prompts_dir.mkdir(exist_ok=True) # Copy defaults if they don't exist if not (self.templates_dir / 'default.json').exists(): self._create_default_template() if not list(self.rules_dir.glob('*.json')): self._create_default_rules() if not (self.prompts_dir / 'default.txt').exists(): self._create_default_prompt() def _create_default_config(self): """Create default .env file""" default_env = """# Madomeda Configuration # OpenAI-compatible API settings # API endpoint URL OPENAI_API_URL=https://api.openai.com/v1 # Model to use OPENAI_MODEL=gpt-4 # API Key options: # 1. Empty string "" = No API key needed (LLM features disabled) # 2. $ENV_VAR_NAME = Read from environment variable # 3. Direct key = Use the key (WARNING: security risk in plain text) OPENAI_API_KEY="" """ with open(self.env_file, 'w', encoding='utf-8') as f: f.write(default_env) def _create_default_template(self): """Create default template""" import json default_template = { "title": "heur|orig:text", "created": "heur|orig:datetime", "changed": "heur|orig:datetime", "authors": "heur|orig:list", "version": "heur|orig:text", "tags": "heur|orig|ai default:list" } with open(self.templates_dir / 'default.json', 'w', encoding='utf-8') as f: json.dump(default_template, f, indent=2) def _create_default_rules(self): """Create default rules""" import json rules = { '01_lowercase_keys.json': { "name": "lowercase_keys", "description": "All frontmatter keys must be lowercase", "field": "*", "priority": 1, "action": "normalize_keys", "transform": "lower", "llm_prompt": "Ensure all keys are lowercase" }, '02_tag_to_tags.json': { "name": "tag_to_tags", "description": "Rename 'tag' field to 'tags'", "field": "tag", "priority": 2, "action": "rename_field", "from": "tag", "to": "tags", "llm_prompt": "Convert tag field to tags array" }, '03_summary_to_description.json': { "name": "summary_to_description", "description": "Rename 'summary' field to 'description'", "field": "summary", "priority": 2, "action": "rename_field", "from": "summary", "to": "description", "llm_prompt": "Convert summary to description" }, '10_tags_format_list.json': { "name": "tags_format_list", "description": "Convert comma-separated tags to list", "field": "tags", "priority": 10, "action": "normalize_value", "split_on": ",", "pattern": ".*", "replacement": "\\g<0>", "llm_prompt": "Ensure tags is a list, not a comma-separated string" }, '20_tags_replace_spaces.json': { "name": "tags_replace_spaces", "description": "Replace spaces with underscores in tags", "field": "tags", "priority": 20, "action": "normalize_value", "pattern": " ", "replacement": "_", "llm_prompt": "Replace spaces with underscores in tags" }, '21_tags_replace_hyphens.json': { "name": "tags_replace_hyphens", "description": "Replace hyphens with underscores in tags", "field": "tags", "priority": 21, "action": "normalize_value", "pattern": "-", "replacement": "_", "llm_prompt": "Replace hyphens with underscores in tags" }, '30_tags_lowercase.json': { "name": "tags_lowercase", "description": "Convert tags to lowercase", "field": "tags", "priority": 30, "action": "normalize_value", "transform": "lower", "llm_prompt": "Convert all tags to lowercase" }, '40_tags_remove_invalid_chars.json': { "name": "tags_remove_invalid_chars", "description": "Remove characters that are not alphanumeric or underscore", "field": "tags", "priority": 40, "action": "normalize_value", "pattern": "[^a-z0-9_]", "replacement": "", "llm_prompt": "Remove any characters that are not lowercase letters, numbers, or underscores from tags" }, '50_tags_validate_format.json': { "name": "tags_validate_format", "description": "Validate that tags only contain lowercase alphanumeric and underscores", "field": "tags", "priority": 50, "action": "validate", "pattern": "^[a-z0-9_]+$", "multiline": False, "llm_prompt": "Tags must contain only lowercase letters, numbers, and underscores" } } for filename, rule_data in rules.items(): with open(self.rules_dir / filename, 'w', encoding='utf-8') as f: json.dump(rule_data, f, indent=2) def _create_default_prompt(self): """Create default prompt""" default_prompt = """You are a metadata extraction assistant for markdown documents. Your task is to analyze the document content and suggest appropriate frontmatter metadata. Guidelines: - For tags: prefer selecting from the provided existing tags list when appropriate - You may create new tags if they better fit the content - New tags must follow the rules: lowercase letters, numbers, and underscores only - Be concise and accurate - Preserve important existing metadata when present""" with open(self.prompts_dir / 'default.txt', 'w', encoding='utf-8') as f: f.write(default_prompt) def get_api_config(self) -> dict: """Get API configuration with environment variable resolution""" config = { 'url': 'https://api.openai.com/v1', 'model': 'gpt-4', 'api_key': '', 'api_key_source': 'none' } if not self.env_file.exists(): return config # Parse .env file env_vars = {} with open(self.env_file, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue if '=' in line: key, value = line.split('=', 1) env_vars[key.strip()] = value.strip() # Get URL if 'OPENAI_API_URL' in env_vars: config['url'] = env_vars['OPENAI_API_URL'] # Get model if 'OPENAI_MODEL' in env_vars: config['model'] = env_vars['OPENAI_MODEL'] # Get API key with special handling if 'OPENAI_API_KEY' in env_vars: api_key_value = env_vars['OPENAI_API_KEY'] # Remove quotes if present api_key_value = api_key_value.strip('"').strip("'") if not api_key_value: # Empty string = no key needed config['api_key'] = '' config['api_key_source'] = 'none' elif api_key_value.startswith('$'): # Environment variable reference env_var_name = api_key_value[1:] env_value = os.environ.get(env_var_name) if env_value is None: raise ValueError( f"Environment variable '{env_var_name}' not found. " f"Referenced in .env as OPENAI_API_KEY={api_key_value}" ) config['api_key'] = env_value config['api_key_source'] = f'env:{env_var_name}' else: # Plain text key (security warning will be issued) config['api_key'] = api_key_value config['api_key_source'] = 'plaintext' return config def print_config_info(self): """Print configuration location and status""" print(f"Configuration directory: {self.config_dir}") print(f" .env file: {self.env_file}") print(f" Templates: {self.templates_dir}") print(f" Rules: {self.rules_dir}") print(f" Prompts: {self.prompts_dir}") # Get API config to show status try: api_config = self.get_api_config() if api_config['api_key_source'] == 'none': print(f" API Key: Not configured (LLM features disabled)") elif api_config['api_key_source'] == 'plaintext': print(f" API Key: Configured (plain text)") print(f" WARNING: Storing API keys in plain text is a security risk!") print(f" Consider using environment variable: OPENAI_API_KEY=$YOUR_ENV_VAR") elif api_config['api_key_source'].startswith('env:'): env_var = api_config['api_key_source'].split(':', 1)[1] print(f" API Key: From environment variable ${env_var}") except ValueError as e: print(f" API Key: ERROR - {e}") print()