use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub enum LLMProvider {
#[serde(rename = "moonshot")]
Moonshot,
#[serde(rename = "deepseek")]
DeepSeek,
#[serde(rename = "mistral")]
Mistral,
#[serde(rename = "openrouter")]
OpenRouter,
#[serde(rename = "anthropic")]
Anthropic,
#[serde(rename = "gemini")]
Gemini,
}
impl Default for LLMProvider {
fn default() -> Self {
Self::Moonshot
}
}
impl std::fmt::Display for LLMProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LLMProvider::Moonshot => write!(f, "openai"),
LLMProvider::DeepSeek => write!(f, "deepseek"),
LLMProvider::Mistral => write!(f, "mistral"),
LLMProvider::OpenRouter => write!(f, "openrouter"),
LLMProvider::Anthropic => write!(f, "anthropic"),
LLMProvider::Gemini => write!(f, "gemini"),
}
}
}
impl std::str::FromStr for LLMProvider {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"openai" => Ok(LLMProvider::Moonshot),
"deepseek" => Ok(LLMProvider::DeepSeek),
"mistral" => Ok(LLMProvider::Mistral),
"openrouter" => Ok(LLMProvider::OpenRouter),
"anthropic" => Ok(LLMProvider::Anthropic),
"gemini" => Ok(LLMProvider::Gemini),
_ => Err(format!("Unknown provider: {}", s)),
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Config {
pub project_name: Option<String>,
pub project_path: PathBuf,
pub output_path: PathBuf,
pub internal_path: PathBuf,
pub analyze_dependencies: bool,
pub identify_components: bool,
pub max_depth: u8,
pub core_component_percentage: f64,
pub max_file_size: u64,
pub include_tests: bool,
pub include_hidden: bool,
pub excluded_dirs: Vec<String>,
pub excluded_files: Vec<String>,
pub excluded_extensions: Vec<String>,
pub included_extensions: Vec<String>,
pub llm: LLMConfig,
pub cache: CacheConfig,
pub architecture_meta_path: Option<PathBuf>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct LLMConfig {
pub provider: LLMProvider,
pub api_key: String,
pub api_base_url: String,
pub model_efficient: String,
pub model_powerful: String,
pub max_tokens: u32,
pub temperature: f32,
pub retry_attempts: u32,
pub retry_delay_ms: u64,
pub timeout_seconds: u64,
pub enable_preset_tools: bool,
pub max_parallels: usize,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CacheConfig {
pub enabled: bool,
pub cache_dir: PathBuf,
pub expire_hours: u64,
}
impl Config {
pub fn from_file(path: &PathBuf) -> Result<Self> {
let mut file =
File::open(path).context(format!("Failed to open config file: {:?}", path))?;
let mut content = String::new();
file.read_to_string(&mut content)
.context("Failed to read config file")?;
let config: Config = toml::from_str(&content).context("Failed to parse config file")?;
Ok(config)
}
pub fn get_project_name(&self) -> String {
if let Some(ref name) = self.project_name {
if !name.trim().is_empty() {
return name.clone();
}
}
self.infer_project_name()
}
fn infer_project_name(&self) -> String {
if let Some(name) = self.extract_project_name_from_config_files() {
return name;
}
self.project_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
}
fn extract_project_name_from_config_files(&self) -> Option<String> {
if let Some(name) = self.extract_from_cargo_toml() {
return Some(name);
}
if let Some(name) = self.extract_from_package_json() {
return Some(name);
}
if let Some(name) = self.extract_from_pyproject_toml() {
return Some(name);
}
if let Some(name) = self.extract_from_pom_xml() {
return Some(name);
}
None
}
pub fn extract_from_cargo_toml(&self) -> Option<String> {
let cargo_path = self.project_path.join("Cargo.toml");
if !cargo_path.exists() {
return None;
}
match std::fs::read_to_string(&cargo_path) {
Ok(content) => {
let mut in_package_section = false;
for line in content.lines() {
let line = line.trim();
if line == "[package]" {
in_package_section = true;
continue;
}
if line.starts_with('[') && in_package_section {
in_package_section = false;
continue;
}
if in_package_section && line.starts_with("name") && line.contains("=") {
if let Some(name_part) = line.split('=').nth(1) {
let name = name_part.trim().trim_matches('"').trim_matches('\'');
if !name.is_empty() {
return Some(name.to_string());
}
}
}
}
}
Err(_) => return None,
}
None
}
pub fn extract_from_package_json(&self) -> Option<String> {
let package_path = self.project_path.join("package.json");
if !package_path.exists() {
return None;
}
match std::fs::read_to_string(&package_path) {
Ok(content) => {
for line in content.lines() {
let line = line.trim();
if line.starts_with("\"name\"") && line.contains(":") {
if let Some(name_part) = line.split(':').nth(1) {
let name = name_part
.trim()
.trim_matches(',')
.trim_matches('"')
.trim_matches('\'');
if !name.is_empty() {
return Some(name.to_string());
}
}
}
}
}
Err(_) => return None,
}
None
}
pub fn extract_from_pyproject_toml(&self) -> Option<String> {
let pyproject_path = self.project_path.join("pyproject.toml");
if !pyproject_path.exists() {
return None;
}
match std::fs::read_to_string(&pyproject_path) {
Ok(content) => {
let mut in_project_section = false;
let mut in_poetry_section = false;
for line in content.lines() {
let line = line.trim();
if line == "[project]" {
in_project_section = true;
in_poetry_section = false;
continue;
}
if line == "[tool.poetry]" {
in_poetry_section = true;
in_project_section = false;
continue;
}
if line.starts_with('[') && (in_project_section || in_poetry_section) {
in_project_section = false;
in_poetry_section = false;
continue;
}
if (in_project_section || in_poetry_section)
&& line.starts_with("name")
&& line.contains("=")
{
if let Some(name_part) = line.split('=').nth(1) {
let name = name_part.trim().trim_matches('"').trim_matches('\'');
if !name.is_empty() {
return Some(name.to_string());
}
}
}
}
}
Err(_) => return None,
}
None
}
fn extract_from_pom_xml(&self) -> Option<String> {
let pom_path = self.project_path.join("pom.xml");
if !pom_path.exists() {
return None;
}
match std::fs::read_to_string(&pom_path) {
Ok(content) => {
let lines: Vec<&str> = content.lines().collect();
for line in lines {
let line = line.trim();
if line.starts_with("<name>") && line.ends_with("</name>") {
let name = line
.trim_start_matches("<name>")
.trim_end_matches("</name>");
if !name.is_empty() {
return Some(name.to_string());
}
}
if line.starts_with("<artifactId>") && line.ends_with("</artifactId>") {
let name = line
.trim_start_matches("<artifactId>")
.trim_end_matches("</artifactId>");
if !name.is_empty() {
return Some(name.to_string());
}
}
}
}
Err(_) => return None,
}
None
}
}
impl Default for Config {
fn default() -> Self {
Self {
project_name: None,
project_path: PathBuf::from("."),
output_path: PathBuf::from("./litho.docs"),
internal_path: PathBuf::from("./.litho"),
analyze_dependencies: true,
identify_components: true,
max_depth: 10,
core_component_percentage: 20.0,
max_file_size: 64 * 1024, include_tests: false,
include_hidden: false,
excluded_dirs: vec![
".litho".to_string(),
"litho.docs".to_string(),
"target".to_string(),
"node_modules".to_string(),
".git".to_string(),
"build".to_string(),
"dist".to_string(),
"venv".to_string(),
".svelte-kit".to_string(),
"__pycache__".to_string(),
],
excluded_files: vec![
"litho.toml".to_string(),
"*.litho".to_string(),
"*.log".to_string(),
"*.tmp".to_string(),
"*.cache".to_string(),
"bun.lock".to_string(),
"package-lock.json".to_string(),
"yarn.lock".to_string(),
"Cargo.lock".to_string(),
".gitignore".to_string(),
"*.md".to_string(),
"*.txt".to_string(),
".env".to_string(),
],
excluded_extensions: vec![
"jpg".to_string(),
"jpeg".to_string(),
"png".to_string(),
"gif".to_string(),
"bmp".to_string(),
"ico".to_string(),
"mp3".to_string(),
"mp4".to_string(),
"avi".to_string(),
"pdf".to_string(),
"zip".to_string(),
"tar".to_string(),
"exe".to_string(),
"dll".to_string(),
"so".to_string(),
"archive".to_string(),
],
included_extensions: vec![],
architecture_meta_path: None,
llm: LLMConfig::default(),
cache: CacheConfig::default(),
}
}
}
impl Default for LLMConfig {
fn default() -> Self {
Self {
provider: LLMProvider::default(),
api_key: std::env::var("LITHO_LLM_API_KEY").unwrap_or_default(),
api_base_url: String::from("https://api-inference.modelscope.cn/v1"),
model_efficient: String::from("Qwen/Qwen3-Next-80B-A3B-Instruct"),
model_powerful: String::from("Qwen/Qwen3-235B-A22B-Instruct-2507"),
max_tokens: 131072,
temperature: 0.1,
retry_attempts: 5,
retry_delay_ms: 5000,
timeout_seconds: 300,
enable_preset_tools: false,
max_parallels: 3,
}
}
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
enabled: true,
cache_dir: PathBuf::from(".litho/cache"),
expire_hours: 8760,
}
}
}