use anyhow::Result;
use crate::plugin::PluginRegistry;
use crate::config::HookTrigger;
pub async fn build_system_prompt() -> Result<String> {
build_system_prompt_for_model("an AI assistant", None, &HookTrigger::OnContextBuild).await
}
pub async fn build_system_prompt_for_model(model: &str, plugins: Option<&PluginRegistry>, trigger: &HookTrigger) -> Result<String> {
let mut parts: Vec<String> = Vec::new();
parts.push(base_system_prompt(model));
parts.push(format!("# Environment"));
parts.push(format!("- Platform: {}", std::env::consts::OS));
parts.push(format!("- Shell: {}", std::env::var("SHELL").unwrap_or_else(|_| "sh".into())));
if let Ok(cwd) = std::env::current_dir() {
parts.push(format!("- Working directory: {}", cwd.display()));
}
parts.push(format!(
"- Date: {}",
chrono::Local::now().format("%Y-%m-%d")
));
if let Some(git_info) = git_status().await {
parts.push(format!("\n# Git Status\n{}", git_info));
}
if let Some(claude_md) = read_claude_md().await {
parts.push(format!("\n# Project Context (CLAUDE.md)\n{}", claude_md));
}
if let Some(registry) = plugins {
if let Ok(plugin_context) = registry.execute_all(trigger, None) {
if !plugin_context.is_empty() {
parts.push(format!("\n# Plugin Context\n{}", plugin_context));
}
}
}
Ok(parts.join("\n"))
}
fn base_system_prompt(model: &str) -> String {
format!(r#"You are {}, running inside claux, a terminal AI coding assistant.
You are an interactive agent that helps users with software engineering tasks. Use the tools available to you to assist the user.
# Tool usage
- Use Read to read files, not Bash with cat
- Use Edit for surgical file changes
- Use Write to create new files
- Use Glob to find files by pattern
- Use Grep to search file contents
- Use Bash for shell commands, builds, git operations
# Style
- Be concise and direct
- Lead with the answer, not the reasoning
- When referencing code, include file_path:line_number
"#, model)
}
async fn git_status() -> Option<String> {
let branch = run_cmd("git", &["branch", "--show-current"]).await?;
let status = run_cmd("git", &["status", "--short"]).await.unwrap_or_default();
let log = run_cmd("git", &["log", "--oneline", "-n", "5"]).await.unwrap_or_default();
let mut info = format!("Branch: {}", branch.trim());
if !status.is_empty() {
let status = if status.len() > 2000 {
format!("{}... (truncated)", &status[..2000])
} else {
status
};
info.push_str(&format!("\nStatus:\n{}", status));
}
if !log.is_empty() {
info.push_str(&format!("\nRecent commits:\n{}", log));
}
Some(info)
}
async fn read_claude_md() -> Option<String> {
let mut parts: Vec<String> = Vec::new();
let cwd = std::env::current_dir().ok()?;
for name in &["CLAUDE.md", ".claude/CLAUDE.md"] {
let path = cwd.join(name);
if path.exists() {
if let Ok(content) = std::fs::read_to_string(&path) {
parts.push(format!("# {} ({})\n{}", name, cwd.display(), content));
}
}
}
let mut dir = cwd.as_path();
while let Some(parent) = dir.parent() {
if parent == cwd {
dir = parent;
continue;
}
for name in &["CLAUDE.md", ".claude/CLAUDE.md"] {
let path = parent.join(name);
if path.exists() {
if let Ok(content) = std::fs::read_to_string(&path) {
parts.push(format!("# {} ({})\n{}", name, parent.display(), content));
}
}
}
dir = parent;
}
if let Ok(home) = std::env::var("HOME") {
let path = std::path::PathBuf::from(&home)
.join(".claude")
.join("CLAUDE.md");
if path.exists() {
if let Ok(content) = std::fs::read_to_string(&path) {
parts.push(format!("# ~/.claude/CLAUDE.md\n{}", content));
}
}
}
if parts.is_empty() {
None
} else {
Some(parts.join("\n\n"))
}
}
async fn run_cmd(program: &str, args: &[&str]) -> Option<String> {
let output = tokio::process::Command::new(program)
.args(args)
.output()
.await
.ok()?;
if !output.status.success() {
return None;
}
Some(String::from_utf8_lossy(&output.stdout).to_string())
}