# VibePod Configuration Specification

---

## Configuration Hierarchy

```
Environment Variables (highest priority)
        │
        ▼
Project Config (.vibepod/config.yaml)
        │
        ▼
Global Config (~/.config/vibepod/config.yaml)
        │
        ▼
Default Values (lowest priority)
```

---

## Directory Structure

### Unix (Linux/macOS)

```
~/.config/vibepod/
├── config.yaml              # Global configuration
├── logs.db                  # SQLite request logs
├── AGENTS.lock              # Global skills lockfile
└── agents/
    ├── claude/
    │   ├── credentials.json
    │   └── settings.json
    ├── gemini/
    │   └── credentials.json
    ├── opencode/
    │   └── credentials.json
    ├── devstral/
    │   └── credentials.json
    └── auggie/
        └── credentials.json
```

### Windows

```
%LOCALAPPDATA%\vibepod\
├── config.yaml
├── logs.db
├── AGENTS.lock
└── agents\
    └── ...
```

### macOS (Alternative)

```
~/Library/Application Support/vibepod/
├── config.yaml
└── ...
```

Use `platformdirs` to handle this automatically.

---

## Global Configuration

### `~/.config/vibepod/config.yaml`

```yaml
# VibePod Global Configuration
# Documentation: https://vibepod.dev/docs/configuration

# Schema version
version: 1

# ═══════════════════════════════════════════════════════════════════════════════
# GENERAL SETTINGS
# ═══════════════════════════════════════════════════════════════════════════════

# Default agent when none specified
# Options: claude, gemini, opencode, devstral, auggie
default_agent: claude

# Automatically pull latest images before run
auto_pull: false

# Remove containers after exit
auto_remove: true

# Docker network for VibePod containers
network: vibepod-network

# Log level: debug, info, warning, error
log_level: info

# Disable colored output
no_color: false

# ═══════════════════════════════════════════════════════════════════════════════
# AGENT CONFIGURATIONS
# ═══════════════════════════════════════════════════════════════════════════════

agents:
  claude:
    # Enable this agent
    enabled: true
    
    # Docker image
    image: vibepod/claude:latest
    
    # Config directory (for credentials, settings)
    config_dir: ~/.config/vibepod/agents/claude
    
    # Additional environment variables
    env: {}
    
    # Additional volume mounts (host:container)
    volumes: []
    
    # Enable proxy by default for this agent
    proxy: false
    
  gemini:
    enabled: true
    image: vibepod/gemini:latest
    config_dir: ~/.config/vibepod/agents/gemini
    env: {}
    volumes: []
    proxy: false
    
  opencode:
    enabled: true
    image: vibepod/opencode:latest
    config_dir: ~/.config/vibepod/agents/opencode
    env: {}
    volumes: []
    proxy: false
    
  devstral:
    enabled: true
    image: vibepod/devstral:latest
    config_dir: ~/.config/vibepod/agents/devstral
    env: {}
    volumes: []
    proxy: false
    
  auggie:
    enabled: true
    image: vibepod/auggie:latest
    config_dir: ~/.config/vibepod/agents/auggie
    env: {}
    volumes: []
    proxy: false

# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING (Datasette)
# ═══════════════════════════════════════════════════════════════════════════════

logging:
  # Enable API request logging
  enabled: true
  
  # Datasette Docker image
  image: vibepod/datasette:latest
  
  # SQLite database path
  db_path: ~/.config/vibepod/logs.db
  
  # Datasette UI port
  ui_port: 8001
  
  # Log retention in days (0 = forever)
  retention_days: 30
  
  # Log request bodies (may contain sensitive data)
  log_request_body: false
  
  # Log response bodies (may contain sensitive data)
  log_response_body: false

# ═══════════════════════════════════════════════════════════════════════════════
# SKILLS (AGENTS.lock)
# ═══════════════════════════════════════════════════════════════════════════════

skills:
  # Enable skills management
  enabled: true
  
  # Auto-sync skills before running agents
  auto_sync: true
  
  # Global AGENTS.lock file
  global_lockfile: ~/.config/vibepod/AGENTS.lock
  
  # Shorthand expansions for skill sources
  shortcuts:
    vibepod: https://github.com/VibePod/skills/tree/main
    anthropic: https://github.com/anthropics/skills/tree/main/skills

# ═══════════════════════════════════════════════════════════════════════════════
# ALIASES
# ═══════════════════════════════════════════════════════════════════════════════

# Custom command aliases
aliases:
  c: run claude
  g: run gemini
  o: run opencode
  d: run devstral
  a: run auggie
  ui: logs ui

# ═══════════════════════════════════════════════════════════════════════════════
# HOOKS
# ═══════════════════════════════════════════════════════════════════════════════

hooks:
  # Commands to run before starting any agent
  pre_run: []
  # Example:
  #   - echo "Starting agent..."
  #   - ./scripts/pre-run.sh
  
  # Commands to run after agent exits
  post_run: []
  
  # Commands to run before stopping
  pre_stop: []
  
  # Commands to run after container is removed
  post_stop: []
```

