Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/arunachalamkalimuthu_merlin-ai-code-review.json
T

1 line
34 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "arunachalamkalimuthu/merlin-ai-code-review", "files": {"config.example.toml": {"0729aaecc523a068c877c493ef72736856f63bcc": {"t": 1785745335.8266635, "n": 3589, "h": "5016cf6f28e64a1f", "c": "# Merlin configuration — copy to merlin.toml and customize\n# Secrets are loaded from environment variables (never put them here).\n\n[ai]\n# AI provider — choose one:\n#\n# Proprietary\n# \"anthropic\" — Claude via ANTHROPIC_API_KEY (default)\n# \"openai\" — GPT-4o via OPENAI_API_KEY\n# \"claude-code\" — Claude Code CLI (no API key needed; run `claude auth login` once)\n# \"gemini\" — Google Gemini via GEMINI_API_KEY\n# \"bedrock\" — Amazon Bedrock via AWS credentials\n# \"azure-openai\" — Azure OpenAI via AZURE_OPENAI_API_KEY\n#\n# Open-source (hosted)\n# \"groq\" — Llama 3 / Mixtral via GROQ_API_KEY (ultra-fast)\n# \"together-ai\" — 100+ open-source models via TOGETHER_API_KEY\n# \"deep-seek\" — DeepSeek Coder / Chat via DEEPSEEK_API_KEY\n# \"mistral\" — Mistral / Codestral via MISTRAL_API_KEY\n# \"open-router\" — 200+ models via OPENROUTER_API_KEY\n#\n# Self-hosted\n# \"ollama\" — Local Ollama instance (no API key required)\nprovider = \"anthropic\"\n\n# Model to use — override to pin a specific version\n# Anthropic: \"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5-20251001\"\n# OpenAI: \"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\"\n# Gemini: \"gemini-1.5-pro\", \"gemini-1.5-flash\", \"gemini-2.0-flash\"\n# Ollama: any model pulled with `ollama pull` (e.g. \"llama3.1\", \"codellama\", \"mistral\")\nmodel = \"claude-sonnet-4-6\"\n\n# Maximum tokens in the AI response\nmax_tokens = 4096\n\n# Temperature (lower = more deterministic reviews)\ntemperature = 0.2\n\n# (claude-code only) Token for CI headless auth: claude auth login --token <TOKEN>\n# claude_code_token = \"...\"\n\n# (ollama only) Base URL of your Ollama server. Default: http://localhost:11434\n# ollama_base_url = \"http://localhost:11434\"\n\n[review]\n# Which categories to focus on\nfocus = [\"bugs\", \"security\", \"style\", \"performance\"]\n\n# Cap total inline comments per review run (prevents PR spam)\nmax_comments = 30\n\n# Split diff files into chunks of this many lines before sending to AI\nchunk_lines = 200\n\n# Run a second AI pass to filter false positives and refine severity (default: false)\n# reflect = true\n\n[platform]\n# Usually auto-detected from CI environment variables:\n# GitHub Actions → GITHUB_ACTIONS=true\n# GitLab CI → GITLAB_CI=true\n# Bitbucket → BITBUCKET_PIPELINE_UUID\n# Azure DevOps → TF_BUILD=True\n# Gitea Actions → GITEA_ACTIONS=true\n#\n# Uncomment to force a specific platform:\n# type = \"github\" # or \"gitlab\", \"bitbucket\", \"azure-devops\", \"gitea\"\n\n[rag]\n# Enable retrieval-augmented generation for context-aware reviews\nenabled = false\n\n# Vector store backend: \"local\" | \"memory\" | \"qdrant\" | \"chroma\" | \"pinecone\"\nstore = \"local\"\n\n# Embedding model (via Ollama)\nembed_model = \"nomic-embed-text\"\n\n# Ollama server for embeddings\n# ollama_base_url = \"http://localhost:11434\"\n\n# Collection name in the vector store\n# collection = \"merlin\"\n\n# Number of results to retrieve per query\n# top_k = 5\n\n# Minimum similarity score (0.0–1.0)\n# min_score = 0.5\n\n# Path for local JSONL store\n# local_path = \"merlin-rag.jsonl\"\n\n# Qdrant configuration (when store = \"qdrant\")\n# qdrant_url = \"http://localhost:6333\"\n# qdrant_api_key = \"\" # optional for local Qdrant\n\n# ChromaDB configuration (when store = \"chroma\")\n# chroma_url = \"http://localhost:8000\"\n\n# Pinecone configuration (when store = \"pinecone\")\n# pinecone_api_key = \"\" # or set PINECONE_API_KEY env var\n# pinecone_host = \"https://your-index.svc.pinecone.io\"\n\n[agent]\n# Maximum ReAct loop iterations before giving up\n# max_iterations = 10\n\n# Port for Slack/Discord webhook server\n# port = 8090\n"}, "0475e029c6429f372acaf0036abdc52083766899": {"t": 1785750369.967014, "n": 3589, "h": "5016cf6f28e64a1f", "c": "# Merlin configuration — copy to merlin.toml and customize\n# Secrets are loaded from environment variables (never put them here).\n\n[ai]\n# AI provider — choose one:\n#\n# Proprietary\n# \"anthropic\" — Claude via ANTHROPIC_API_KEY (default)\n# \"openai\" — GPT-4o via OPENAI_API_KEY\n# \"claude-code\" — Claude Code CLI (no API key needed; run `claude auth login` once)\n# \"gemini\" — Google Gemini via GEMINI_API_KEY\n# \"bedrock\" — Amazon Bedrock via AWS credentials\n# \"azure-openai\" — Azure OpenAI via AZURE_OPENAI_API_KEY\n#\n# Open-source (hosted)\n# \"groq\" — Llama 3 / Mixtral via GROQ_API_KEY (ultra-fast)\n# \"together-ai\" — 100+ open-source models via TOGETHER_API_KEY\n# \"deep-seek\" — DeepSeek Coder / Chat via DEEPSEEK_API_KEY\n# \"mistral\" — Mistral / Codestral via MISTRAL_API_KEY\n# \"open-router\" — 200+ models via OPENROUTER_API_KEY\n#\n# Self-hosted\n# \"ollama\" — Local Ollama instance (no API key required)\nprovider = \"anthropic\"\n\n# Model to use — override to pin a specific version\n# Anthropic: \"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5-20251001\"\n# OpenAI: \"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\"\n# Gemini: \"gemini-1.5-pro\", \"gemini-1.5-flash\", \"gemini-2.0-flash\"\n# Ollama: any model pulled with `ollama pull` (e.g. \"llama3.1\", \"codellama\", \"mistral\")\nmodel = \"claude-sonnet-4-6\"\n\n# Maximum tokens in the AI response\nmax_tokens = 4096\n\n# Temperature (lower = more deterministic reviews)\ntemperature = 0.2\n\n# (claude-code only) Token for CI headless auth: claude auth login --token <TOKEN>\n# claude_code_token = \"...\"\n\n# (ollama only) Base URL of your Ollama server. Default: http://localhost:11434\n# ollama_base_url = \"http://localhost:11434\"\n\n[review]\n# Which categories to focus on\nfocus = [\"bugs\", \"security\", \"style\", \"performance\"]\n\n# Cap total inline comments per review run (prevents PR spam)\nmax_comments = 30\n\n# Split diff files into chunks of this many lines before sending to AI\nchunk_lines = 200\n\n# Run a second AI pass to filter false positives and refine severity (default: false)\n# reflect = true\n\n[platform]\n# Usually auto-detected from CI environment variables:\n# GitHub Actions → GITHUB_ACTIONS=true\n# GitLab CI → GITLAB_CI=true\n# Bitbucket → BITBUCKET_PIPELINE_UUID\n# Azure DevOps → TF_BUILD=True\n# Gitea Actions → GITEA_ACTIONS=true\n#\n# Uncomment to force a specific platform:\n# type = \"github\" # or \"gitlab\", \"bitbucket\", \"azure-devops\", \"gitea\"\n\n[rag]\n# Enable retrieval-augmented generation for context-aware reviews\nenabled = false\n\n# Vector store backend: \"local\" | \"memory\" | \"qdrant\" | \"chroma\" | \"pinecone\"\nstore = \"local\"\n\n# Embedding model (via Ollama)\nembed_model = \"nomic-embed-text\"\n\n# Ollama server for embeddings\n# ollama_base_url = \"http://localhost:11434\"\n\n# Collection name in the vector store\n# collection = \"merlin\"\n\n# Number of results to retrieve per query\n# top_k = 5\n\n# Minimum similarity score (0.0–1.0)\n# min_score = 0.5\n\n# Path for local JSONL store\n# local_path = \"merlin-rag.jsonl\"\n\n# Qdrant configuration (when store = \"qdrant\")\n# qdrant_url = \"http://localhost:6333\"\n# qdrant_api_key = \"\" # optional for local Qdrant\n\n# ChromaDB configuration (when store = \"chroma\")\n# chroma_url = \"http://localhost:8000\"\n\n# Pinecone configuration (when store = \"pinecone\")\n# pinecone_api_key = \"\" # or set PINECONE_API_KEY env var\n# pinecone_host = \"https://your-index.svc.pinecone.io\"\n\n[agent]\n# Maximum ReAct loop iterations before giving up\n# max_iterations = 10\n\n# Port for Slack/Discord webhook server\n# port = 8090\n"}, "e0b91775c8adc04ad8f1539fd927ab1cd2baf55b": {"t": 1785750371.07255, "n": 3090, "h": "1d8bdf0b44363ed9", "c": "# Merlin configuration — copy to merlin.toml and customize\n# Secrets are loaded from environment variables (never put them here).\n\n[ai]\n# AI provider — choose one:\n# \"anthropic\" — Claude via ANTHROPIC_API_KEY (default)\n# \"openai\" — GPT-4o via OPENAI_API_KEY\n# \"claude-code\" — Claude Code CLI (no API key needed; run `claude auth login` once)\n# \"gemini\" — Google Gemini via GEMINI_API_KEY\n# \"ollama\" — Local Ollama instance (no API key required)\nprovider = \"anthropic\"\n\n# Model to use — override to pin a specific version\n# Anthropic: \"claude-sonnet-4-6\", \"claude-opus-4-6\", \"claude-haiku-4-5-20251001\"\n# OpenAI: \"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\"\n# Gemini: \"gemini-1.5-pro\", \"gemini-1.5-flash\", \"gemini-2.0-flash\"\n# Ollama: any model pulled with `ollama pull` (e.g. \"llama3.1\", \"codellama\", \"mistral\")\nmodel = \"claude-sonnet-4-6\"\n\n# Maximum tokens in the AI response\nmax_tokens = 4096\n\n# Temperature (lower = more deterministic reviews)\ntemperature = 0.2\n\n# (claude-code only) Token for CI headless auth: claude auth login --token <TOKEN>\n# claude_code_token = \"...\"\n\n# (ollama only) Base URL of your Ollama server. Default: http://localhost:11434\n# ollama_base_url = \"http://localhost:11434\"\n\n[review]\n# Which categories to focus on\nfocus = [\"bugs\", \"security\", \"style\", \"performance\"]\n\n# Cap total inline comments per review run (prevents PR spam)\nmax_comments = 30\n\n# Split diff files into chunks of this many lines before sending to AI\nchunk_lines = 200\n\n# Run a second AI pass to filter false positives and refine severity (default: false)\n# reflect = true\n\n[platform]\n# Usually auto-detected from CI environment variables:\n# GitHub Actions → GITHUB_ACTIONS=true\n# GitLab CI → GITLAB_CI=true\n# Bitbucket → BITBUCKET_PIPELINE_UUID\n# Azure DevOps → TF_BUILD=True\n# Gitea Actions → GITEA_ACTIONS=true\n#\n# Uncomment to force a specific platform:\n# type = \"github\" # or \"gitlab\", \"bitbucket\", \"azure-devops\", \"gitea\"\n\n[rag]\n# Enable retrieval-augmented generation for context-aware reviews\nenabled = false\n\n# Vector store backend: \"local\" | \"memory\" | \"qdrant\" | \"chroma\" | \"pinecone\"\nstore = \"local\"\n\n# Embedding model (via Ollama)\nembed_model = \"nomic-embed-text\"\n\n# Ollama server for embeddings\n# ollama_base_url = \"http://localhost:11434\"\n\n# Collection name in the vector store\n# collection = \"merlin\"\n\n# Number of results to retrieve per query\n# top_k = 5\n\n# Minimum similarity score (0.0–1.0)\n# min_score = 0.5\n\n# Path for local JSONL store\n# local_path = \"merlin-rag.jsonl\"\n\n# Qdrant configuration (when store = \"qdrant\")\n# qdrant_url = \"http://localhost:6333\"\n# qdrant_api_key = \"\" # optional for local Qdrant\n\n# ChromaDB configuration (when store = \"chroma\")\n# chroma_url = \"http://localhost:8000\"\n\n# Pinecone configuration (when store = \"pinecone\")\n# pinecone_api_key = \"\" # or set PINECONE_API_KEY env var\n# pinecone_host = \"https://your-index.svc.pinecone.io\"\n\n[agent]\n# Maximum ReAct loop iterations before giving up\n# max_iterations = 10\n\n# Port for Slack/Discord webhook server\n# port = 8090\n"}}, "src/config/mod.rs": {"0729aaecc523a068c877c493ef72736856f63bcc": {"t": 1785745827.4353318, "n": 11152, "h": "fbd9e55bf617fb84", "c": "//! Configuration schema, TOML loading, and environment-variable credential helpers.\n//!\n//! [`Config`] is the root struct that maps directly to `merlin.toml`. Load it\n//! with [`Config::load`] or [`Config::load_default`]; all fields have sensible\n//! defaults so an empty (or missing) file is valid.\n//!\n//! Secrets are read from environment variables — never stored in the config\n//! file. The helper methods on [`Config`] (e.g. [`Config::anthropic_api_key`])\n//! return [`crate::error::MerlinError::EnvVar`] when the required variable is\n//! absent, so callers get a clear error message instead of a panic.\n//!\n//! # Example — minimal `merlin.toml`\n//!\n//! ```toml\n//! [ai]\n//! provider = \"anthropic\"\n//! model = \"claude-sonnet-4-6\"\n//! max_tokens = 4096\n//!\n//! [review]\n//! focus = [\"bugs\", \"security\"]\n//! max_comments = 20\n//! reflect = true\n//! ```\n\npub mod ai;\npub mod integrations;\npub mod platform;\npub mod rag;\npub mod review;\n\npub use ai::{AiConfig, AiProviderType, PersonaConfig};\npub use integrations::{\n AgentConfig, AuditConfig, CoverageConfig, JiraConfig, LinearConfig, SnykConfig,\n};\npub use platform::{PlatformConfig, PlatformType};\npub use rag::{EmbedderType, RagConfig, VectorStoreType};\npub use review::ReviewConfig;\n\nuse std::path::Path;\n\nuse crate::error::{MerlinError, Result};\n\n/// Root configuration struct — the complete contents of `merlin.toml`.\n///\n/// Load with [`Config::load`] or [`Config::load_default`]. All sub-structs\n/// have sensible defaults, so an empty file — or no file at all — is valid.\n///\n/// # Examples\n///\n/// ```no_run\n/// use merlin::config::Config;\n///\n/// let cfg = Config::load_default().unwrap();\n/// assert_eq!(cfg.ai.max_tokens, 4096);\n/// ```\n#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)]\npub struct Config {\n /// AI provider and model settings.\n #[serde(default)]\n pub ai: AiConfig,\n /// Review behaviour settings (focus, chunk size, comment cap).\n #[serde(default)]\n pub review: ReviewConfig,\n /// VCS platform connection settings.\n #[serde(default)]\n pub platform: PlatformConfig,\n /// Jira integration settings.\n #[serde(default)]\n pub jira: JiraConfig,\n /// Linear integration settings.\n #[serde(default)]\n pub linear: LinearConfig,\n /// Code coverage report settings.\n #[serde(default)]\n pub coverage: CoverageConfig,\n /// Audit log settings.\n #[serde(default)]\n pub audit: AuditConfig,\n /// Snyk vulnerability scanning settings.\n #[serde(default)]\n pub snyk: SnykConfig,\n /// Autonomous agent runtime settings.\n #[serde(default)]\n pub agent: AgentConfig,\n /// RAG pipeline settings.\n #[serde(default)]\n pub rag: RagConfig,\n}\n\nimpl Config {\n /// Load config from a TOML file, falling back to all-defaults if the file\n /// is not found.\n ///\n /// # Errors\n ///\n /// Returns [`MerlinError::Io`] if the file exists but cannot be read, or\n /// [`MerlinError::TomlDe`] if the file is not valid TOML.\n pub fn load(path: &Path) -> Result<Self> {\n if !path.exists() {\n tracing::debug!(\"Config file not found at {:?}, using defaults\", path);\n return Ok(Config::default());\n }\n let contents = std::fs::read_to_string(path)?;\n let config: Config = toml::from_str(&contents)?;\n Ok(config)\n }\n\n /// Load from the default path (`merlin.toml` in the current directory).\n ///\n /// Equivalent to `Config::load(Path::new(\"merlin.toml\"))`.\n ///\n /// # Errors\n ///\n /// See [`Config::load`].\n pub fn load_default() -> Result<Self> {\n Self::load(Path::new(\"merlin.toml\"))\n }\n\n /// Return the Anthropic API key from env (`ANTHROPIC_API_KEY`).\n pub fn anthropic_api_key() -> Result<String> {\n std::env::var(\"ANTHROPIC_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"ANTHROPIC_API_KEY\".to_string()))\n }\n\n /// Return the OpenAI API key from env (`OPENAI_API_KEY`).\n pub fn openai_api_key() -> Result<String> {\n std::env::var(\"OPENAI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"OPENAI_API_KEY\".to_string()))\n }\n\n /// Return the Google Gemini API key from env (`GEMINI_API_KEY`).\n pub fn gemini_api_key() -> Result<String> {\n std::env::var(\"GEMINI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"GEMINI_API_KEY\".to_string()))\n }\n\n /// Return the Azure OpenAI API key from env (`AZURE_OPENAI_API_KEY`).\n pub fn azure_openai_api_key() -> Result<String> {\n std::env::var(\"AZURE_OPENAI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"AZURE_OPENAI_API_KEY\".to_string()))\n }\n\n /// Return the Groq API key from env (`GROQ_API_KEY`).\n pub fn groq_api_key() -> Result<String> {\n std::env::var(\"GROQ_API_KEY\").map_err(|_| MerlinError::EnvVar(\"GROQ_API_KEY\".to_string()))\n }\n\n /// Return the Together AI API key from env (`TOGETHER_API_KEY`).\n pub fn together_api_key() -> Result<String> {\n std::env::var(\"TOGETHER_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"TOGETHER_API_KEY\".to_string()))\n }\n\n /// Return the DeepSeek API key from env (`DEEPSEEK_API_KEY`).\n pub fn deepseek_api_key() -> Result<String> {\n std::env::var(\"DEEPSEEK_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"DEEPSEEK_API_KEY\".to_string()))\n }\n\n /// Return the Mistral AI API key from env (`MISTRAL_API_KEY`).\n pub fn mistral_api_key() -> Result<String> {\n std::env::var(\"MISTRAL_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"MISTRAL_API_KEY\".to_string()))\n }\n\n /// Return the OpenRouter API key from env (`OPENROUTER_API_KEY`).\n pub fn openrouter_api_key() -> Result<String> {\n std::env::var(\"OPENROUTER_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"OPENROUTER_API_KEY\".to_string()))\n }\n\n /// Return AWS credentials from env (access key, secret key, optional session token).\n pub fn aws_credentials() -> Result<(String, String, Option<String>)> {\n let access = std::env::var(\"AWS_ACCESS_KEY_ID\")\n .map_err(|_| MerlinError::EnvVar(\"AWS_ACCESS_KEY_ID\".to_string()))?;\n let secret = std::env::var(\"AWS_SECRET_ACCESS_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"AWS_SECRET_ACCESS_KEY\".to_string()))?;\n let token = std::env::var(\"AWS_SESSION_TOKEN\").ok();\n Ok((access, secret, token))\n }\n\n /// Return the Jira API token from env (`JIRA_TOKEN`).\n pub fn jira_token() -> Result<String> {\n std::env::var(\"JIRA_TOKEN\").map_err(|_| MerlinError::EnvVar(\"JIRA_TOKEN\".to_string()))\n }\n\n /// Return the Linear API key from env (`LINEAR_API_KEY`).\n pub fn linear_api_key() -> Result<String> {\n std::env::var(\"LINEAR_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"LINEAR_API_KEY\".to_string()))\n }\n\n /// Return the GitHub token from env (`GITHUB_TOKEN`).\n pub fn github_token() -> Result<String> {\n std::env::var(\"GITHUB_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITHUB_TOKEN\".to_string()))\n }\n\n /// Return the GitLab token from env (`GITLAB_TOKEN`).\n pub fn gitlab_token() -> Result<String> {\n std::env::var(\"GITLAB_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITLAB_TOKEN\".to_string()))\n }\n\n /// Return the Bitbucket token from env (`BITBUCKET_TOKEN` or `BITBUCKET_APP_PASSWORD`).\n pub fn bitbucket_token() -> Result<String> {\n std::env::var(\"BITBUCKET_TOKEN\")\n .or_else(|_| std::env::var(\"BITBUCKET_APP_PASSWORD\"))\n .map_err(|_| MerlinError::EnvVar(\"BITBUCKET_TOKEN\".to_string()))\n }\n\n /// Return the Azure DevOps PAT from env (`AZURE_DEVOPS_TOKEN` or `SYSTEM_ACCESSTOKEN`).\n pub fn azure_devops_token() -> Result<String> {\n std::env::var(\"AZURE_DEVOPS_TOKEN\")\n .or_else(|_| std::env::var(\"SYSTEM_ACCESSTOKEN\"))\n .map_err(|_| MerlinError::EnvVar(\"AZURE_DEVOPS_TOKEN\".to_string()))\n }\n\n /// Return the Gitea token from env (`GITEA_TOKEN`).\n pub fn gitea_token() -> Result<String> {\n std::env::var(\"GITEA_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITEA_TOKEN\".to_string()))\n }\n\n /// Return the Snyk API token from env (`SNYK_TOKEN`).\n pub fn snyk_token() -> Result<String> {\n std::env::var(\"SNYK_TOKEN\").map_err(|_| MerlinError::EnvVar(\"SNYK_TOKEN\".to_string()))\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use std::io::Write;\n\n #[test]\n fn test_default_config() {\n let cfg = Config::default();\n assert_eq!(cfg.ai.max_tokens, 4096);\n assert_eq!(cfg.review.max_comments, 30);\n assert_eq!(cfg.review.chunk_lines, 200);\n assert_eq!(cfg.review.focus.len(), 4);\n assert!(!cfg.review.reflect);\n assert!(cfg.jira.base_url.is_none());\n assert!(cfg.coverage.threshold == 0.0);\n assert!(cfg.audit.enabled);\n }\n\n #[test]\n fn test_load_toml() {\n let toml = r#\"\n[ai]\nprovider = \"openai\"\nmodel = \"gpt-4o\"\nmax_tokens = 2048\ntemperature = 0.5\n\n[review]\nmax_comments = 10\nchunk_lines = 100\nfocus = [\"bugs\", \"security\"]\nreflect = true\n\n[review.persona]\nname = \"security-focused\"\nsystem_prompt_extra = \"Be extra strict about security.\"\n\n[jira]\nbase_url = \"https://company.atlassian.net\"\nproject_key = \"PROJ\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::Openai));\n assert_eq!(cfg.ai.model, \"gpt-4o\");\n assert_eq!(cfg.review.max_comments, 10);\n assert_eq!(cfg.review.focus, vec![\"bugs\", \"security\"]);\n assert!(cfg.review.reflect);\n assert_eq!(cfg.review.persona.name.as_deref(), Some(\"security-focused\"));\n assert_eq!(cfg.jira.project_key.as_deref(), Some(\"PROJ\"));\n }\n\n #[test]\n fn test_load_from_missing_file() {\n let result = Config::load(Path::new(\"/tmp/merlin_nonexistent_xyz.toml\"));\n assert!(result.is_ok());\n }\n\n #[test]\n fn test_load_from_file() {\n let mut f = tempfile::NamedTempFile::new().unwrap();\n write!(f, \"[ai]\\nprovider = \\\"anthropic\\\"\\n\").unwrap();\n let cfg = Config::load(f.path()).unwrap();\n assert!(matches!(cfg.ai.provider, AiProviderType::Anthropic));\n }\n\n #[test]\n fn test_azure_openai_config() {\n let toml = r#\"\n[ai]\nprovider = \"azure-openai\"\nmodel = \"gpt-4o\"\nazure_openai_endpoint = \"https://myresource.openai.azure.com\"\nazure_openai_api_version = \"2024-02-01\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::AzureOpenai));\n assert_eq!(\n cfg.ai.azure_openai_endpoint.as_deref(),\n Some(\"https://myresource.openai.azure.com\")\n );\n }\n\n #[test]\n fn test_bedrock_config() {\n let toml = r#\"\n[ai]\nprovider = \"bedrock\"\nmodel = \"anthropic.claude-sonnet-4-6-20250514-v1:0\"\nbedrock_region = \"us-east-1\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::Bedrock));\n assert_eq!(cfg.ai.bedrock_region.as_deref(), Some(\"us-east-1\"));\n }\n}\n"}, "c589e43d69671f053e8eaaf7813c544a6e3772c3": {"t": 1785750371.667212, "n": 11152, "h": "fbd9e55bf617fb84", "c": "//! Configuration schema, TOML loading, and environment-variable credential helpers.\n//!\n//! [`Config`] is the root struct that maps directly to `merlin.toml`. Load it\n//! with [`Config::load`] or [`Config::load_default`]; all fields have sensible\n//! defaults so an empty (or missing) file is valid.\n//!\n//! Secrets are read from environment variables — never stored in the config\n//! file. The helper methods on [`Config`] (e.g. [`Config::anthropic_api_key`])\n//! return [`crate::error::MerlinError::EnvVar`] when the required variable is\n//! absent, so callers get a clear error message instead of a panic.\n//!\n//! # Example — minimal `merlin.toml`\n//!\n//! ```toml\n//! [ai]\n//! provider = \"anthropic\"\n//! model = \"claude-sonnet-4-6\"\n//! max_tokens = 4096\n//!\n//! [review]\n//! focus = [\"bugs\", \"security\"]\n//! max_comments = 20\n//! reflect = true\n//! ```\n\npub mod ai;\npub mod integrations;\npub mod platform;\npub mod rag;\npub mod review;\n\npub use ai::{AiConfig, AiProviderType, PersonaConfig};\npub use integrations::{\n AgentConfig, AuditConfig, CoverageConfig, JiraConfig, LinearConfig, SnykConfig,\n};\npub use platform::{PlatformConfig, PlatformType};\npub use rag::{EmbedderType, RagConfig, VectorStoreType};\npub use review::ReviewConfig;\n\nuse std::path::Path;\n\nuse crate::error::{MerlinError, Result};\n\n/// Root configuration struct — the complete contents of `merlin.toml`.\n///\n/// Load with [`Config::load`] or [`Config::load_default`]. All sub-structs\n/// have sensible defaults, so an empty file — or no file at all — is valid.\n///\n/// # Examples\n///\n/// ```no_run\n/// use merlin::config::Config;\n///\n/// let cfg = Config::load_default().unwrap();\n/// assert_eq!(cfg.ai.max_tokens, 4096);\n/// ```\n#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)]\npub struct Config {\n /// AI provider and model settings.\n #[serde(default)]\n pub ai: AiConfig,\n /// Review behaviour settings (focus, chunk size, comment cap).\n #[serde(default)]\n pub review: ReviewConfig,\n /// VCS platform connection settings.\n #[serde(default)]\n pub platform: PlatformConfig,\n /// Jira integration settings.\n #[serde(default)]\n pub jira: JiraConfig,\n /// Linear integration settings.\n #[serde(default)]\n pub linear: LinearConfig,\n /// Code coverage report settings.\n #[serde(default)]\n pub coverage: CoverageConfig,\n /// Audit log settings.\n #[serde(default)]\n pub audit: AuditConfig,\n /// Snyk vulnerability scanning settings.\n #[serde(default)]\n pub snyk: SnykConfig,\n /// Autonomous agent runtime settings.\n #[serde(default)]\n pub agent: AgentConfig,\n /// RAG pipeline settings.\n #[serde(default)]\n pub rag: RagConfig,\n}\n\nimpl Config {\n /// Load config from a TOML file, falling back to all-defaults if the file\n /// is not found.\n ///\n /// # Errors\n ///\n /// Returns [`MerlinError::Io`] if the file exists but cannot be read, or\n /// [`MerlinError::TomlDe`] if the file is not valid TOML.\n pub fn load(path: &Path) -> Result<Self> {\n if !path.exists() {\n tracing::debug!(\"Config file not found at {:?}, using defaults\", path);\n return Ok(Config::default());\n }\n let contents = std::fs::read_to_string(path)?;\n let config: Config = toml::from_str(&contents)?;\n Ok(config)\n }\n\n /// Load from the default path (`merlin.toml` in the current directory).\n ///\n /// Equivalent to `Config::load(Path::new(\"merlin.toml\"))`.\n ///\n /// # Errors\n ///\n /// See [`Config::load`].\n pub fn load_default() -> Result<Self> {\n Self::load(Path::new(\"merlin.toml\"))\n }\n\n /// Return the Anthropic API key from env (`ANTHROPIC_API_KEY`).\n pub fn anthropic_api_key() -> Result<String> {\n std::env::var(\"ANTHROPIC_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"ANTHROPIC_API_KEY\".to_string()))\n }\n\n /// Return the OpenAI API key from env (`OPENAI_API_KEY`).\n pub fn openai_api_key() -> Result<String> {\n std::env::var(\"OPENAI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"OPENAI_API_KEY\".to_string()))\n }\n\n /// Return the Google Gemini API key from env (`GEMINI_API_KEY`).\n pub fn gemini_api_key() -> Result<String> {\n std::env::var(\"GEMINI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"GEMINI_API_KEY\".to_string()))\n }\n\n /// Return the Azure OpenAI API key from env (`AZURE_OPENAI_API_KEY`).\n pub fn azure_openai_api_key() -> Result<String> {\n std::env::var(\"AZURE_OPENAI_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"AZURE_OPENAI_API_KEY\".to_string()))\n }\n\n /// Return the Groq API key from env (`GROQ_API_KEY`).\n pub fn groq_api_key() -> Result<String> {\n std::env::var(\"GROQ_API_KEY\").map_err(|_| MerlinError::EnvVar(\"GROQ_API_KEY\".to_string()))\n }\n\n /// Return the Together AI API key from env (`TOGETHER_API_KEY`).\n pub fn together_api_key() -> Result<String> {\n std::env::var(\"TOGETHER_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"TOGETHER_API_KEY\".to_string()))\n }\n\n /// Return the DeepSeek API key from env (`DEEPSEEK_API_KEY`).\n pub fn deepseek_api_key() -> Result<String> {\n std::env::var(\"DEEPSEEK_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"DEEPSEEK_API_KEY\".to_string()))\n }\n\n /// Return the Mistral AI API key from env (`MISTRAL_API_KEY`).\n pub fn mistral_api_key() -> Result<String> {\n std::env::var(\"MISTRAL_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"MISTRAL_API_KEY\".to_string()))\n }\n\n /// Return the OpenRouter API key from env (`OPENROUTER_API_KEY`).\n pub fn openrouter_api_key() -> Result<String> {\n std::env::var(\"OPENROUTER_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"OPENROUTER_API_KEY\".to_string()))\n }\n\n /// Return AWS credentials from env (access key, secret key, optional session token).\n pub fn aws_credentials() -> Result<(String, String, Option<String>)> {\n let access = std::env::var(\"AWS_ACCESS_KEY_ID\")\n .map_err(|_| MerlinError::EnvVar(\"AWS_ACCESS_KEY_ID\".to_string()))?;\n let secret = std::env::var(\"AWS_SECRET_ACCESS_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"AWS_SECRET_ACCESS_KEY\".to_string()))?;\n let token = std::env::var(\"AWS_SESSION_TOKEN\").ok();\n Ok((access, secret, token))\n }\n\n /// Return the Jira API token from env (`JIRA_TOKEN`).\n pub fn jira_token() -> Result<String> {\n std::env::var(\"JIRA_TOKEN\").map_err(|_| MerlinError::EnvVar(\"JIRA_TOKEN\".to_string()))\n }\n\n /// Return the Linear API key from env (`LINEAR_API_KEY`).\n pub fn linear_api_key() -> Result<String> {\n std::env::var(\"LINEAR_API_KEY\")\n .map_err(|_| MerlinError::EnvVar(\"LINEAR_API_KEY\".to_string()))\n }\n\n /// Return the GitHub token from env (`GITHUB_TOKEN`).\n pub fn github_token() -> Result<String> {\n std::env::var(\"GITHUB_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITHUB_TOKEN\".to_string()))\n }\n\n /// Return the GitLab token from env (`GITLAB_TOKEN`).\n pub fn gitlab_token() -> Result<String> {\n std::env::var(\"GITLAB_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITLAB_TOKEN\".to_string()))\n }\n\n /// Return the Bitbucket token from env (`BITBUCKET_TOKEN` or `BITBUCKET_APP_PASSWORD`).\n pub fn bitbucket_token() -> Result<String> {\n std::env::var(\"BITBUCKET_TOKEN\")\n .or_else(|_| std::env::var(\"BITBUCKET_APP_PASSWORD\"))\n .map_err(|_| MerlinError::EnvVar(\"BITBUCKET_TOKEN\".to_string()))\n }\n\n /// Return the Azure DevOps PAT from env (`AZURE_DEVOPS_TOKEN` or `SYSTEM_ACCESSTOKEN`).\n pub fn azure_devops_token() -> Result<String> {\n std::env::var(\"AZURE_DEVOPS_TOKEN\")\n .or_else(|_| std::env::var(\"SYSTEM_ACCESSTOKEN\"))\n .map_err(|_| MerlinError::EnvVar(\"AZURE_DEVOPS_TOKEN\".to_string()))\n }\n\n /// Return the Gitea token from env (`GITEA_TOKEN`).\n pub fn gitea_token() -> Result<String> {\n std::env::var(\"GITEA_TOKEN\").map_err(|_| MerlinError::EnvVar(\"GITEA_TOKEN\".to_string()))\n }\n\n /// Return the Snyk API token from env (`SNYK_TOKEN`).\n pub fn snyk_token() -> Result<String> {\n std::env::var(\"SNYK_TOKEN\").map_err(|_| MerlinError::EnvVar(\"SNYK_TOKEN\".to_string()))\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n use std::io::Write;\n\n #[test]\n fn test_default_config() {\n let cfg = Config::default();\n assert_eq!(cfg.ai.max_tokens, 4096);\n assert_eq!(cfg.review.max_comments, 30);\n assert_eq!(cfg.review.chunk_lines, 200);\n assert_eq!(cfg.review.focus.len(), 4);\n assert!(!cfg.review.reflect);\n assert!(cfg.jira.base_url.is_none());\n assert!(cfg.coverage.threshold == 0.0);\n assert!(cfg.audit.enabled);\n }\n\n #[test]\n fn test_load_toml() {\n let toml = r#\"\n[ai]\nprovider = \"openai\"\nmodel = \"gpt-4o\"\nmax_tokens = 2048\ntemperature = 0.5\n\n[review]\nmax_comments = 10\nchunk_lines = 100\nfocus = [\"bugs\", \"security\"]\nreflect = true\n\n[review.persona]\nname = \"security-focused\"\nsystem_prompt_extra = \"Be extra strict about security.\"\n\n[jira]\nbase_url = \"https://company.atlassian.net\"\nproject_key = \"PROJ\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::Openai));\n assert_eq!(cfg.ai.model, \"gpt-4o\");\n assert_eq!(cfg.review.max_comments, 10);\n assert_eq!(cfg.review.focus, vec![\"bugs\", \"security\"]);\n assert!(cfg.review.reflect);\n assert_eq!(cfg.review.persona.name.as_deref(), Some(\"security-focused\"));\n assert_eq!(cfg.jira.project_key.as_deref(), Some(\"PROJ\"));\n }\n\n #[test]\n fn test_load_from_missing_file() {\n let result = Config::load(Path::new(\"/tmp/merlin_nonexistent_xyz.toml\"));\n assert!(result.is_ok());\n }\n\n #[test]\n fn test_load_from_file() {\n let mut f = tempfile::NamedTempFile::new().unwrap();\n write!(f, \"[ai]\\nprovider = \\\"anthropic\\\"\\n\").unwrap();\n let cfg = Config::load(f.path()).unwrap();\n assert!(matches!(cfg.ai.provider, AiProviderType::Anthropic));\n }\n\n #[test]\n fn test_azure_openai_config() {\n let toml = r#\"\n[ai]\nprovider = \"azure-openai\"\nmodel = \"gpt-4o\"\nazure_openai_endpoint = \"https://myresource.openai.azure.com\"\nazure_openai_api_version = \"2024-02-01\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::AzureOpenai));\n assert_eq!(\n cfg.ai.azure_openai_endpoint.as_deref(),\n Some(\"https://myresource.openai.azure.com\")\n );\n }\n\n #[test]\n fn test_bedrock_config() {\n let toml = r#\"\n[ai]\nprovider = \"bedrock\"\nmodel = \"anthropic.claude-sonnet-4-6-20250514-v1:0\"\nbedrock_region = \"us-east-1\"\n\"#;\n let cfg: Config = toml::from_str(toml).expect(\"parse failed\");\n assert!(matches!(cfg.ai.provider, AiProviderType::Bedrock));\n assert_eq!(cfg.ai.bedrock_region.as_deref(), Some(\"us-east-1\"));\n }\n}\n"}}}}