summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuval Adam <_@yuv.al>2025-03-06 13:55:06 +0100
committerYuval Adam <_@yuv.al>2025-03-06 13:55:06 +0100
commitceec45a6d8fad9d8750b8719b9bc3aa206a5ee9d (patch)
tree8d65b0800a395fa94a496ddb6890adbe2c4538cf
parent510bafb2a53743b4199b59e3f9c8a8f23be130dc (diff)
Implement configuration management
-rw-r--r--README.md32
-rw-r--r--strangeloop/cli.py90
-rw-r--r--strangeloop/config.py130
3 files changed, 252 insertions, 0 deletions
diff --git a/README.md b/README.md
index 3589867..07b2487 100644
--- a/README.md
+++ b/README.md
@@ -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