use anyhow::Result;
use clap::{Parser, Subcommand};
use matrixcode_core::{
AgentEvent, Config, cancel::CancellationToken,
agent::AgentBuilder,
AnthropicProvider,
SessionManager,
tools::all_tools_with_skills,
memory::MemoryStorage,
};
use matrixcode_tui::{TuiApp, setup_terminal, restore_terminal};
use std::path::{PathBuf, Path};
use std::sync::Arc;
use termimad::MadSkin;
use termimad::gray;
fn create_matrixcode_skin() -> MadSkin {
let mut skin = MadSkin::default();
skin.headers[0].compound_style.set_fg(termimad::crossterm::style::Color::Cyan);
skin.headers[0].add_attr(termimad::crossterm::style::Attribute::Bold);
skin.headers[1].compound_style.set_fg(termimad::crossterm::style::Color::DarkCyan);
skin.headers[1].add_attr(termimad::crossterm::style::Attribute::Bold);
skin.headers[2].compound_style.set_fg(termimad::crossterm::style::Color::Yellow);
skin.bold.set_fg(termimad::crossterm::style::Color::White);
skin.bold.add_attr(termimad::crossterm::style::Attribute::Bold);
skin.inline_code.set_fg(termimad::crossterm::style::Color::Yellow);
skin.inline_code.set_bg(gray(20));
skin.code_block.set_fg(gray(15));
skin.code_block.set_bg(gray(5));
skin.italic.set_fg(gray(12));
skin
}
fn print_markdown(text: &str) {
let skin = create_matrixcode_skin();
skin.print_text(text);
}
fn handle_init_command(cmd: &str, project_path: Option<&Path>) -> InitCommandResult {
let parts: Vec<&str> = cmd.split_whitespace().collect();
let subcmd = parts.get(1).copied().unwrap_or("");
match subcmd {
"" => {
InitCommandResult::GenerateOverview
}
"status" => {
let path = project_path
.map(|p| p.to_path_buf())
.or_else(|| std::env::current_dir().ok())
.unwrap_or_default();
let overview_path = path.join(matrixcode_core::overview::OVERVIEW_FILENAME);
let matrix_dir = path.join(matrixcode_core::overview::MATRIXCODE_DIR);
let has_overview = overview_path.exists();
let has_memory = matrix_dir.join("memory.json").exists();
let has_session = matrix_dir.join("session.json").exists();
let overview_info = if has_overview {
if let Ok(content) = std::fs::read_to_string(&overview_path) {
let lines = content.lines().count();
format!("✓ exists ({} lines)", lines)
} else {
"✓ exists".into()
}
} else {
"❌ not found (use /init to generate)".into()
};
InitCommandResult::Message(format!(
"📊 Project: {}\n Overview: {}\n Memory: {}\n Session: {}",
path.display(),
overview_info,
if has_memory { "✓ exists" } else { "❌ none" },
if has_session { "✓ exists" } else { "❌ none" }
))
}
"clear" | "reset" => {
let path = project_path
.map(|p| p.to_path_buf())
.or_else(|| std::env::current_dir().ok())
.unwrap_or_default();
let overview_path = path.join(matrixcode_core::overview::OVERVIEW_FILENAME);
let matrix_dir = path.join(matrixcode_core::overview::MATRIXCODE_DIR);
let mut reset_msg = String::new();
if overview_path.exists() {
match std::fs::remove_file(&overview_path) {
Ok(_) => reset_msg.push_str(&format!("✓ Removed overview: {}\n", overview_path.display())),
Err(e) => reset_msg.push_str(&format!("❌ Failed to remove overview: {}\n", e)),
}
}
if matrix_dir.exists() {
match std::fs::remove_dir_all(&matrix_dir) {
Ok(_) => reset_msg.push_str(&format!("✓ Removed config dir: {}\n", matrix_dir.display())),
Err(e) => reset_msg.push_str(&format!("❌ Failed to remove config dir: {}\n", e)),
}
}
if reset_msg.is_empty() {
InitCommandResult::Message("⚠️ No project configuration found to reset.".into())
} else {
reset_msg.push_str("\nRun '/init' to regenerate project overview");
InitCommandResult::Message(reset_msg)
}
}
_ => {
InitCommandResult::Message("Unknown init command. Use: /init, /init status, /init reset".into())
}
}
}
enum InitCommandResult {
Message(String),
GenerateOverview,
}
#[derive(Parser)]
#[command(name = "matrixcode")]
#[command(about = "AI Code Agent with multi-model support")]
#[command(version)]
struct Cli {
#[arg(short, long, default_value = "terminal")]
mode: String,
#[arg(short, long)]
continue_session: bool,
#[arg(short = 'r', long)]
resume: bool,
#[arg(long)]
resume_id: Option<String>,
#[arg(long)]
list_sessions: bool,
#[arg(long)]
skills_dir: Option<PathBuf>,
#[arg(long, default_value = "true")]
think: bool,
#[arg(long, default_value = "16384")]
max_tokens: u32,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Chat {
#[arg(short, long)]
message: Option<String>,
},
QuickAction {
#[arg(short, long)]
action: String,
#[arg(short, long)]
file: Option<String>,
},
NewSession,
History,
Status,
}
fn main() -> Result<()> {
let cli = Cli::parse();
if cli.list_sessions {
list_sessions();
return Ok(());
}
if cli.resume {
return interactive_resume();
}
if cli.mode == "daemon" {
return run_daemon_mode();
}
match cli.mode.as_str() {
"terminal" | "tui" => run_terminal_mode(cli),
"service" | "json" => run_service_mode(cli),
_ => {
eprintln!("Unknown mode: {}", cli.mode);
std::process::exit(1);
}
}
}
fn interactive_resume() -> Result<()> {
use std::io::{self, Write, BufRead};
let mgr = SessionManager::new()?;
let sessions = mgr.list_sessions();
if sessions.is_empty() {
println!("No sessions found.");
println!("\nTip: Use 'matrixcode' to start a new session.");
return Ok(());
}
println!("📚 Sessions:\n");
for (i, session) in sessions.iter().enumerate() {
let project = session.project_path.as_deref()
.map(|p| p.split('/').last().unwrap_or(p))
.unwrap_or("unknown");
let is_current = mgr.has_current() && mgr.current_id() == Some(session.id.as_str());
println!(" {}. {} - {} ({} msgs, {} tokens) {}",
i + 1,
session.short_id(),
project,
session.message_count,
session.total_output_tokens,
if is_current { "[current]" } else { "" }
);
}
println!("\nSelect session to resume (1-{}), or 'q' to quit:", sessions.len());
print!("> ");
io::stdout().flush()?;
let stdin = io::stdin();
let mut lines = stdin.lock().lines();
if let Some(Ok(line)) = lines.next() {
let input = line.trim();
if input == "q" || input == "quit" || input == "exit" {
println!("Cancelled.");
return Ok(());
}
if let Ok(num) = input.parse::<usize>() {
if num > 0 && num <= sessions.len() {
let session = &sessions[num - 1];
println!("\n✓ Resuming session: {}", session.short_id());
println!(" Project: {}", session.project_path.as_deref().unwrap_or("unknown"));
println!(" Messages: {}", session.message_count);
println!("\nStarting matrixcode with resumed session...\n");
let cli = Cli {
mode: "terminal".to_string(),
continue_session: false,
resume: false,
resume_id: Some(session.id.clone()),
list_sessions: false,
skills_dir: None,
think: true,
max_tokens: 16384,
command: None,
};
return run_terminal_mode(cli);
}
}
for session in sessions.iter() {
if session.short_id() == input || session.id == input || session.id.starts_with(input) {
println!("\n✓ Resuming session: {}", session.short_id());
println!(" Project: {}", session.project_path.as_deref().unwrap_or("unknown"));
println!(" Messages: {}", session.message_count);
println!("\nStarting matrixcode with resumed session...\n");
let cli = Cli {
mode: "terminal".to_string(),
continue_session: false,
resume: false,
resume_id: Some(session.id.clone()),
list_sessions: false,
skills_dir: None,
think: true,
max_tokens: 16384,
command: None,
};
return run_terminal_mode(cli);
}
}
println!("Invalid selection: '{}'. Please enter a number 1-{} or session ID.", input, sessions.len());
}
Ok(())
}
fn load_skills(extra_dirs: &[PathBuf]) -> Vec<matrixcode_core::skills::Skill> {
use matrixcode_core::skills::discover_skills;
use std::path::PathBuf;
let mut roots: Vec<PathBuf> = Vec::new();
if let Some(home) = dirs::home_dir() {
roots.push(home.join(".matrix").join("skills"));
}
if let Ok(cwd) = std::env::current_dir() {
roots.push(cwd.join(".matrix").join("skills"));
roots.push(cwd.join("skills"));
}
roots.extend(extra_dirs.iter().cloned());
let skills = discover_skills(&roots);
skills
}
fn list_sessions() {
use matrixcode_core::session::SessionManager;
let mgr = SessionManager::new().ok();
if let Some(mgr) = mgr {
let sessions = mgr.list_sessions();
if sessions.is_empty() {
println!("No sessions found.");
println!("\nTip: Use 'matrixcode' to start a new session.");
} else {
println!("Sessions:\n");
for (i, session) in sessions.iter().enumerate() {
let status = if mgr.has_current() && mgr.current_id() == Some(session.id.as_str()) {
" [current]"
} else {
""
};
let project = session.project_path.as_deref().unwrap_or("unknown");
println!(" {}. {} ({}){}",
i + 1,
session.short_id(),
project,
status
);
}
println!("\nTotal: {} sessions", sessions.len());
println!("\nResume: matrixcode --resume <id>");
}
} else {
println!("No session manager available.");
println!("Sessions directory: ~/.matrix/sessions/");
}
}
fn run_terminal_mode(cli: Cli) -> Result<()> {
let config = Config::load();
let api_key = config.api_key.clone()
.or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok())
.ok_or_else(|| anyhow::anyhow!("No API key found. Set ANTHROPIC_AUTH_TOKEN or configure in ~/.matrix/config.json"))?;
let model = config.model.clone()
.or_else(|| std::env::var("ANTHROPIC_MODEL").ok())
.unwrap_or_else(|| "claude-sonnet-4-20250514".to_string());
let base_url = config.base_url.clone()
.or_else(|| std::env::var("ANTHROPIC_BASE_URL").ok())
.unwrap_or_else(|| "https://api.anthropic.com".to_string());
let skills_dirs: Vec<PathBuf> = cli.skills_dir.iter().cloned().collect();
let skills = load_skills(&skills_dirs);
if let Some(cmd) = cli.command {
handle_command(cmd, &skills);
return Ok(());
}
let rt = tokio::runtime::Runtime::new()?;
let (event_tx, event_rx) = tokio::sync::mpsc::channel(100);
let (task_tx, mut task_rx) = tokio::sync::mpsc::channel::<String>(10);
let (ask_tx, ask_rx) = tokio::sync::mpsc::channel::<String>(1);
let cancel_token = CancellationToken::new();
let project_path = std::env::current_dir().ok();
let (restored_messages, session_mgr_state) = {
let mut mgr = SessionManager::new().ok();
let mut messages = Vec::new();
if let Some(ref mut mgr) = mgr {
if cli.continue_session || cli.resume_id.is_some() {
let session = if let Some(ref query) = cli.resume_id {
mgr.resume(query, project_path.as_deref()).ok().flatten()
} else {
mgr.continue_last(project_path.as_deref()).ok().flatten()
};
if let Some(s) = session {
messages = s.messages.clone();
}
} else {
let _ = mgr.start_new(project_path.as_deref());
}
}
(messages, mgr)
};
let agent_cancel = cancel_token.clone();
let agent_event_tx = event_tx.clone();
let agent_api_key = api_key.clone();
let agent_model = model.clone();
let agent_base_url = base_url.clone();
let agent_think = cli.think;
let agent_max_tokens = cli.max_tokens;
let agent_restored_messages = restored_messages.clone();
let agent_project_path = project_path.clone();
let agent_approve_mode = config.approve_mode.as_ref()
.map(|m| matrixcode_core::approval::ApproveMode::parse(m))
.unwrap_or(matrixcode_core::approval::ApproveMode::Ask);
let shared_approve_mode = std::sync::Arc::new(std::sync::atomic::AtomicU8::new(agent_approve_mode.to_u8()));
let agent_fast_model = config.fast_model.clone()
.or_else(|| std::env::var("ANTHROPIC_DEFAULT_HAIKU_MODEL").ok());
let agent_skills = skills.clone();
let agent_shared_approve_mode = shared_approve_mode.clone();
let _agent_task = rt.spawn(async move {
let provider = AnthropicProvider::new(agent_api_key.clone(), agent_model.clone(), agent_base_url.clone());
let fast_provider: Option<AnthropicProvider> = if let Some(ref fast_model) = agent_fast_model {
Some(AnthropicProvider::new(agent_api_key.clone(), fast_model.clone(), agent_base_url.clone()))
} else {
None
};
let project_path_ref = agent_project_path.as_deref();
let mut memory_storage = matrixcode_core::memory::MemoryStorage::new(project_path_ref).ok();
let memory = memory_storage.as_ref()
.and_then(|ms| ms.load_combined().ok());
if let Some(ref mem) = memory
&& !mem.entries.is_empty() {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::with_data(
matrixcode_core::EventType::MemoryLoaded,
matrixcode_core::EventData::Memory {
summary: mem.generate_prompt_summary(10),
entries_count: mem.entries.len(),
},
)).await;
}
let initial_memory_summary = memory.as_ref()
.map(|mem| mem.generate_prompt_summary(20))
.unwrap_or_default();
let project_overview = project_path_ref
.and_then(|path| matrixcode_core::overview::ProjectOverview::load(path).ok().flatten());
if let Some(ref overview) = project_overview {
matrixcode_core::debug::debug_log().log("overview", &format!("Loaded project overview: {} chars", overview.content.len()));
}
let system_prompt = matrixcode_core::prompt::build_system_prompt(
&matrixcode_core::prompt::PromptProfile::Default,
&agent_skills,
project_overview.as_ref().map(|o| o.content.as_str()),
if initial_memory_summary.is_empty() { None } else { Some(&initial_memory_summary) },
);
let mut agent = AgentBuilder::new(Box::new(provider))
.system_prompt(system_prompt)
.model_name(agent_model.clone())
.max_tokens(agent_max_tokens)
.think(agent_think)
.tools(all_tools_with_skills(Arc::new(agent_skills.clone())))
.event_tx(agent_event_tx.clone())
.approve_mode(agent_approve_mode)
.build();
agent.set_approve_mode_shared(agent_shared_approve_mode);
if !agent_restored_messages.is_empty() {
agent.set_messages(agent_restored_messages);
}
let mut session_mgr = session_mgr_state;
agent.set_cancel_token(agent_cancel.clone());
agent.set_ask_channel(ask_rx);
while let Some(msg) = task_rx.recv().await {
let mut msg = msg;
if agent_cancel.is_cancelled() {
agent_event_tx.send(AgentEvent::error(
"Operation interrupted by user".to_string(),
Some("interrupted".to_string()),
None,
)).await.ok();
agent_cancel.reset();
continue;
}
let keywords = matrixcode_core::memory::extract_context_keywords(&msg);
if !keywords.is_empty() {
matrixcode_core::debug_keywords!(&keywords, &msg);
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::with_data(
matrixcode_core::EventType::KeywordsExtracted,
matrixcode_core::EventData::Keywords {
keywords: keywords.clone(),
source: msg.clone(),
},
)).await;
}
if msg == "/new" {
agent.clear_history();
if let Some(ref mut mgr) = session_mgr {
let _ = mgr.start_new(agent_project_path.as_deref());
}
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::session_ended()).await;
continue;
}
if msg.starts_with("/init") {
let result = handle_init_command(&msg, agent_project_path.as_deref());
match result {
InitCommandResult::Message(msg) => {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: msg,
percentage: None,
},
)).await;
}
InitCommandResult::GenerateOverview => {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: "🔄 Generating project overview...".into(),
percentage: Some(10),
},
)).await;
if let Some(ref path) = agent_project_path {
let overview_provider = AnthropicProvider::new(
agent_api_key.clone(),
agent_model.clone(),
agent_base_url.clone(),
);
match matrixcode_core::overview::ProjectOverview::generate_with_ai(path.as_path(), &overview_provider).await {
Ok(overview) => {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: format!("✓ Project overview generated: {}", overview.path.display()),
percentage: Some(100),
},
)).await;
matrixcode_core::debug::debug_log().log("overview", &format!("Generated overview with {} chars", overview.content.len()));
}
Err(e) => {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::error(
format!("Failed to generate overview: {}", e),
Some("overview_error".into()),
None,
)).await;
}
}
} else {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::error(
String::from("No project path set. Cannot generate overview."),
Some("no_project".into()),
None,
)).await;
}
}
}
continue;
}
if msg == "/skills" || msg.starts_with("/skills ") {
let parts: Vec<&str> = msg.split_whitespace().collect();
let subcmd = parts.get(1).copied().unwrap_or("");
let response = if subcmd.is_empty() || subcmd == "list" {
if agent_skills.is_empty() {
"📚 No skills loaded.\n\nSkills directories searched (in order):\n 1. ~/.matrix/skills (MatrixCode global)\n 2. .matrix/skills (Project local)\n 3. --skills-dir (CLI option)\n\nTo add a skill, create a .md file with frontmatter:\n---\nname: my-skill\ndescription: My skill description\n---\nSkill content here...".to_string()
} else {
let mut info = format!("📚 Loaded skills ({}):\n\n", agent_skills.len());
for skill in &agent_skills {
info.push_str(&format!("• {}: {}\n", skill.name, skill.description));
}
info.push_str("\nUsage: `/skills <name>` to view skill content.");
info.push_str("\n `/skills reload` to re-scan directories.");
info
}
} else if subcmd == "reload" {
let skills_dirs: Vec<PathBuf> = Vec::new();
let new_skills = load_skills(&skills_dirs);
let count = new_skills.len();
format!("🔄 Skills reloaded: {} skill(s) found.\n\nNote: Restart MatrixCode to use new skills.", count)
} else {
let skill_name = subcmd;
if let Some(skill) = agent_skills.iter().find(|s| s.name == skill_name) {
let files = matrixcode_core::skills::list_skill_files(&skill.dir);
let files_info = if files.len() > 1 {
format!("\n\n📁 Associated files:\n{}", files.iter().map(|f| format!(" - {}", f)).collect::<Vec<_>>().join("\n"))
} else {
String::new()
};
format!("📚 Skill: {}\n\n{}\n{}\n\nSource: {}",
skill.name,
skill.body,
files_info,
skill.source_file.display()
)
} else {
let similar: Vec<_> = agent_skills.iter()
.filter(|s| s.name.contains(skill_name) || skill_name.contains(&s.name))
.map(|s| s.name.as_str())
.collect();
if similar.is_empty() {
format!("❌ Skill '{}' not found.\n\nUse `/skills` to see available skills.", skill_name)
} else {
format!("❌ Skill '{}' not found.\n\nSimilar skills: {}", skill_name, similar.join(", "))
}
}
};
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: response,
percentage: None,
},
)).await;
continue;
}
if msg.starts_with("/") && !msg.starts_with("/skills")
&& !msg.starts_with("/compact") && !msg.starts_with("/compress")
&& !msg.starts_with("/help") && !msg.starts_with("/init")
&& !msg.starts_with("/memory") && !msg.starts_with("/overview")
&& !msg.starts_with("/save") && !msg.starts_with("/sessions")
&& !msg.starts_with("/resume") && !msg.starts_with("/loop")
&& !msg.starts_with("/exit") && !msg.starts_with("/quit")
&& !msg.starts_with("/clear") && !msg.starts_with("/debug")
&& !msg.starts_with("/status") && !msg.starts_with("/new")
&& !msg.starts_with("/load") && !msg.starts_with("/mode")
&& !msg.starts_with("/model") && !msg.starts_with("/retry")
&& !msg.starts_with("/history") && !msg.starts_with("/cron")
&& msg != "/"
{
let skill_name = msg.trim_start_matches('/');
matrixcode_core::debug::debug_log().log("skill",
&format!("Looking for skill '{}' in {} available skills", skill_name, agent_skills.len()));
for sk in &agent_skills {
matrixcode_core::debug::debug_log().log("skill", &format!(" - available: {}", sk.name));
}
if let Some(skill) = agent_skills.iter().find(|s| s.name == skill_name) {
let files = matrixcode_core::skills::list_skill_files(&skill.dir);
let files_info = if files.len() > 1 {
format!("\n\n📁 Associated files (use `read` tool to explore):\n{}",
files.iter().map(|f| format!(" - {}", f)).collect::<Vec<_>>().join("\n"))
} else {
String::new()
};
let skill_activation = format!(
"使用 skill '{}' 来处理当前任务。\n\n---\n{}\n---\n{}\n\n请按照上述 skill 指导开始执行。",
skill.name,
skill.body,
files_info
);
msg = skill_activation;
matrixcode_core::debug::debug_log().log("skill", &format!("Activated skill: {}", skill.name));
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: format!("🎯 Activating skill: {}", skill.name),
percentage: None,
},
)).await;
} else {
matrixcode_core::debug::debug_log().log("skill", &format!("Skill '{}' not found", skill_name));
}
}
if msg == "/compact" || msg == "/compress" {
let original_tokens = matrixcode_core::compress::estimate_total_tokens(agent.get_messages());
if original_tokens > 100 {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::with_data(
matrixcode_core::EventType::CompressionTriggered,
matrixcode_core::EventData::Progress {
message: format!("Compressing {} tokens...", original_tokens),
percentage: None,
},
)).await;
match matrixcode_core::compress::compress_messages(
agent.get_messages(),
matrixcode_core::compress::CompressionStrategy::SlidingWindow,
&matrixcode_core::compress::CompressionConfig::default(),
) {
Ok(compressed) => {
let compressed_tokens = matrixcode_core::compress::estimate_total_tokens(&compressed);
agent.set_messages(compressed);
let ratio = compressed_tokens as f32 / original_tokens as f32;
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::with_data(
matrixcode_core::EventType::CompressionCompleted,
matrixcode_core::EventData::Compression {
original_tokens: original_tokens as u64,
compressed_tokens: compressed_tokens as u64,
ratio,
},
)).await;
matrixcode_core::debug_compress!(original_tokens as u32, compressed_tokens, ratio);
}
Err(e) => {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::error(
format!("Compression failed: {}", e),
None,
None,
)).await;
}
}
} else {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
"Context too small, no need to compress",
None,
)).await;
}
continue;
}
if let Some(mode) = msg.strip_prefix("/mode:") {
let new_mode = match mode {
"ask" => matrixcode_core::approval::ApproveMode::Ask,
"auto" => matrixcode_core::approval::ApproveMode::Auto,
"strict" => matrixcode_core::approval::ApproveMode::Strict,
_ => continue,
};
agent.set_approve_mode(new_mode);
continue;
}
if msg == "/new" {
if let Some(ref mut mgr) = session_mgr {
let project_path = std::env::current_dir().ok();
mgr.start_new(project_path.as_deref()).ok();
agent.clear_history();
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::session_ended()).await;
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
"✓ New session created",
None,
)).await;
}
continue;
}
if msg == "/memory" || msg.starts_with("/memory ") {
let parts: Vec<&str> = msg.split_whitespace().collect();
let subcmd = parts.get(1).copied().unwrap_or("");
if let Some(ref mut ms) = memory_storage {
let response = match subcmd {
"" | "list" => {
if let Ok(mem) = ms.load_combined() {
if mem.entries.is_empty() {
"📝 No memories stored yet.\n\nMemories are auto-detected from AI responses.".to_string()
} else {
let mut info = format!("📝 Memories ({} entries):\n\n", mem.entries.len());
for (i, entry) in mem.entries.iter().enumerate().take(20) {
info.push_str(&format!("{}. {}\n", i + 1,
entry.content.chars().take(100).collect::<String>().trim_end_matches('\n')));
}
if mem.entries.len() > 20 {
info.push_str(&format!("\n... and {} more entries", mem.entries.len() - 20));
}
info
}
} else {
"❌ Failed to load memories".to_string()
}
}
"clear" => {
if let Ok(mut mem) = ms.load_global() {
mem.entries.clear();
ms.save_global(&mem).ok();
"✓ All memories cleared".to_string()
} else {
"❌ Failed to clear memories".to_string()
}
}
"stats" => {
if let Ok(mem) = ms.load_combined() {
format!("📝 Memory stats:\n Total entries: {}\n Enabled: {}",
mem.entries.len(),
mem.enabled)
} else {
"❌ Failed to get memory stats".to_string()
}
}
_ => {
"Unknown memory command. Use: list, clear, stats".to_string()
}
};
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
response,
None,
)).await;
} else {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
"❌ Memory storage not available",
None,
)).await;
}
continue;
}
if msg == "/overview" || msg.starts_with("/overview ") {
let parts: Vec<&str> = msg.split_whitespace().collect();
let subcmd = parts.get(1).copied().unwrap_or("");
let cwd = std::env::current_dir().unwrap_or_default();
let overview_path = cwd.join(matrixcode_core::overview::OVERVIEW_FILENAME);
let response = match subcmd {
"" | "show" => {
if overview_path.exists() {
let content = std::fs::read_to_string(&overview_path).unwrap_or_default();
let lines = content.lines().count();
format!("📄 Project Overview ({} lines):\n\n{}", lines,
content.chars().take(2000).collect::<String>())
} else {
"❌ No overview found. Run '/init' to generate one.".to_string()
}
}
"regenerate" | "gen" => {
"Use '/init' to regenerate project overview".to_string()
}
"path" => {
format!("Overview path: {}", overview_path.display())
}
_ => {
"Unknown overview command. Use: show, regenerate, path".to_string()
}
};
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
response,
None,
)).await;
continue;
}
if msg == "/save" || msg.starts_with("/save ") {
let parts: Vec<&str> = msg.split_whitespace().collect();
let name = parts.get(1).copied();
if let Some(ref mut mgr) = session_mgr {
let messages = agent.get_messages();
mgr.set_messages(messages.to_vec());
if let Some(n) = name {
mgr.rename_current(n).ok();
}
mgr.save_current().ok();
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
if name.is_some() {
format!("✓ Session saved as '{}'", name.unwrap())
} else {
"✓ Session saved".to_string()
},
None,
)).await;
} else {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
"❌ Session manager not available",
None,
)).await;
}
continue;
}
if msg == "/sessions" || msg == "/resume" {
if let Some(ref mgr) = session_mgr {
let sessions = mgr.list_sessions();
if sessions.is_empty() {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
"No saved sessions found",
None,
)).await;
} else {
let mut info = format!("📚 Sessions ({}):\n\n", sessions.len());
for session in sessions.iter().take(10) {
let project = session.project_path.as_deref()
.map(|p| p.split('/').last().unwrap_or(p))
.unwrap_or("unknown");
info.push_str(&format!("• {} - {} ({} msgs, {} out)\n",
session.short_id(),
project,
session.message_count,
session.total_output_tokens));
}
if sessions.len() > 10 {
info.push_str(&format!("\n... and {} more sessions", sessions.len() - 10));
}
info.push_str("\n\nUse '/load <id>' to resume a session");
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
info,
None,
)).await;
}
} else {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
"❌ Session manager not available",
None,
)).await;
}
continue;
}
if msg.starts_with("/load ") {
let session_id = msg.strip_prefix("/load ").unwrap_or("");
if let Some(ref mut mgr) = session_mgr {
let project_path = std::env::current_dir().ok();
if mgr.resume(session_id, project_path.as_deref()).is_ok() {
if let Some(msgs) = mgr.messages() {
let messages = msgs.to_vec();
agent.set_messages(messages.clone());
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
format!("✓ Session '{}' loaded ({} messages)", session_id, messages.len()),
None,
)).await;
}
} else {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
format!("❌ Session '{}' not found", session_id),
None,
)).await;
}
} else {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::progress(
"❌ Session manager not available",
None,
)).await;
}
continue;
}
if let Some(ref mem) = memory {
let context_keywords = if let Some(ref fp) = fast_provider {
matrixcode_core::memory::extract_keywords_hybrid(&msg, Some(fp as &dyn matrixcode_core::providers::Provider)).await
} else {
matrixcode_core::memory::extract_context_keywords(&msg)
};
let contextual_summary = mem.generate_contextual_summary_with_keywords(&context_keywords, 15);
if !contextual_summary.is_empty() {
agent.update_memory_summary(Some(contextual_summary));
matrixcode_core::debug::debug_log().keywords_extracted(&context_keywords, &msg);
if !context_keywords.is_empty() {
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::with_data(
matrixcode_core::EventType::KeywordsExtracted,
matrixcode_core::EventData::Keywords {
keywords: context_keywords,
source: msg.chars().take(50).collect(),
},
)).await;
}
}
}
match agent.run(msg.clone()).await {
Ok(_) => {
if let Some(ref mut mgr) = session_mgr {
let (input_tokens, output_tokens) = agent.get_token_counts();
let messages = agent.get_messages();
mgr.set_messages(messages.to_vec());
mgr.update_stats(input_tokens as u32, output_tokens);
let _ = mgr.save_current();
matrixcode_core::debug::debug_log().session_save(messages.len(), output_tokens);
}
if let Some(ref mut ms) = memory_storage {
let messages = agent.get_messages();
if let Some(last_msg) = messages.last() {
let text = match &last_msg.content {
matrixcode_core::providers::MessageContent::Text(t) => t.clone(),
matrixcode_core::providers::MessageContent::Blocks(blocks) => {
blocks.iter().filter_map(|b| match b {
matrixcode_core::ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
}).collect::<Vec<_>>().join("\n")
}
};
let detected = matrixcode_core::memory::detect_memories_from_text(
&text, None
);
if !detected.is_empty() {
let detected_count = detected.len();
if let Ok(mut mem) = ms.load_global() {
for entry in detected {
mem.add(entry);
}
let _ = ms.save_global(&mem);
matrixcode_core::debug_memory!(detected_count, text.len());
let _ = agent_event_tx.send(matrixcode_core::AgentEvent::with_data(
matrixcode_core::EventType::MemoryDetected,
matrixcode_core::EventData::Memory {
summary: format!("Detected {} memory entries", detected_count),
entries_count: detected_count,
},
)).await;
}
}
}
}
}
Err(e) => {
agent_event_tx.send(AgentEvent::error(
format!("Agent error: {}", e),
Some("agent_error".to_string()),
None,
)).await.ok();
}
}
}
});
let _guard = rt.enter();
let mut terminal = setup_terminal()?;
let mut app = TuiApp::new(task_tx, event_rx, cancel_token)
.with_ask_channel(ask_tx)
.with_shared_approve_mode(shared_approve_mode)
.with_config(&model, cli.think, cli.max_tokens, None);
if !restored_messages.is_empty() {
app.load_messages(restored_messages);
}
let result = app.run(&mut terminal);
restore_terminal()?;
result
}
fn handle_command(cmd: Commands, skills: &[matrixcode_core::skills::Skill]) {
let config = Config::load();
let api_key = config.api_key.clone()
.or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok())
.unwrap_or_else(|| {
eprintln!("❌ No API key found. Set ANTHROPIC_AUTH_TOKEN or configure in ~/.matrix/config.json");
std::process::exit(1);
});
let model = config.model.clone()
.or_else(|| std::env::var("ANTHROPIC_MODEL").ok())
.unwrap_or_else(|| "claude-sonnet-4-20250514".to_string());
let base_url = config.base_url.clone()
.or_else(|| std::env::var("ANTHROPIC_BASE_URL").ok())
.unwrap_or_else(|| "https://api.anthropic.com".to_string());
let approve_mode = config.approve_mode.as_ref()
.map(|m| matrixcode_core::approval::ApproveMode::parse(m))
.unwrap_or(matrixcode_core::approval::ApproveMode::Ask);
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
match cmd {
Commands::Chat { message } => {
if let Some(msg) = message {
let system_prompt = matrixcode_core::prompt::build_system_prompt(
&matrixcode_core::prompt::PromptProfile::Default,
skills,
None,
None,
);
let provider = AnthropicProvider::new(api_key, model.clone(), base_url);
let mut agent = AgentBuilder::new(Box::new(provider))
.system_prompt(system_prompt)
.model_name(model.clone())
.max_tokens(4096)
.tools(all_tools_with_skills(Arc::new(skills.to_vec())))
.approve_mode(approve_mode)
.build();
match agent.run(msg).await {
Ok(_) => {
let messages = agent.get_messages();
for msg in messages.iter() {
if msg.role == matrixcode_core::providers::Role::Assistant {
let is_thinking = match &msg.content {
matrixcode_core::providers::MessageContent::Text(t) => {
t.contains("<thinking>") || t.starts_with("Let me") || t.starts_with("I need to")
},
matrixcode_core::providers::MessageContent::Blocks(blocks) => {
blocks.iter().any(|b| match b {
matrixcode_core::ContentBlock::Thinking { thinking, .. } => !thinking.is_empty(),
_ => false,
})
},
};
if is_thinking {
let text = match &msg.content {
matrixcode_core::providers::MessageContent::Text(t) => t.clone(),
matrixcode_core::providers::MessageContent::Blocks(blocks) => {
blocks.iter().filter_map(|b| match b {
matrixcode_core::ContentBlock::Thinking { thinking, .. } => Some(thinking.as_str()),
matrixcode_core::ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
}).collect::<Vec<_>>().join("\n")
},
};
println!();
println!("💭 Thinking:");
println!("─{}", "─".repeat(40));
let clean_text = text.replace("<thinking>", "").replace("</thinking>", "");
println!("{}", clean_text.trim());
println!("─{}", "─".repeat(40));
}
}
}
if let Some(last) = messages.last() {
if last.role == matrixcode_core::providers::Role::Assistant {
let text = match &last.content {
matrixcode_core::providers::MessageContent::Text(t) => t.clone(),
matrixcode_core::providers::MessageContent::Blocks(blocks) => {
blocks.iter().filter_map(|b| match b {
matrixcode_core::ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
}).collect::<Vec<_>>().join("\n")
},
};
if !text.contains("<thinking>") && !text.starts_with("Let me") && !text.starts_with("I need to") {
println!();
println!("📝 Response:");
println!("─{}", "─".repeat(40));
print_markdown(&text);
println!("─{}", "─".repeat(40));
}
}
}
let (input, output) = agent.get_token_counts();
println!();
println!("📊 Tokens: {} in, {} out", input, output);
}
Err(e) => {
eprintln!("❌ Error: {}", e);
}
}
} else {
println!("Starting interactive chat session...");
println!("Note: For interactive chat, run 'matrixcode' without subcommand.");
}
}
Commands::Status => {
println!("MatrixCode Status:\n");
println!(" Version: {}", env!("CARGO_PKG_VERSION"));
println!(" Mode: Ready");
if config.api_key.is_some() || std::env::var("ANTHROPIC_AUTH_TOKEN").ok().is_some() {
println!(" API: ✓ configured");
} else {
println!(" API: ❌ not configured");
println!(" Set ANTHROPIC_AUTH_TOKEN or configure in ~/.matrix/config.json");
}
if let Some(model) = &config.model {
println!(" Model: {}", model);
} else if let Ok(model) = std::env::var("ANTHROPIC_MODEL") {
println!(" Model: {} (from env)", model);
} else {
println!(" Model: claude-sonnet-4-20250514 (default)");
}
if let Some(base_url) = &config.base_url {
println!(" Base URL: {}", base_url);
} else if let Ok(url) = std::env::var("ANTHROPIC_BASE_URL") {
println!(" Base URL: {} (from env)", url);
}
if let Some(mode) = &config.approve_mode {
println!(" Approve Mode: {}", mode);
} else {
println!(" Approve Mode: ask (default)");
}
if let Some(mgr) = SessionManager::new().ok() {
println!(" Sessions: {} (current: {})",
mgr.list_sessions().len(),
if mgr.has_current() { "yes" } else { "no" }
);
}
let project_path = std::env::current_dir().ok();
if let Some(path) = &project_path {
if let Ok(storage) = MemoryStorage::new(Some(path.as_path())) {
if let Ok(mem) = storage.load_combined() {
println!(" Memory: {} entries", mem.entries.len());
}
}
let overview_path = path.join(matrixcode_core::overview::OVERVIEW_FILENAME);
if overview_path.exists() {
if let Ok(metadata) = std::fs::metadata(&overview_path) {
let size = metadata.len();
if let Ok(modified) = metadata.modified() {
let modified_time: chrono::DateTime<chrono::Local> = modified.into();
println!(" Overview: ✓ MATRIX.md ({}, modified: {})",
if size > 1024 { format!("{} KB", size / 1024) } else { format!("{} bytes", size) },
modified_time.format("%Y-%m-%d %H:%M")
);
} else {
println!(" Overview: ✓ MATRIX.md ({})", size);
}
}
} else {
println!(" Overview: ❌ not found (use /init to generate)");
}
}
}
Commands::History => {
if let Some(mgr) = SessionManager::new().ok() {
let sessions = mgr.list_sessions();
if sessions.is_empty() {
println!("No session history found.");
} else {
println!("Session History:\n");
for session in sessions {
let project = session.project_path.as_deref().unwrap_or("unknown");
let is_current = mgr.has_current() && mgr.current_id() == Some(session.id.as_str());
println!("Session: {} ({})", session.short_id(), session.id);
println!(" Project: {}", project);
println!(" Created: {}", session.created_at.format("%Y-%m-%d %H:%M"));
println!(" Current: {}", if is_current { "yes" } else { "no" });
println!(" Messages: {}", session.message_count);
println!(" Tokens: {} in, {} out", session.last_input_tokens, session.total_output_tokens);
println!();
}
println!("Total: {} sessions", sessions.len());
println!("\nResume: matrixcode --resume <id>");
}
} else {
println!("Session manager not available.");
}
}
Commands::NewSession => {
println!("Creating new session...");
if let Some(mut mgr) = SessionManager::new().ok() {
let project_path = std::env::current_dir().ok();
if let Ok(_) = mgr.start_new(project_path.as_deref()) {
println!("✓ New session created");
if let Some(id) = mgr.current_id() {
println!(" Session ID: {}", id);
}
println!("\nStart chatting with: matrixcode");
} else {
println!("❌ Failed to create new session");
}
} else {
println!("Session manager not available.");
}
}
Commands::QuickAction { action, file } => {
println!("⚡ Quick Action: {}", action);
if let Some(f) = &file {
println!(" Target: {}", f);
}
let prompt = match action.as_str() {
"explain" => {
if let Some(f) = file {
format!("Please explain the code in {} in detail, including its purpose, structure, and key concepts.", f)
} else {
"Please explain the code in detail.".to_string()
}
}
"fix" => {
if let Some(f) = file {
format!("Please analyze {} for bugs or issues and fix them.", f)
} else {
"Please analyze the code for bugs or issues and fix them.".to_string()
}
}
"refactor" => {
if let Some(f) = file {
format!("Please refactor {} to improve its structure, readability, and maintainability.", f)
} else {
"Please refactor the code to improve its structure.".to_string()
}
}
"test" => {
if let Some(f) = file {
format!("Please write unit tests for the code in {}.", f)
} else {
"Please write unit tests for the code.".to_string()
}
}
"doc" | "document" => {
if let Some(f) = file {
format!("Please add documentation and comments to {}.", f)
} else {
"Please add documentation and comments to the code.".to_string()
}
}
"optimize" => {
if let Some(f) = file {
format!("Please optimize {} for performance and efficiency.", f)
} else {
"Please optimize the code for performance.".to_string()
}
}
"review" => {
if let Some(f) = file {
format!("Please review {} and provide feedback on code quality, potential issues, and improvements.", f)
} else {
"Please review the code and provide feedback.".to_string()
}
}
other => {
if let Some(f) = file {
format!("{}: {}", other, f)
} else {
other.to_string()
}
}
};
let system_prompt = matrixcode_core::prompt::build_system_prompt(
&matrixcode_core::prompt::PromptProfile::Fast, skills,
None,
None,
);
let provider = AnthropicProvider::new(api_key, model.clone(), base_url);
let mut agent = AgentBuilder::new(Box::new(provider))
.system_prompt(system_prompt)
.model_name(model.clone())
.max_tokens(4096)
.tools(all_tools_with_skills(Arc::new(skills.to_vec())))
.approve_mode(matrixcode_core::approval::ApproveMode::Auto) .build();
match agent.run(prompt).await {
Ok(_) => {
let messages = agent.get_messages();
for msg in messages.iter() {
if msg.role == matrixcode_core::providers::Role::Assistant {
let is_thinking = match &msg.content {
matrixcode_core::providers::MessageContent::Text(t) => {
t.contains("<thinking>") || t.starts_with("Let me") || t.starts_with("I need to")
},
matrixcode_core::providers::MessageContent::Blocks(blocks) => {
blocks.iter().any(|b| match b {
matrixcode_core::ContentBlock::Thinking { thinking, .. } => !thinking.is_empty(),
_ => false,
})
},
};
if is_thinking {
let text = match &msg.content {
matrixcode_core::providers::MessageContent::Text(t) => t.clone(),
matrixcode_core::providers::MessageContent::Blocks(blocks) => {
blocks.iter().filter_map(|b| match b {
matrixcode_core::ContentBlock::Thinking { thinking, .. } => Some(thinking.as_str()),
matrixcode_core::ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
}).collect::<Vec<_>>().join("\n")
},
};
println!();
println!("💭 Thinking:");
println!("─{}", "─".repeat(40));
let clean_text = text.replace("<thinking>", "").replace("</thinking>", "");
println!("{}", clean_text.trim());
println!("─{}", "─".repeat(40));
}
}
}
if let Some(last) = messages.last() {
if last.role == matrixcode_core::providers::Role::Assistant {
let text = match &last.content {
matrixcode_core::providers::MessageContent::Text(t) => t.clone(),
matrixcode_core::providers::MessageContent::Blocks(blocks) => {
blocks.iter().filter_map(|b| match b {
matrixcode_core::ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
}).collect::<Vec<_>>().join("\n")
},
};
if !text.contains("<thinking>") && !text.starts_with("Let me") && !text.starts_with("I need to") {
println!();
println!("📝 Result:");
println!("─{}", "─".repeat(40));
print_markdown(&text);
println!("─{}", "─".repeat(40));
}
}
}
let (input, output) = agent.get_token_counts();
println!();
println!("📊 Tokens: {} in, {} out", input, output);
println!("✓ Action completed");
}
Err(e) => {
eprintln!("❌ Error: {}", e);
}
}
}
}
});
}
fn run_service_mode(cli: Cli) -> Result<()> {
let config = Config::load();
match cli.command {
Some(Commands::Chat { message }) => {
let api_key = config.api_key.clone()
.or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok())
.ok_or_else(|| anyhow::anyhow!("No API key found"))?;
let model = config.model.clone()
.or_else(|| std::env::var("ANTHROPIC_MODEL").ok())
.unwrap_or_else(|| "claude-sonnet-4-20250514".to_string());
let base_url = config.base_url.clone()
.or_else(|| std::env::var("ANTHROPIC_BASE_URL").ok())
.unwrap_or_else(|| "https://api.anthropic.com".to_string());
let skills_dirs: Vec<PathBuf> = cli.skills_dir.iter().cloned().collect();
let skills = load_skills(&skills_dirs);
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
println!("{}", AgentEvent::session_started().to_json()?);
let system_prompt = matrixcode_core::prompt::build_system_prompt(
&matrixcode_core::prompt::PromptProfile::Default,
&skills,
None,
None,
);
let provider = AnthropicProvider::new(api_key, model.clone(), base_url);
let mut agent = AgentBuilder::new(Box::new(provider))
.system_prompt(system_prompt)
.model_name(model)
.max_tokens(4096)
.tools(all_tools_with_skills(Arc::new(skills.clone())))
.approve_mode(matrixcode_core::approval::ApproveMode::Auto)
.build();
match agent.run(message.unwrap_or_default()).await {
Ok(_) => {
let messages = agent.get_messages();
if let Some(last) = messages.last() {
let text = match &last.content {
matrixcode_core::providers::MessageContent::Text(t) => t.clone(),
matrixcode_core::providers::MessageContent::Blocks(blocks) => {
blocks.iter().filter_map(|b| match b {
matrixcode_core::ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
}).collect::<Vec<_>>().join("\n")
}
};
println!("{}", AgentEvent::text_delta(text).to_json()?);
}
}
Err(e) => {
println!("{}", AgentEvent::error(format!("Agent error: {}", e), None, None).to_json()?);
}
}
println!("{}", AgentEvent::session_ended().to_json()?);
Ok::<_, anyhow::Error>(())
})?;
}
Some(Commands::History) => {
println!("{}", AgentEvent::session_started().to_json()?);
if let Some(mgr) = SessionManager::new().ok() {
let sessions = mgr.list_sessions();
if sessions.is_empty() {
let data = serde_json::json!({
"type": "history",
"sessions": [],
"message": "No sessions found"
});
println!("{}", AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: serde_json::to_string(&data)?,
percentage: None,
},
).to_json()?);
} else {
let sessions_json: Vec<serde_json::Value> = sessions.iter().map(|s| {
serde_json::json!({
"id": s.id,
"short_id": s.short_id(),
"project_path": s.project_path,
"created_at": s.created_at.to_rfc3339(),
"message_count": s.message_count,
"input_tokens": s.last_input_tokens,
"output_tokens": s.total_output_tokens,
"is_current": mgr.has_current() && mgr.current_id() == Some(s.id.as_str())
})
}).collect();
let data = serde_json::json!({
"type": "history",
"sessions": sessions_json,
"total": sessions.len()
});
println!("{}", AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: serde_json::to_string(&data)?,
percentage: None,
},
).to_json()?);
}
} else {
println!("{}", AgentEvent::error("Session manager not available".to_string(), None, None).to_json()?);
}
println!("{}", AgentEvent::session_ended().to_json()?);
}
Some(Commands::Status) => {
println!("{}", AgentEvent::session_started().to_json()?);
let mut status = serde_json::json!({
"version": env!("CARGO_PKG_VERSION"),
"mode": "service",
"api_configured": config.api_key.is_some() || std::env::var("ANTHROPIC_AUTH_TOKEN").ok().is_some(),
});
if let Some(model) = &config.model {
status["model"] = serde_json::json!(model);
} else if let Ok(model) = std::env::var("ANTHROPIC_MODEL") {
status["model"] = serde_json::json!(format!("{} (env)", model));
} else {
status["model"] = serde_json::json!("claude-sonnet-4-20250514 (default)");
}
if let Some(base_url) = &config.base_url {
status["base_url"] = serde_json::json!(base_url);
}
if let Some(approve_mode) = &config.approve_mode {
status["approve_mode"] = serde_json::json!(approve_mode);
}
if let Some(mgr) = SessionManager::new().ok() {
status["sessions_count"] = serde_json::json!(mgr.list_sessions().len());
status["has_current_session"] = serde_json::json!(mgr.has_current());
}
let project_path = std::env::current_dir().ok();
if let Some(path) = &project_path {
if let Ok(storage) = MemoryStorage::new(Some(path.as_path())) {
if let Ok(mem) = storage.load_combined() {
status["memory_entries"] = serde_json::json!(mem.entries.len());
}
}
let overview_path = path.join(matrixcode_core::overview::OVERVIEW_FILENAME);
status["has_overview"] = serde_json::json!(overview_path.exists());
}
println!("{}", AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: serde_json::to_string(&status)?,
percentage: None,
},
).to_json()?);
println!("{}", AgentEvent::session_ended().to_json()?);
}
Some(Commands::NewSession) => {
println!("{}", AgentEvent::session_started().to_json()?);
if let Some(mut mgr) = SessionManager::new().ok() {
let project_path = std::env::current_dir().ok();
match mgr.start_new(project_path.as_deref()) {
Ok(_) => {
let data = serde_json::json!({
"success": true,
"session_id": mgr.current_id(),
"message": "New session created"
});
println!("{}", AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: serde_json::to_string(&data)?,
percentage: None,
},
).to_json()?);
}
Err(e) => {
println!("{}", AgentEvent::error(format!("Failed to create session: {}", e), None, None).to_json()?);
}
}
} else {
println!("{}", AgentEvent::error("Session manager not available".to_string(), None, None).to_json()?);
}
println!("{}", AgentEvent::session_ended().to_json()?);
}
Some(Commands::QuickAction { action, file }) => {
let api_key = config.api_key.clone()
.or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok())
.ok_or_else(|| anyhow::anyhow!("No API key found"))?;
let model = config.model.clone()
.or_else(|| std::env::var("ANTHROPIC_MODEL").ok())
.unwrap_or_else(|| "claude-sonnet-4-20250514".to_string());
let base_url = config.base_url.clone()
.or_else(|| std::env::var("ANTHROPIC_BASE_URL").ok())
.unwrap_or_else(|| "https://api.anthropic.com".to_string());
let skills_dirs: Vec<PathBuf> = cli.skills_dir.iter().cloned().collect();
let skills = load_skills(&skills_dirs);
let prompt = match action.as_str() {
"explain" => {
if let Some(f) = &file {
format!("Please explain the code in {} in detail, including its purpose, structure, and key concepts.", f)
} else {
"Please explain the code in detail.".to_string()
}
}
"fix" => {
if let Some(f) = &file {
format!("Please analyze {} for bugs or issues and fix them.", f)
} else {
"Please analyze the code for bugs or issues and fix them.".to_string()
}
}
"refactor" => {
if let Some(f) = &file {
format!("Please refactor {} to improve its structure, readability, and maintainability.", f)
} else {
"Please refactor the code to improve its structure.".to_string()
}
}
"test" => {
if let Some(f) = &file {
format!("Please write unit tests for the code in {}.", f)
} else {
"Please write unit tests for the code.".to_string()
}
}
"doc" | "document" => {
if let Some(f) = &file {
format!("Please add documentation and comments to {}.", f)
} else {
"Please add documentation and comments to the code.".to_string()
}
}
"optimize" => {
if let Some(f) = &file {
format!("Please optimize {} for performance and efficiency.", f)
} else {
"Please optimize the code for performance.".to_string()
}
}
"review" => {
if let Some(f) = &file {
format!("Please review {} and provide feedback on code quality, potential issues, and improvements.", f)
} else {
"Please review the code and provide feedback.".to_string()
}
}
other => {
if let Some(f) = &file {
format!("{}: {}", other, f)
} else {
other.to_string()
}
}
};
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
println!("{}", AgentEvent::session_started().to_json()?);
let action_data = serde_json::json!({
"action": action,
"file": file,
"status": "started"
});
println!("{}", AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: serde_json::to_string(&action_data)?,
percentage: Some(0),
},
).to_json()?);
let system_prompt = matrixcode_core::prompt::build_system_prompt(
&matrixcode_core::prompt::PromptProfile::Fast,
&skills,
None,
None,
);
let provider = AnthropicProvider::new(api_key, model.clone(), base_url);
let mut agent = AgentBuilder::new(Box::new(provider))
.system_prompt(system_prompt)
.model_name(model)
.max_tokens(4096)
.tools(all_tools_with_skills(Arc::new(skills.clone())))
.approve_mode(matrixcode_core::approval::ApproveMode::Auto)
.build();
match agent.run(prompt).await {
Ok(_) => {
let messages = agent.get_messages();
if let Some(last) = messages.last() {
let text = match &last.content {
matrixcode_core::providers::MessageContent::Text(t) => t.clone(),
matrixcode_core::providers::MessageContent::Blocks(blocks) => {
blocks.iter().filter_map(|b| match b {
matrixcode_core::ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
}).collect::<Vec<_>>().join("\n")
}
};
println!("{}", AgentEvent::text_delta(text).to_json()?);
}
let (input, output) = agent.get_token_counts();
let result_data = serde_json::json!({
"action": action,
"file": file,
"status": "completed",
"input_tokens": input,
"output_tokens": output
});
println!("{}", AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: serde_json::to_string(&result_data)?,
percentage: Some(100),
},
).to_json()?);
}
Err(e) => {
println!("{}", AgentEvent::error(format!("Quick action failed: {}", e), None, None).to_json()?);
}
}
println!("{}", AgentEvent::session_ended().to_json()?);
Ok::<_, anyhow::Error>(())
})?;
}
None => {
println!("{}", AgentEvent::error("Please specify a command".to_string(), None, None).to_json()?);
}
}
Ok(())
}
fn run_daemon_mode() -> Result<()> {
use std::io::{BufRead, Write};
eprintln!("MatrixCode Daemon started (listening on stdin)");
let stdin = std::io::stdin();
let stdout = std::io::stdout();
let mut stdout_lock = stdout.lock();
for line in stdin.lock().lines() {
let line = line?;
if line.is_empty() {
continue;
}
let request: DaemonRequest = match serde_json::from_str(&line) {
Ok(req) => req,
Err(e) => {
let error_event = AgentEvent::error(
format!("Invalid request: {}", e),
Some("parse_error".to_string()),
None,
);
writeln!(stdout_lock, "{}", error_event.to_json()?)?;
writeln!(stdout_lock, "---END---")?;
stdout_lock.flush()?;
continue;
}
};
let events = handle_daemon_request(request)?;
for event in events {
writeln!(stdout_lock, "{}", event.to_json()?)?;
}
writeln!(stdout_lock, "---END---")?;
stdout_lock.flush()?;
}
Ok(())
}
#[derive(serde::Deserialize)]
struct DaemonRequest {
#[serde(rename = "type")]
request_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
action: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
max_tokens: Option<u32>,
}
fn handle_daemon_request(request: DaemonRequest) -> Result<Vec<AgentEvent>> {
let mut events = Vec::new();
let config = Config::load();
let skills = load_skills(&[]);
events.push(AgentEvent::session_started());
match request.request_type.as_str() {
"chat" => {
if let Some(content) = request.content {
let api_key = config.api_key.clone()
.or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok())
.ok_or_else(|| anyhow::anyhow!("No API key found"))?;
let model = request.model.clone()
.or(config.model.clone())
.or_else(|| std::env::var("ANTHROPIC_MODEL").ok())
.unwrap_or_else(|| "claude-sonnet-4-20250514".to_string());
let base_url = config.base_url.clone()
.or_else(|| std::env::var("ANTHROPIC_BASE_URL").ok())
.unwrap_or_else(|| "https://api.anthropic.com".to_string());
let max_tokens = request.max_tokens.unwrap_or(4096);
let rt = tokio::runtime::Runtime::new()?;
let result = rt.block_on(async {
let provider = AnthropicProvider::new(api_key, model.clone(), base_url);
let mut agent = AgentBuilder::new(Box::new(provider))
.model_name(model)
.max_tokens(max_tokens)
.tools(all_tools_with_skills(Arc::new(skills.clone())))
.approve_mode(matrixcode_core::approval::ApproveMode::Auto)
.build();
agent.run(content).await
});
match result {
Ok(_) => {
events.push(AgentEvent::text_delta("Chat completed".to_string()));
}
Err(e) => {
events.push(AgentEvent::error(format!("Chat failed: {}", e), None, None));
}
}
} else {
events.push(AgentEvent::error("No content provided for chat", None, None));
}
}
"quick_action" => {
if let Some(action) = request.action.clone() {
let prompt = build_quick_action_prompt(&action, request.file.as_ref());
let api_key = config.api_key.clone()
.or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok())
.ok_or_else(|| anyhow::anyhow!("No API key found"))?;
let model = request.model.clone()
.or(config.model.clone())
.or_else(|| std::env::var("ANTHROPIC_MODEL").ok())
.unwrap_or_else(|| "claude-sonnet-4-20250514".to_string());
let base_url = config.base_url.clone()
.or_else(|| std::env::var("ANTHROPIC_BASE_URL").ok())
.unwrap_or_else(|| "https://api.anthropic.com".to_string());
events.push(AgentEvent::tool_use_start("action_1", action.clone(), None));
let rt = tokio::runtime::Runtime::new()?;
let result = rt.block_on(async {
let provider = AnthropicProvider::new(api_key, model.clone(), base_url);
let mut agent = AgentBuilder::new(Box::new(provider))
.model_name(model)
.max_tokens(4096)
.tools(all_tools_with_skills(Arc::new(skills.clone())))
.approve_mode(matrixcode_core::approval::ApproveMode::Auto)
.build();
agent.run(prompt).await
});
match result {
Ok(_) => {
events.push(AgentEvent::tool_result("action_1", "action", "Action completed".to_string(), false));
}
Err(e) => {
events.push(AgentEvent::tool_result("action_1", "action", format!("Error: {}", e), true));
}
}
} else {
events.push(AgentEvent::error("No action specified", None, None));
}
}
"status" => {
let status = serde_json::json!({
"version": env!("CARGO_PKG_VERSION"),
"mode": "daemon",
"api_configured": config.api_key.is_some() || std::env::var("ANTHROPIC_AUTH_TOKEN").ok().is_some(),
"model": config.model.clone()
.or_else(|| std::env::var("ANTHROPIC_MODEL").ok())
.unwrap_or_else(|| "claude-sonnet-4-20250514 (default)".to_string()),
});
events.push(AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: serde_json::to_string(&status)?,
percentage: None,
},
));
}
"history" => {
if let Some(mgr) = SessionManager::new().ok() {
let sessions = mgr.list_sessions();
let sessions_json: Vec<serde_json::Value> = sessions.iter().map(|s| {
serde_json::json!({
"id": s.id,
"short_id": s.short_id(),
"project_path": s.project_path,
"created_at": s.created_at.to_rfc3339(),
"message_count": s.message_count,
})
}).collect();
let data = serde_json::json!({
"type": "history",
"sessions": sessions_json,
"total": sessions.len()
});
events.push(AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: serde_json::to_string(&data)?,
percentage: None,
},
));
} else {
events.push(AgentEvent::error("Session manager not available", None, None));
}
}
"new_session" => {
if let Some(mut mgr) = SessionManager::new().ok() {
let project_path = std::env::current_dir().ok();
match mgr.start_new(project_path.as_deref()) {
Ok(_) => {
let data = serde_json::json!({
"success": true,
"session_id": mgr.current_id(),
"message": "New session created"
});
events.push(AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: serde_json::to_string(&data)?,
percentage: None,
},
));
}
Err(e) => {
events.push(AgentEvent::error(format!("Failed to create session: {}", e), None, None));
}
}
} else {
events.push(AgentEvent::error("Session manager not available", None, None));
}
}
"load_session" => {
if let Some(session_id) = request.session_id.clone() {
if let Some(mut mgr) = SessionManager::new().ok() {
let project_path = std::env::current_dir().ok();
match mgr.resume(&session_id, project_path.as_deref()) {
Ok(Some(session)) => {
let data = serde_json::json!({
"success": true,
"session_id": session.metadata.id,
"message_count": session.messages.len(),
"message": "Session loaded"
});
events.push(AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: serde_json::to_string(&data)?,
percentage: None,
},
));
}
Ok(None) => {
events.push(AgentEvent::error(format!("Session '{}' not found", session_id), None, None));
}
Err(e) => {
events.push(AgentEvent::error(format!("Failed to load session: {}", e), None, None));
}
}
} else {
events.push(AgentEvent::error("Session manager not available", None, None));
}
} else {
events.push(AgentEvent::error("No session_id provided", None, None));
}
}
"list_sessions" => {
if let Some(mgr) = SessionManager::new().ok() {
let sessions = mgr.list_sessions();
let sessions_json: Vec<serde_json::Value> = sessions.iter().map(|s| {
serde_json::json!({
"id": s.id,
"short_id": s.short_id(),
"project": s.project_path.as_deref().unwrap_or("unknown"),
})
}).collect();
events.push(AgentEvent::with_data(
matrixcode_core::EventType::Progress,
matrixcode_core::EventData::Progress {
message: serde_json::to_string(&serde_json::json!({ "sessions": sessions_json }))?,
percentage: None,
},
));
} else {
events.push(AgentEvent::error("Session manager not available", None, None));
}
}
"ping" => {
events.push(AgentEvent::text_delta("pong".to_string()));
}
_ => {
events.push(AgentEvent::error(
format!("Unknown request type: {}", request.request_type),
Some("unknown_type".to_string()),
None,
));
}
}
events.push(AgentEvent::session_ended());
Ok(events)
}
fn build_quick_action_prompt(action: &str, file: Option<&String>) -> String {
match action {
"explain" => {
if let Some(f) = file {
format!("Please explain the code in {} in detail, including its purpose, structure, and key concepts.", f)
} else {
"Please explain the code in detail.".to_string()
}
}
"fix" => {
if let Some(f) = file {
format!("Please analyze {} for bugs or issues and fix them.", f)
} else {
"Please analyze the code for bugs or issues and fix them.".to_string()
}
}
"refactor" => {
if let Some(f) = file {
format!("Please refactor {} to improve its structure, readability, and maintainability.", f)
} else {
"Please refactor the code to improve its structure.".to_string()
}
}
"test" => {
if let Some(f) = file {
format!("Please write unit tests for the code in {}.", f)
} else {
"Please write unit tests for the code.".to_string()
}
}
"doc" | "document" => {
if let Some(f) = file {
format!("Please add documentation and comments to {}.", f)
} else {
"Please add documentation and comments to the code.".to_string()
}
}
"optimize" => {
if let Some(f) = file {
format!("Please optimize {} for performance and efficiency.", f)
} else {
"Please optimize the code for performance.".to_string()
}
}
"review" => {
if let Some(f) = file {
format!("Please review {} and provide feedback on code quality, potential issues, and improvements.", f)
} else {
"Please review the code and provide feedback.".to_string()
}
}
other => {
if let Some(f) = file {
format!("{}: {}", other, f)
} else {
other.to_string()
}
}
}
}