mod client;
mod executor;
pub(crate) mod schema;
pub mod skill;
use client::{ApiRequest, ContentBlock, Message, MessageContent};
use std::fs;
use std::path::Path;
const DEFAULT_MODEL: &str = "claude-sonnet-4-5-20250929";
const MAX_TOKENS: u32 = 4096;
const MAX_ITERATIONS: usize = 15;
const SYSTEM_PROMPT: &str = "\
You are a code analysis expert. You have access to the `km` code metrics tool \
which can analyze a software repository across multiple dimensions: lines of code, \
complexity, maintainability, duplication, hotspots, ownership, and temporal coupling.
Analyze the repository using the available tools. Run the tools you consider most \
relevant, then provide a comprehensive analysis including:
1. **Overview**: Project size and language breakdown
2. **Code Health**: Overall score and grade interpretation
3. **Complexity Hotspots**: Files with highest complexity or risk
4. **Maintainability**: Files that are hardest to maintain
5. **Key Findings**: Notable patterns, risks, or areas for improvement
6. **Recommendations**: Prioritized, actionable suggestions
Be specific — reference file names and metrics. Keep the analysis concise but thorough.";
pub fn run(
provider: &str,
path: &Path,
model: Option<&str>,
output: Option<&Path>,
) -> Result<(), Box<dyn std::error::Error>> {
if provider != "claude" {
return Err(format!("Unsupported provider: {provider}. Supported: claude").into());
}
let api_key = std::env::var("ANTHROPIC_API_KEY").map_err(|_| {
"ANTHROPIC_API_KEY environment variable not set. \
Get your key at https://console.anthropic.com/"
})?;
let canonical_path = path
.canonicalize()
.map_err(|e| format!("Cannot resolve path '{}': {e}", path.display()))?;
let model = model.unwrap_or(DEFAULT_MODEL).to_string();
let tool_defs = schema::tool_definitions();
agentic_loop(&api_key, &model, &tool_defs, &canonical_path, output)
}
fn collect_final_text(content: &[ContentBlock]) -> String {
let mut text = String::new();
for block in content {
if let ContentBlock::Text { text: t } = block {
text.push_str(t);
}
}
text
}
fn save_output(text: &str, output: Option<&Path>) -> Result<(), Box<dyn std::error::Error>> {
if let Some(path) = output {
fs::write(path, text)?;
eprintln!("Report saved to {}", path.display());
}
Ok(())
}
fn execute_tool_uses(content: &[ContentBlock], project_path: &Path) -> Vec<ContentBlock> {
content
.iter()
.filter_map(|block| {
if let ContentBlock::ToolUse { id, name, input } = block {
eprintln!(" Running tool: {name}");
let result = executor::execute_tool(name, input, project_path);
Some(ContentBlock::ToolResult {
tool_use_id: id.clone(),
content: result,
})
} else {
None
}
})
.collect()
}
fn agentic_loop(
api_key: &str,
model: &str,
tool_defs: &[serde_json::Value],
project_path: &Path,
output: Option<&Path>,
) -> Result<(), Box<dyn std::error::Error>> {
let http_client = client::build_client()?;
let path_str = project_path.to_string_lossy();
let mut messages = vec![Message {
role: "user".to_string(),
content: MessageContent::Text(format!("Analyze the repository at: {path_str}")),
}];
for iteration in 0..MAX_ITERATIONS {
let request = ApiRequest {
model: model.to_string(),
max_tokens: MAX_TOKENS,
system: SYSTEM_PROMPT.to_string(),
tools: tool_defs.to_vec(),
messages: messages.clone(),
};
eprintln!("Calling Claude API (turn {})...", iteration + 1);
let response = client::send_message(&http_client, api_key, &request)?;
if response.stop_reason == "max_tokens" {
eprintln!("warning: response truncated (max_tokens reached)");
return Err("API response was truncated due to max_tokens limit. \
Try again or increase MAX_TOKENS."
.into());
}
let has_tool_use = response
.content
.iter()
.any(|b| matches!(b, ContentBlock::ToolUse { .. }));
if !has_tool_use {
let analysis = collect_final_text(&response.content);
println!("{analysis}");
save_output(&analysis, output)?;
return Ok(());
}
messages.push(Message {
role: "assistant".to_string(),
content: MessageContent::Blocks(response.content.clone()),
});
let tool_results = execute_tool_uses(&response.content, project_path);
messages.push(Message {
role: "user".to_string(),
content: MessageContent::Blocks(tool_results),
});
}
Err(format!("Exceeded maximum iterations ({MAX_ITERATIONS})").into())
}