---

## Project Configuration

### `.vibepod/config.yaml`

Project-specific settings that override global config:

```yaml
# VibePod Project Configuration
# This file overrides ~/.config/vibepod/config.yaml

# Default agent for this project
default_agent: claude

# Project-specific agent settings
agents:
  claude:
    # Additional environment variables
    env:
      PROJECT_NAME: myapp
      DEBUG: "1"
    
    # Additional volumes
    volumes:
      - ./data:/data:ro
      - ./secrets:/secrets:ro
    
    # Always use proxy for this project
    proxy: true

# Project hooks
hooks:
  pre_run:
    - docker compose up -d postgres redis
  post_stop:
    - docker compose down
```

---

## Environment Variables

Environment variables take highest priority:

| Variable | Config Path | Description |
|----------|-------------|-------------|
| `VP_CONFIG_DIR` | - | Override config directory |
| `VP_DEFAULT_AGENT` | `default_agent` | Default agent |
| `VP_AUTO_PULL` | `auto_pull` | Auto-pull images |
| `VP_LOG_LEVEL` | `log_level` | Log level |
| `VP_NO_COLOR` | `no_color` | Disable colors |
| `VP_PROXY_ENABLED` | `logging.enabled` | Enable proxy |
| `VP_DATASETTE_PORT` | `logging.ui_port` | Datasette port |
| `DOCKER_HOST` | - | Docker socket |

### Agent-Specific Variables

| Variable | Description |
|----------|-------------|
| `ANTHROPIC_API_KEY` | Claude API key |
| `GOOGLE_API_KEY` | Gemini API key |
| `OPENAI_API_KEY` | OpenCode API key |
| `MISTRAL_API_KEY` | Devstral API key |

---

## Config Module Implementation

### `src/vibepod/core/config.py`

