diff options
| author | Yuval Adam <_@yuv.al> | 2025-03-06 13:55:06 +0100 |
|---|---|---|
| committer | Yuval Adam <_@yuv.al> | 2025-03-06 13:55:06 +0100 |
| commit | ceec45a6d8fad9d8750b8719b9bc3aa206a5ee9d (patch) | |
| tree | 8d65b0800a395fa94a496ddb6890adbe2c4538cf | |
| parent | 510bafb2a53743b4199b59e3f9c8a8f23be130dc (diff) | |
Implement configuration management
| -rw-r--r-- | README.md | 32 | ||||
| -rw-r--r-- | strangeloop/cli.py | 90 | ||||
| -rw-r--r-- | strangeloop/config.py | 130 |
3 files changed, 252 insertions, 0 deletions
@@ -30,6 +30,13 @@ uvx strangeloop process FILE_PATH [--output OUTPUT_PATH] # Ask Claude Sonnet 3.7 a question uvx strangeloop ask "What is recursive self-improvement in AI?" + +# Configuration management +uvx strangeloop config set api_key "your-api-key" +uvx strangeloop config get api_key +uvx strangeloop config list +uvx strangeloop config delete api_key +uvx strangeloop config path ``` If you've installed the package: @@ -47,3 +54,28 @@ strangeloop ask "What is the meaning of life?" --max-tokens 2048 --temperature 0 The following environment variables are required: - `ANTHROPIC_API_KEY`: Your Anthropic API key for accessing Claude Sonnet 3.7 + +## Configuration + +Strangeloop uses the XDG Base Directory Specification for storing configuration. The configuration file is stored at: + +- Linux/macOS: `~/.config/strangeloop/config.json` (or `$XDG_CONFIG_HOME/strangeloop/config.json` if set) +- Windows: `%APPDATA%\strangeloop\config.json` + +You can manage configuration using the `config` command: + +```bash +# Set a configuration value +strangeloop config set model "claude-3-opus-20240229" + +# Get a configuration value +strangeloop config get model + +# List all configuration values +strangeloop config list + +# Delete a configuration value +strangeloop config delete model + +# Show the configuration file path +strangeloop config path diff --git a/strangeloop/cli.py b/strangeloop/cli.py index 3a20521..9b4900d 100644 --- a/strangeloop/cli.py +++ b/strangeloop/cli.py @@ -4,8 +4,10 @@ Strangeloop CLI - A recursive and self-referential AI agent framework. """ import click import sys +import json from pathlib import Path from .llm import ask_claude +from .config import get_config @click.group() @@ -75,5 +77,93 @@ def ask(question, max_tokens, temperature): sys.exit(1) +@cli.group() +def config(): + """Manage Strangeloop configuration.""" + pass + + +@config.command(name="set") +@click.argument("key", required=True) +@click.argument("value", required=True) +def config_set(key, value): + """Set a configuration value.""" + try: + # Try to parse as JSON if possible + try: + value = json.loads(value) + except json.JSONDecodeError: + # If not valid JSON, use as string + pass + + config = get_config() + config.set(key, value) + click.echo(f"Configuration '{key}' set to: {value}") + except Exception as e: + click.echo(f"Error setting configuration: {str(e)}", err=True) + sys.exit(1) + + +@config.command(name="get") +@click.argument("key", required=True) +def config_get(key): + """Get a configuration value.""" + try: + config = get_config() + value = config.get(key) + if value is None: + click.echo(f"Configuration '{key}' is not set") + else: + if isinstance(value, (dict, list)): + click.echo(json.dumps(value, indent=2)) + else: + click.echo(value) + except Exception as e: + click.echo(f"Error getting configuration: {str(e)}", err=True) + sys.exit(1) + + +@config.command(name="list") +def config_list(): + """List all configuration values.""" + try: + config = get_config() + values = config.list_all() + if not values: + click.echo("No configuration values set") + else: + click.echo(json.dumps(values, indent=2)) + except Exception as e: + click.echo(f"Error listing configuration: {str(e)}", err=True) + sys.exit(1) + + +@config.command(name="delete") +@click.argument("key", required=True) +def config_delete(key): + """Delete a configuration value.""" + try: + config = get_config() + if config.delete(key): + click.echo(f"Configuration '{key}' deleted") + else: + click.echo(f"Configuration '{key}' not found") + except Exception as e: + click.echo(f"Error deleting configuration: {str(e)}", err=True) + sys.exit(1) + + +@config.command(name="path") +def config_path(): + """Show the configuration file path.""" + try: + config = get_config() + click.echo(f"Configuration directory: {config.config_dir}") + click.echo(f"Configuration file: {config.config_file}") + except Exception as e: + click.echo(f"Error getting configuration path: {str(e)}", err=True) + sys.exit(1) + + if __name__ == "__main__": cli() diff --git a/strangeloop/config.py b/strangeloop/config.py new file mode 100644 index 0000000..e8570f0 --- /dev/null +++ b/strangeloop/config.py @@ -0,0 +1,130 @@ +""" +Configuration management for Strangeloop. +Uses XDG Base Directory Specification for storing configuration. +""" +import os +import json +from pathlib import Path +from typing import Dict, Any, Optional + + +class Config: + """Configuration manager for Strangeloop.""" + + def __init__(self): + """Initialize the configuration manager.""" + self.config_dir = self._get_config_dir() + self.config_file = self.config_dir / "config.json" + self._ensure_config_exists() + self.config = self._load_config() + + def _get_config_dir(self) -> Path: + """ + Get the configuration directory following XDG Base Directory Specification. + + Returns: + Path to the configuration directory + """ + # Use XDG_CONFIG_HOME if defined, otherwise fallback to ~/.config + xdg_config_home = os.environ.get("XDG_CONFIG_HOME") + if xdg_config_home: + base_dir = Path(xdg_config_home) + else: + base_dir = Path.home() / ".config" + + return base_dir / "strangeloop" + + def _ensure_config_exists(self) -> None: + """Ensure the configuration directory and file exist.""" + self.config_dir.mkdir(parents=True, exist_ok=True) + + if not self.config_file.exists(): + # Create default config + default_config = {} + with open(self.config_file, "w") as f: + json.dump(default_config, f, indent=2) + + def _load_config(self) -> Dict[str, Any]: + """ + Load the configuration from the config file. + + Returns: + The configuration as a dictionary + """ + try: + with open(self.config_file, "r") as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + # Return empty config if file is invalid or doesn't exist + return {} + + def _save_config(self) -> None: + """Save the current configuration to the config file.""" + with open(self.config_file, "w") as f: + json.dump(self.config, f, indent=2) + + def get(self, key: str, default: Any = None) -> Any: + """ + Get a configuration value. + + Args: + key: The configuration key + default: Default value if key doesn't exist + + Returns: + The configuration value or default + """ + return self.config.get(key, default) + + def set(self, key: str, value: Any) -> None: + """ + Set a configuration value. + + Args: + key: The configuration key + value: The value to set + """ + self.config[key] = value + self._save_config() + + def delete(self, key: str) -> bool: + """ + Delete a configuration value. + + Args: + key: The configuration key + + Returns: + True if key was deleted, False if it didn't exist + """ + if key in self.config: + del self.config[key] + self._save_config() + return True + return False + + def list_all(self) -> Dict[str, Any]: + """ + Get all configuration values. + + Returns: + Dictionary of all configuration values + """ + return dict(self.config) + + +# Singleton instance +_config_instance = None + + +def get_config() -> Config: + """ + Get the singleton Config instance. + + Returns: + The Config instance + """ + global _config_instance + if _config_instance is None: + _config_instance = Config() + return _config_instance |
