collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use super::defaults::scaffold_defaults;
use crate::common::Result;
use std::path::PathBuf;

// ---------------------------------------------------------------------------
// Directory layout
// ---------------------------------------------------------------------------
//
//  ~/.collet/                ← collet_home()
//  ├── config.toml           ← main config
//  ├── agents/               ← agent definition files (*.md)
//  ├── skills/               ← user skill definitions
//  ├── mcp.json              ← MCP server config
//  ├── projects/             ← project-scoped runtime data (hashed by cwd)
//  │   └── <hash12>/
//  │       ├── sessions/     ← conversation sessions
//  │       ├── knowledge.json
//  │       ├── checkpoint.json
//  │       ├── STATUS.md
//  │       └── trust.json
//  ├── worktrees/            ← isolated git worktrees (auto-deleted after use)
//  │   └── <project-name>/
//  │       └── <task-id>/
//  └── logs/
//      └── bench.jsonl

/// Return the collet home directory (`~/.collet` by default).
///
/// Priority:
///   1. `COLLET_HOME` env var
///   2. `[paths] home` in config.toml (passed as override)
///   3. Default: `~/.collet`
pub fn collet_home(override_path: Option<&str>) -> PathBuf {
    if let Ok(env) = std::env::var("COLLET_HOME") {
        return expand_tilde(&env);
    }
    if let Some(p) = override_path {
        return expand_tilde(p);
    }
    dirs::home_dir()
        .unwrap_or_else(|| PathBuf::from("."))
        .join(".collet")
}

/// Return path to main config file (`<collet_home>/config.toml`).
pub fn config_file_path() -> PathBuf {
    collet_home(None).join("config.toml")
}

/// Return path to the secrets file (`<collet_home>/.secrets`).
pub fn secrets_file_path() -> PathBuf {
    collet_home(None).join(".secrets")
}

/// Return `<collet_home>/logs/`.
pub fn logs_dir() -> PathBuf {
    collet_home(None).join("logs")
}

/// Return `<collet_home>/logs/bench.jsonl`.
pub fn bench_log_path() -> PathBuf {
    logs_dir().join("bench.jsonl")
}

/// Return `{dir}/{prefix}.{YYYY-MM-DD}.log` for today and prune files matching
/// `{prefix}.*.log` that are older than `keep_days` days.
///
/// Designed for log sources that are not managed by `tracing_appender`
/// (e.g. `remote`, `stderr`). The caller is responsible for creating `dir`.
pub fn dated_log_path(dir: &std::path::Path, prefix: &str, keep_days: u64) -> PathBuf {
    let today = chrono::Local::now().format("%Y-%m-%d").to_string();
    let current_name = format!("{prefix}.{today}.log");
    let cutoff = std::time::Duration::from_secs(keep_days * 24 * 60 * 60);

    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.flatten() {
            let name = entry.file_name();
            let name = name.to_string_lossy();
            if name.starts_with(&format!("{prefix}."))
                && name.ends_with(".log")
                && *name != current_name
            {
                let is_old = entry
                    .metadata()
                    .ok()
                    .and_then(|m| m.modified().ok())
                    .and_then(|t| t.elapsed().ok())
                    .map(|d| d > cutoff)
                    .unwrap_or(false);
                if is_old {
                    let _ = std::fs::remove_file(entry.path());
                }
            }
        }
    }

    dir.join(current_name)
}

/// Expand a leading `~` to the home directory.
pub(super) fn expand_tilde(path: &str) -> PathBuf {
    if let Some(rest) = path.strip_prefix("~/") {
        dirs::home_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join(rest)
    } else if path == "~" {
        dirs::home_dir().unwrap_or_else(|| PathBuf::from("."))
    } else {
        PathBuf::from(path)
    }
}

/// Return a project-scoped data directory under `<collet_home>/projects/`.
///
/// Uses a BLAKE3 hash prefix (12 hex chars) of the canonical working directory.
/// Example: `~/.collet/projects/a1b2c3d4e5f6/`
pub fn project_data_dir(working_dir: &str) -> PathBuf {
    project_data_dir_with(working_dir, None)
}

/// Like `project_data_dir` but accepts a collet_home override (from `Config.collet_home`).
pub fn project_data_dir_with(working_dir: &str, home_override: Option<&str>) -> PathBuf {
    let canonical =
        std::fs::canonicalize(working_dir).unwrap_or_else(|_| PathBuf::from(working_dir));
    let hash = blake3::hash(canonical.to_string_lossy().as_bytes());
    let hex = hash.to_hex();
    let short = &hex.as_str()[..12];
    collet_home(home_override).join("projects").join(short)
}

/// Ensure `<collet_home>/` exists and return it.
///
/// On first creation, seeds `mcp.example.json` so users have a reference.
pub fn ensure_collet_home() -> Result<PathBuf> {
    let dir = collet_home(None);
    if !dir.exists() {
        std::fs::create_dir_all(&dir).map_err(|e| {
            crate::common::AgentError::Config(format!(
                "Failed to create collet home: {} - {}",
                dir.display(),
                e
            ))
        })?;
    }
    // Seed mcp.example.json if it doesn't exist yet.
    let example = dir.join("mcp.example.json");
    if !example.exists() {
        let _ = std::fs::write(&example, MCP_EXAMPLE_JSON);
    }
    // Always seed default agents/skills/commands (write_if_missing is a no-op for existing files).
    let _ = scaffold_defaults(&dir);
    Ok(dir)
}

/// Example MCP configuration covering stdio and HTTP transports.
///
/// Supported fields per entry:
///   - `command`     : string          — executable for stdio transport
///   - `args`        : string array    — arguments passed to command
///   - `env`         : object          — environment variables (`${VAR}` expanded)
///   - `url`         : string          — base URL for HTTP transport
///   - `headers`     : object          — HTTP headers (`${VAR}` expanded)
///   - `enabled`     : bool            — set false to disable without removing (default: true)
///   - `source`      : string          — install source hint (informational)
///   - `description` : string          — human-readable label (optional, for your notes)
pub const MCP_EXAMPLE_JSON: &str = r##"{
  "mcpServers": {
    "context7": {
      "command": "npx",
      "args": ["-y", "@upstash/context7-mcp@latest"],
      "description": "Library documentation lookup via Context7"
    },
    "playwright": {
      "command": "npx",
      "args": ["-y", "@playwright/mcp@latest"],
      "env": {
        "DISPLAY": ":0"
      },
      "description": "Browser automation and E2E testing"
    },
    "example-http": {
      "url": "https://api.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${EXAMPLE_API_KEY}"
      },
      "enabled": false,
      "description": "Example HTTP MCP server (disabled by default)"
    }
  }
}
"##;

// ---------------------------------------------------------------------------
// Backward-compatible aliases (removed callers will use new names directly)
// ---------------------------------------------------------------------------

/// Alias: config dir is now `collet_home()` (config.toml lives directly inside).
#[inline]
pub fn config_dir() -> PathBuf {
    collet_home(None)
}

/// Alias: ensure_config_dir → ensure_collet_home.
#[inline]
pub fn ensure_config_dir() -> Result<PathBuf> {
    ensure_collet_home()
}