```python
"""Configuration management."""

from __future__ import annotations

import os
from pathlib import Path
from typing import Any, Optional

import yaml
from platformdirs import user_config_dir

from vibepod.constants import (
    APP_NAME,
    CONFIG_DIR,
    GLOBAL_CONFIG_FILE,
    PROJECT_CONFIG_DIR,
    PROJECT_CONFIG_FILE,
)


def get_config_dir() -> Path:
    """Get configuration directory."""
    env_dir = os.environ.get("VP_CONFIG_DIR")
    if env_dir:
        return Path(env_dir)
    return CONFIG_DIR


def get_global_config_path() -> Path:
    """Get global config file path."""
    return get_config_dir() / "config.yaml"


def get_project_config_path() -> Optional[Path]:
    """Get project config file path if exists."""
    cwd = Path.cwd()
    project_config = cwd / PROJECT_CONFIG_DIR / PROJECT_CONFIG_FILE
    if project_config.exists():
        return project_config
    return None


def load_yaml(path: Path) -> dict[str, Any]:
    """Load YAML file."""
    if not path.exists():
        return {}
    with open(path) as f:
        return yaml.safe_load(f) or {}


def save_yaml(path: Path, data: dict[str, Any]) -> None:
    """Save YAML file."""
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w") as f:
        yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False)


def deep_merge(base: dict, override: dict) -> dict:
    """Deep merge two dictionaries."""
    result = base.copy()
    for key, value in override.items():
        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
            result[key] = deep_merge(result[key], value)
        else:
            result[key] = value
    return result


def get_default_config() -> dict[str, Any]:
    """Get default configuration."""
    return {
        "version": 1,
        "default_agent": "claude",
        "auto_pull": False,
        "auto_remove": True,
        "network": "vibepod-network",
        "log_level": "info",
        "no_color": False,
        "agents": {
            "claude": {
                "enabled": True,
                "image": "vibepod/claude:latest",
                "env": {},
                "volumes": [],
                "proxy": False,
            },
            "gemini": {
                "enabled": True,
                "image": "vibepod/gemini:latest",
                "env": {},
                "volumes": [],
                "proxy": False,
            },
            "opencode": {
                "enabled": True,
                "image": "vibepod/opencode:latest",
                "env": {},
                "volumes": [],
                "proxy": False,
            },
            "devstral": {
                "enabled": True,
                "image": "vibepod/devstral:latest",
                "env": {},
                "volumes": [],
                "proxy": False,
            },
            "auggie": {
                "enabled": True,
                "image": "vibepod/auggie:latest",
                "env": {},
                "volumes": [],
                "proxy": False,
            },
        },
        "logging": {
            "enabled": True,
            "image": "vibepod/datasette:latest",
            "db_path": str(CONFIG_DIR / "logs.db"),
            "ui_port": 8001,
            "retention_days": 30,
            "log_request_body": False,
            "log_response_body": False,
        },
        "skills": {
            "enabled": True,
            "auto_sync": True,
            "global_lockfile": str(CONFIG_DIR / "AGENTS.lock"),
            "shortcuts": {
                "vibepod": "https://github.com/VibePod/skills/tree/main",
                "anthropic": "https://github.com/anthropics/skills/tree/main/skills",
            },
        },
        "aliases": {
            "c": "run claude",
            "g": "run gemini",
            "o": "run opencode",
            "d": "run devstral",
            "a": "run auggie",
            "ui": "logs ui",
        },
        "hooks": {
            "pre_run": [],
            "post_run": [],
            "pre_stop": [],
            "post_stop": [],
        },
    }


def apply_env_overrides(config: dict[str, Any]) -> dict[str, Any]:
    """Apply environment variable overrides."""
    env_mappings = {
        "VP_DEFAULT_AGENT": ("default_agent", str),
        "VP_AUTO_PULL": ("auto_pull", lambda x: x.lower() == "true"),
        "VP_LOG_LEVEL": ("log_level", str),
        "VP_NO_COLOR": ("no_color", lambda x: x.lower() == "true"),
        "VP_DATASETTE_PORT": ("logging.ui_port", int),
    }
    
    for env_var, (config_path, converter) in env_mappings.items():
        value = os.environ.get(env_var)
        if value is not None:
            keys = config_path.split(".")
            target = config
            for key in keys[:-1]:
                target = target.setdefault(key, {})
            target[keys[-1]] = converter(value)
    
    return config


def get_config() -> dict[str, Any]:
    """Get merged configuration (default + global + project + env)."""
    # Start with defaults
    config = get_default_config()
    
    # Merge global config
    global_config = load_yaml(get_global_config_path())
    config = deep_merge(config, global_config)
    
    # Merge project config
    project_path = get_project_config_path()
    if project_path:
        project_config = load_yaml(project_path)
        config = deep_merge(config, project_config)
    
    # Apply environment overrides
    config = apply_env_overrides(config)
    
    return config


def get_config_value(key: str, default: Any = None) -> Any:
    """Get a specific config value using dot notation."""
    config = get_config()
    keys = key.split(".")
    value = config
    for k in keys:
        if isinstance(value, dict) and k in value:
            value = value[k]
        else:
            return default
    return value


def set_config_value(key: str, value: Any, global_config: bool = True) -> None:
    """Set a config value."""
    if global_config:
        path = get_global_config_path()
    else:
        path = Path.cwd() / PROJECT_CONFIG_DIR / PROJECT_CONFIG_FILE
    
    config = load_yaml(path)
    keys = key.split(".")
    target = config
    for k in keys[:-1]:
        target = target.setdefault(k, {})
    target[keys[-1]] = value
    save_yaml(path, config)


def init_config(path: Path) -> None:
    """Initialize config file with defaults."""
    if not path.exists():
        path.parent.mkdir(parents=True, exist_ok=True)
        save_yaml(path, {})


def ensure_config_dir() -> None:
    """Ensure config directory exists."""
    config_dir = get_config_dir()
    config_dir.mkdir(parents=True, exist_ok=True)
    (config_dir / "agents").mkdir(exist_ok=True)
```

---

## Configuration Validation

### Schema

```python
"""Config validation schema."""

CONFIG_SCHEMA = {
    "version": {"type": int, "required": True},
    "default_agent": {"type": str, "choices": ["claude", "gemini", "opencode", "devstral", "auggie"]},
    "auto_pull": {"type": bool},
    "auto_remove": {"type": bool},
    "log_level": {"type": str, "choices": ["debug", "info", "warning", "error"]},
    "agents": {
        "type": dict,
        "values": {
            "enabled": {"type": bool},
            "image": {"type": str},
            "env": {"type": dict},
            "volumes": {"type": list},
            "proxy": {"type": bool},
        },
    },
    "logging": {
        "enabled": {"type": bool},
        "ui_port": {"type": int, "min": 1, "max": 65535},
        "retention_days": {"type": int, "min": 0},
    },
}
```

---

## First Run Initialization

On first run, VibePod should:

1. Create config directory: `~/.config/vibepod/`
2. Create agents subdirectory: `~/.config/vibepod/agents/`
3. NOT create default config.yaml (use defaults in code)
4. Inform user of config location

```python
def first_run_setup():
    """Set up VibePod on first run."""
    config_dir = get_config_dir()
    
    if not config_dir.exists():
        console.print(f"[dim]Creating config directory: {config_dir}[/dim]")
        config_dir.mkdir(parents=True)
        (config_dir / "agents").mkdir()
        
        console.print("[green]VibePod initialized![/green]")
        console.print(f"Config: {config_dir / 'config.yaml'}")
        console.print(f"Logs:   {config_dir / 'logs.db'}")
```
