mod agent;
mod services;
mod system;
mod tools;
mod tui;
mod utils;
use crate::agent::provider::{AnthropicProvider, Provider, StaticProvider};
use crate::agent::Agent;
use crate::system::database::{EncryptedSqliteMemory, MemoryStore};
use crate::system::fs::{FileSystem, RealFileSystem};
use crate::system::grep::RipGrep;
use crate::system::secrets::{KeyringStore, SecretStore};
use crate::tools::bash::BashTool;
use crate::tools::grep::GrepTool;
use crate::tools::knowledge::ProjectFactTool;
use crate::tools::ls::ListTool;
use crate::tools::read::FileReadTool;
use crate::tools::write::FileWriteTool;
use clap::Parser;
use std::env;
use std::fs;
use std::sync::Arc;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(short, long)]
logout: bool,
}
#[derive(Debug)]
struct Config {
api_key: String,
model: String,
source: String,
}
pub(crate) const DEFAULT_MODEL: &str = "claude-sonnet-4-6";
fn parse_key_file(content: &str) -> Option<(String, String)> {
let lines: Vec<&str> = content.lines().collect();
let key = lines.first().map(|s| s.trim()).filter(|s| !s.is_empty())?;
let model = lines
.get(1)
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.unwrap_or(DEFAULT_MODEL)
.to_string();
Some((key.to_string(), model))
}
async fn discover_config_ext(file_path: &str) -> Option<Config> {
if let Ok(key) = env::var("ANTHROPIC_API_KEY") {
let model = env::var("ANTHROPIC_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.to_string());
return Some(Config {
api_key: key.trim().to_string(),
model,
source: "ENV".to_string(),
});
}
let primary_service = "magi-rs";
let legacy_service = "magi-rust";
let primary_store = KeyringStore::new(primary_service);
let legacy_store = KeyringStore::new(legacy_service);
if let Ok(Some(key)) = primary_store.get_secret("ANTHROPIC_API_KEY").await {
let model = env::var("ANTHROPIC_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.to_string());
return Some(Config {
api_key: key,
model,
source: format!("Keyring ({})", primary_service),
});
}
if let Ok(Some(key)) = legacy_store.get_secret("ANTHROPIC_API_KEY").await {
if primary_store
.set_secret("ANTHROPIC_API_KEY", &key)
.await
.is_ok()
{
let _ = legacy_store.delete_secret("ANTHROPIC_API_KEY").await;
}
let model = env::var("ANTHROPIC_MODEL").unwrap_or_else(|_| DEFAULT_MODEL.to_string());
return Some(Config {
api_key: key,
model,
source: format!("Keyring (Migrated from {})", legacy_service),
});
}
if let Ok(content) = fs::read_to_string(file_path) {
if let Some((api_key, model)) = parse_key_file(&content) {
return Some(Config {
api_key,
model,
source: file_path.to_string(),
});
}
}
None
}
async fn discover_or_create_master_key() -> anyhow::Result<String> {
let primary_store = KeyringStore::new("magi-rs-internal");
if let Ok(Some(key)) = primary_store.get_secret("DB_MASTER_KEY").await {
return Ok(key);
}
let legacy_store = KeyringStore::new("magi-rust-internal");
if let Ok(Some(key)) = legacy_store.get_secret("DB_MASTER_KEY").await {
if primary_store
.set_secret("DB_MASTER_KEY", &key)
.await
.is_ok()
{
let _ = legacy_store.delete_secret("DB_MASTER_KEY").await;
}
return Ok(key);
}
use base64::{engine::general_purpose::STANDARD, Engine as _};
use rand::{thread_rng, RngCore};
let mut key_bytes = [0u8; 32];
thread_rng().fill_bytes(&mut key_bytes);
let new_key = STANDARD.encode(key_bytes);
primary_store.set_secret("DB_MASTER_KEY", &new_key).await?;
Ok(new_key)
}
#[derive(Debug)]
enum MemoryAttachment {
Encrypted(String),
Ephemeral,
}
fn decide_memory_attachment(key_result: anyhow::Result<String>) -> MemoryAttachment {
match key_result {
Ok(master_pwd) => MemoryAttachment::Encrypted(master_pwd),
Err(_) => MemoryAttachment::Ephemeral,
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
let workspace_root = env::current_dir()?;
if args.logout {
let _ = KeyringStore::new("magi-rs")
.delete_secret("ANTHROPIC_API_KEY")
.await;
let _ = KeyringStore::new("magi-rust")
.delete_secret("ANTHROPIC_API_KEY")
.await;
println!("Logged out successfully.");
return Ok(());
}
let config = discover_config_ext("key.txt").await;
let (provider, provider_info): (Arc<dyn Provider>, String) = if let Some(ref c) = config {
(
Arc::new(AnthropicProvider::new(c.api_key.clone(), c.model.clone())),
format!("Magi API ({}) Model: {}", c.source, c.model),
)
} else {
(
Arc::new(StaticProvider),
"Static Mode: no API key found. Set ANTHROPIC_API_KEY or use key.txt \
(recommended). /login (OAuth) is best-effort and may be rate-limited."
.to_string(),
)
};
let mut agent = Agent::new(provider);
let db_path = workspace_root.join(".magi-rs-memory.db");
let mut startup_notices = vec![provider_info];
match decide_memory_attachment(discover_or_create_master_key().await) {
MemoryAttachment::Encrypted(master_pwd) => {
let store = EncryptedSqliteMemory::new(db_path, master_pwd)?;
if store.was_reset() {
startup_notices.push(
"Note: existing on-disk history used an incompatible/corrupt format and \
has been reset (fresh start)."
.to_string(),
);
}
let memory: Arc<dyn MemoryStore> = Arc::new(store);
let sessions = memory.list_sessions().await?;
let session_id = if let Some((id, _)) = sessions.first() {
id.clone()
} else {
memory.create_session("default").await?
};
agent.set_memory(memory.clone(), session_id);
let _ = agent.load_history().await;
agent.register_tool(Box::new(ProjectFactTool::new(memory.clone())));
}
MemoryAttachment::Ephemeral => {
startup_notices.push(
"WARNING: the OS keyring is unavailable, so this session runs WITHOUT \
persistence — your conversation and project knowledge will NOT be saved \
(any existing on-disk database is left untouched). Run /login or check \
your OS keyring to restore persistence."
.to_string(),
);
}
}
let fs: Arc<dyn FileSystem> = Arc::new(RealFileSystem::new());
agent.register_tool(Box::new(ListTool::new(fs.clone(), workspace_root.clone())?));
agent.register_tool(Box::new(FileReadTool::new(
fs.clone(),
workspace_root.clone(),
)?));
agent.register_tool(Box::new(FileWriteTool::new(
fs.clone(),
workspace_root.clone(),
)?));
agent.register_tool(Box::new(GrepTool::new(
Box::new(RipGrep::new("rg")),
workspace_root.clone(),
)?));
agent.register_tool(Box::new(BashTool::new(workspace_root.clone())?));
crate::tui::run_tui_ext(agent, startup_notices).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_master_key_present_attaches_encrypted_memory() {
let outcome = decide_memory_attachment(Ok("real-master-key".to_string()));
match outcome {
MemoryAttachment::Encrypted(pwd) => assert_eq!(pwd, "real-master-key"),
MemoryAttachment::Ephemeral => {
panic!("expected encrypted attachment when key is present")
}
}
}
#[test]
fn test_master_key_error_degrades_to_ephemeral_without_constant() {
let outcome = decide_memory_attachment(Err(anyhow::anyhow!("keyring inaccessible")));
assert!(
matches!(outcome, MemoryAttachment::Ephemeral),
"a keyring failure must degrade to an ephemeral session, never to a constant key"
);
if let MemoryAttachment::Encrypted(pwd) =
decide_memory_attachment(Err(anyhow::anyhow!("x")))
{
panic!("error path produced a passphrase: {pwd}");
}
}
#[test]
fn test_parse_key_file_falls_back_to_default_model_on_blank_line() {
assert_eq!(
parse_key_file("sk-ant-xyz\nclaude-opus-4-7\n"),
Some(("sk-ant-xyz".to_string(), "claude-opus-4-7".to_string()))
);
for content in [
"sk-ant-xyz\n\n",
"sk-ant-xyz\n \n",
"sk-ant-xyz\n",
"sk-ant-xyz",
] {
assert_eq!(
parse_key_file(content),
Some(("sk-ant-xyz".to_string(), DEFAULT_MODEL.to_string())),
"blank/absent model line must fall back to DEFAULT_MODEL (content: {content:?})"
);
}
assert_eq!(parse_key_file(""), None);
assert_eq!(parse_key_file(" \n"), None);
assert_eq!(DEFAULT_MODEL, "claude-sonnet-4-6");
}
}