use crate::{
commands::CommandRegistry,
config::{CliConfigOverrides, McPaths},
herdr::{HerdrOwner, HerdrSessionStartSource},
instructions::discover_agents_with_additional_markdown,
sessions::SessionManager,
skills::{SkillRoots, discover_skills, filter_enabled_skills},
tui::TuiSessionConfig,
};
use anyhow::{Error, Result};
use clap::{Parser, Subcommand, ValueEnum};
use std::{env, io, io::IsTerminal};
mod mcp;
mod sessions;
mod validation;
use validation::{
validate_app_launch, validate_initial_prompt_launch, validate_session_launch,
validate_subcommand_launch, validate_tui_launch,
};
pub(crate) type AppResult<T> = std::result::Result<T, AppError>;
#[derive(Debug)]
pub(crate) enum AppError {
Usage(Error),
Runtime(Error),
}
impl AppError {
pub(crate) fn usage(error: impl Into<Error>) -> Self {
Self::Usage(error.into())
}
pub(crate) fn runtime(error: impl Into<Error>) -> Self {
Self::Runtime(error.into())
}
pub(crate) fn exit_code(&self) -> std::process::ExitCode {
match self {
Self::Usage(_) => std::process::ExitCode::from(2),
Self::Runtime(_) => std::process::ExitCode::from(1),
}
}
pub(crate) fn into_error(self) -> Error {
match self {
Self::Usage(error) | Self::Runtime(error) => error,
}
}
pub(crate) fn as_error(&self) -> &Error {
match self {
Self::Usage(error) | Self::Runtime(error) => error,
}
}
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_error().fmt(f)
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.as_error().source()
}
}
impl From<Error> for AppError {
fn from(error: Error) -> Self {
Self::Runtime(error)
}
}
fn runtime<T>(result: Result<T>) -> AppResult<T> {
result.map_err(AppError::runtime)
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct HerdrStartupSnapshot {
session_id: Option<String>,
session_start_source: Option<HerdrSessionStartSource>,
initial_title: Option<String>,
}
impl HerdrStartupSnapshot {
fn from_session(
session: Option<&crate::sessions::Session>,
session_start_source: Option<HerdrSessionStartSource>,
) -> Self {
let session_id = session.map(|session| session.id().to_string());
let initial_title = session
.and_then(|session| crate::sessions::latest_session_title(session).ok())
.flatten();
Self {
session_start_source: session_id.as_ref().and(session_start_source),
session_id,
initial_title,
}
}
}
fn report_herdr_startup(owner: &HerdrOwner, snapshot: &HerdrStartupSnapshot) {
let reporter = owner.reporter();
reporter.report_ready();
reporter.report_metadata(snapshot.initial_title.as_deref(), Some("ready"));
if let (Some(session_id), Some(source)) = (
snapshot.session_id.as_deref(),
snapshot.session_start_source,
) {
reporter.report_agent_session_with_source(session_id, source);
}
}
fn run_with_herdr_lifecycle<T>(
owner: Option<HerdrOwner>,
startup: Option<HerdrStartupSnapshot>,
mode: impl FnOnce() -> Result<T>,
) -> Result<T> {
if let (Some(owner), Some(snapshot)) = (owner.as_ref(), startup.as_ref()) {
report_herdr_startup(owner, snapshot);
}
let mode_result = mode();
if let Some(owner) = owner {
owner.release();
}
mode_result
}
fn prepare_session_for_launch(
session: crate::sessions::Session,
) -> AppResult<crate::sessions::Session> {
let session = runtime(session.admit_standalone_writer())?;
let replay = session
.read_events_tolerant_bounded(
crate::context::REPLAY_JSONL_MAX_LINES,
crate::context::REPLAY_JSONL_MAX_BYTES,
)
.map_err(|_| {
AppError::runtime(anyhow::anyhow!(
"cannot attach session: history is unreadable or exceeds bounded replay limits; use --compact or start a new session"
))
})?;
if !replay.diagnostics.is_empty() {
return Err(AppError::runtime(anyhow::anyhow!(
"cannot attach session: history contains unreadable events; use --compact or start a new session"
)));
}
Ok(session)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum AppMode {
Service,
}
#[derive(Debug, Parser)]
#[command(
name = "magi-code",
about = "Coding agent with Mission Control",
version,
after_long_help = "Mission Control slash commands: /help, /update, /login, /logout, /setmodel, /models, /new, /changes, /rewind [--to <turn>] [--dry-run], /compact, /fast [on|off|status], /prune-sessions, /sessions, /skills, /tools, /subagents, /mcp, /system-prompt, /quit"
)]
pub struct CliArgs {
#[arg(long, help = "Update this Cargo installation from crates.io")]
pub update: bool,
#[arg(
long,
value_enum,
value_name = "MODE",
help = "Run an app-scoped service mode (currently: service)"
)]
pub app: Option<AppMode>,
#[arg(
long = "prompt",
value_name = "PROMPT",
help = "Initial prompt for Mission Control"
)]
pub initial_prompt: Option<String>,
#[arg(long, help = "Override model name")]
pub model: Option<String>,
#[arg(long, help = "Override provider name")]
pub provider: Option<String>,
#[arg(
long,
help = "Deprecated: override provider API key for this process; prefer provider-specific environment variables or /login because argv can be exposed"
)]
pub api_key: Option<String>,
#[arg(
long,
value_name = "THEME",
help = "Select a built-in or custom appearance theme for this run"
)]
pub theme: Option<String>,
#[arg(long, help = "Do not create or attach to a session")]
pub no_session: bool,
#[arg(long = "continue", help = "Continue the most recent session")]
pub continue_session: bool,
#[arg(long, value_name = "SESSION_ID", help = "Resume a specific session")]
pub resume: Option<String>,
#[command(subcommand)]
pub command: Option<CliCommand>,
}
#[derive(Debug, Subcommand)]
pub enum CliCommand {
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[command(about = "Manage a private workspace daemon")]
Daemon {
#[command(subcommand)]
command: DaemonCommand,
},
#[command(about = "Repair or inspect session storage")]
Sessions {
#[command(subcommand)]
command: SessionsCommand,
},
#[command(about = "Diagnose configured MCP servers")]
Mcp {
#[command(subcommand)]
command: McpCommand,
},
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[derive(Debug, Subcommand)]
pub enum DaemonCommand {
StartOrConnect {
#[arg(long)]
workspace: std::path::PathBuf,
#[arg(long)]
state_root: Option<std::path::PathBuf>,
#[arg(long)]
executable: std::path::PathBuf,
},
Status {
#[arg(long)]
workspace: std::path::PathBuf,
#[arg(long)]
state_root: Option<std::path::PathBuf>,
},
Stop {
#[arg(long)]
workspace: std::path::PathBuf,
#[arg(long)]
state_root: Option<std::path::PathBuf>,
},
Foreground {
#[arg(long)]
workspace: std::path::PathBuf,
#[arg(long)]
state_root: Option<std::path::PathBuf>,
#[arg(long, hide = true)]
detached: bool,
},
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
impl DaemonCommand {
fn run(&self) -> Result<()> {
use crate::service::unix::{self, Identity};
let (workspace, root) = match self {
Self::StartOrConnect {
workspace,
state_root,
..
}
| Self::Status {
workspace,
state_root,
}
| Self::Stop {
workspace,
state_root,
}
| Self::Foreground {
workspace,
state_root,
..
} => (workspace, state_root),
};
let root = match root {
Some(root) => root.clone(),
None => McPaths::resolve()?.root,
};
let identity = Identity::resolve(workspace, &root)?;
match self {
Self::StartOrConnect { executable, .. } => {
unix::start_or_connect(&identity, executable)
}
Self::Status { .. } => unix::control(&identity, "status"),
Self::Stop { .. } => unix::control(&identity, "stop"),
Self::Foreground { detached, .. } => unix::foreground(identity, *detached),
}
}
}
#[derive(Debug, Subcommand)]
pub enum SessionsCommand {
#[command(about = "Measure offline compression candidates for active sessions")]
MeasureCompression {
#[arg(
long,
value_name = "MODEL",
help = "Supported openai-codex text model used for local BPE estimates"
)]
model: String,
#[arg(
long,
value_name = "N",
default_value_t = crate::tool_output_measurement::DEFAULT_MAX_FILES,
help = "Maximum recent active session files to measure (1-10000)"
)]
max_files: usize,
#[arg(
long,
value_name = "BYTES",
default_value_t = crate::tool_output_measurement::DEFAULT_MAX_BYTES,
help = "Maximum planned input bytes (1 MiB-1 GiB)"
)]
max_bytes: u64,
},
#[command(about = "Repair recognized legacy session permissions")]
RepairPermissions {
#[arg(long, conflicts_with = "dry_run", help = "Apply without confirmation")]
yes: bool,
#[arg(
long,
conflicts_with = "yes",
help = "Preview without changing permissions"
)]
dry_run: bool,
},
}
#[derive(Debug, Subcommand)]
pub enum McpCommand {
#[command(about = "List configured MCP servers and connection status")]
List,
#[command(about = "Test one configured MCP server")]
Test { server: String },
#[command(about = "Authenticate one OAuth MCP server")]
Login { server: String },
#[command(about = "Delete local OAuth token for one MCP server")]
Logout { server: String },
}
pub fn run(args: CliArgs) -> AppResult<()> {
if args.update {
validation::validate_update_launch(&args)?;
return runtime(crate::updates::run_update().map(|_| ()));
}
validate_app_launch(&args)?;
if args.app.is_some() {
return runtime(crate::service::run_stdio());
}
validate_initial_prompt_launch(&args)?;
if let Some(command) = &args.command {
validate_subcommand_launch(&args)?;
return runtime(run_subcommand(command));
}
validate_session_launch(&args)?;
validate_tui_launch()?;
let cwd = env::current_dir().map_err(AppError::runtime)?;
if args.api_key.is_some() {
eprintln!(
"warning: --api-key is deprecated because command-line arguments can be exposed by process listings; prefer provider-specific environment variables or /login"
);
}
let paths = runtime(McPaths::resolve())?;
runtime(paths.ensure_runtime_dirs())?;
let (config, settings, appearance_settings, session_settings) =
runtime(crate::config::load_startup_config_with_settings(
paths.clone(),
CliConfigOverrides {
provider: args.provider.clone(),
model: args.model.clone(),
api_key: args.api_key.clone(),
color: None,
},
))?;
let appearance_resolution = crate::appearance::resolve_startup(
&paths,
&settings,
&appearance_settings,
args.theme.as_deref(),
None,
io::stdout().is_terminal(),
)
.map_err(|error| {
if error
.downcast_ref::<crate::appearance::InvalidExplicitTheme>()
.is_some()
{
AppError::usage(error)
} else {
AppError::runtime(error)
}
})?;
for diagnostic in &appearance_resolution.diagnostics {
eprintln!("warning: {diagnostic}");
}
let appearance = appearance_resolution.appearance;
let instructions = runtime(discover_agents_with_additional_markdown(
&paths.user_agents,
&cwd,
&settings.instructions.additional_markdown_paths,
))?;
let mut skill_roots = SkillRoots::from_paths(&paths.root, &cwd);
skill_roots.additional_paths = settings.skills.additional_paths.clone();
let discovered_skills = discover_skills(&skill_roots);
let disabled_skills = crate::config::disabled_skill_names_from_settings(&settings);
let skills = filter_enabled_skills(&discovered_skills, &disabled_skills);
let commands = CommandRegistry::mvp();
let manager = SessionManager::new(paths.sessions.clone());
let (active_session, session_start_source) = if args.no_session {
(None, None)
} else if let Some(id) = args.resume {
let id = crate::sessions::validate_session_id(id).map_err(AppError::usage)?;
let session = manager
.open_existing(id)
.and_then(crate::sessions::Session::admit_standalone_writer)
.map_err(|_| {
AppError::runtime(anyhow::anyhow!(
"cannot resume session: session is missing, unreadable, unsafe, or active elsewhere; use --continue or start a new session"
))
})?;
(
Some(prepare_session_for_launch(session)?),
Some(HerdrSessionStartSource::Resume),
)
} else if args.continue_session {
match manager.most_recent() {
Ok(Some(session)) => (
Some(prepare_session_for_launch(runtime(
session.admit_standalone_writer(),
)?)?),
Some(HerdrSessionStartSource::Resume),
),
Ok(None) => (
Some(runtime(
runtime(manager.create())?.admit_standalone_writer(),
)?),
Some(HerdrSessionStartSource::Startup),
),
Err(_) => {
return Err(AppError::runtime(anyhow::anyhow!(
"cannot continue session: history discovery failed; use --resume with a valid session or start a new session"
)));
}
}
} else {
(
Some(runtime(
runtime(manager.create())?.admit_standalone_writer(),
)?),
Some(HerdrSessionStartSource::Startup),
)
};
manager.spawn_auto_prune(
session_settings.retention_days,
active_session.as_ref().map(|session| session.id()),
);
let herdr_owner = HerdrOwner::from_env(&settings.integrations.herdr);
let herdr_reporter = herdr_owner.as_ref().map(HerdrOwner::reporter);
let herdr_startup =
HerdrStartupSnapshot::from_session(active_session.as_ref(), session_start_source);
let mode_result = run_with_herdr_lifecycle(herdr_owner, Some(herdr_startup), || {
crate::tui::run_tui(TuiSessionConfig {
config,
settings,
appearance,
theme_cli_override: args.theme.is_some(),
instructions,
initial_prompt: args.initial_prompt,
discovered_skills,
skills,
commands,
manager,
active_session,
cwd,
herdr_reporter,
})
});
if let Some(restart) = mode_result.map_err(AppError::runtime)? {
runtime(crate::updates::update_and_restart(restart))?;
}
Ok(())
}
fn run_subcommand(command: &CliCommand) -> Result<()> {
match command {
#[cfg(any(target_os = "macos", target_os = "linux"))]
CliCommand::Daemon { command } => command.run(),
CliCommand::Sessions { command } => match command {
SessionsCommand::MeasureCompression {
model,
max_files,
max_bytes,
} => {
let paths = McPaths::resolve_read_only()?;
let stdout = io::stdout();
let mut stdout = stdout.lock();
crate::tool_output_measurement::run(
&paths.sessions,
model,
crate::tool_output_measurement::MeasurementLimits::new(*max_files, *max_bytes),
&mut stdout,
)
}
SessionsCommand::RepairPermissions { yes, dry_run } => {
let paths = McPaths::resolve()?;
sessions::run(&paths.sessions, *yes, *dry_run)
}
},
CliCommand::Mcp { command } => {
let stdout = io::stdout();
let stderr = io::stderr();
let mut stdout = stdout.lock();
let mut stderr = stderr.lock();
match command {
McpCommand::List => mcp::run_list(&mut stdout, &mut stderr),
McpCommand::Test { server } => mcp::run_test(server, &mut stdout, &mut stderr),
McpCommand::Login { server } => mcp::run_login(server, &mut stdout, &mut stderr),
McpCommand::Logout { server } => mcp::run_logout(server, &mut stdout, &mut stderr),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn resumed_session_preserves_history_and_rejects_unreadable_events() {
let temp = TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap();
crate::sessions::record_session_title(
&session,
temp.path(),
"Existing title",
"provider",
"model",
)
.unwrap();
let id = session.id().to_string();
drop(session);
let session = prepare_session_for_launch(manager.open_existing(&id).unwrap()).unwrap();
assert_eq!(
crate::sessions::latest_session_title(&session)
.unwrap()
.as_deref(),
Some("Existing title")
);
drop(session);
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(temp.path().join("sessions").join(format!("{id}.jsonl")))
.unwrap();
writeln!(file, "not-json").unwrap();
let error = prepare_session_for_launch(manager.open_existing(&id).unwrap()).unwrap_err();
assert!(
error
.to_string()
.contains("history contains unreadable events")
);
}
#[test]
fn standalone_resume_rejects_busy_session_before_replay() {
let temp = TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap();
let writer = session.try_frontend_writer().unwrap().unwrap();
std::fs::write(session.path(), "not-json\n").unwrap();
crate::sessions::secure_test_session_root(session.path().parent().unwrap());
let original = std::fs::read(session.path()).unwrap();
let error =
prepare_session_for_launch(manager.open_existing(session.id()).unwrap()).unwrap_err();
assert!(error.to_string().contains("session is busy"));
assert_eq!(std::fs::read(session.path()).unwrap(), original);
drop(writer);
assert!(
prepare_session_for_launch(manager.open_existing(session.id()).unwrap())
.unwrap_err()
.to_string()
.contains("history contains unreadable events")
);
assert!(session.try_frontend_writer().unwrap().is_some());
}
#[test]
fn standalone_writer_survives_owner_drop_until_worker_cleanup() {
let temp = TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap().admit_standalone_writer().unwrap();
let contender = manager.open(session.id()).unwrap();
let worker_session = session.clone();
drop(session);
assert!(contender.try_frontend_writer().unwrap().is_none());
assert!(contender.clone().admit_standalone_writer().is_err());
let worker_session = worker_session.admit_standalone_writer().unwrap();
drop(worker_session);
assert!(contender.try_frontend_writer().unwrap().is_some());
}
#[test]
fn clap_definition_is_valid() {
<CliArgs as clap::CommandFactory>::command().debug_assert();
}
#[test]
fn invalid_measurement_model_is_usage_error_before_storage_resolution() {
let env_guard = crate::test_support::env::env_lock();
let _mc_home = env_guard.save("MC_HOME");
env_guard.set_var("MC_HOME", "relative-private-mc-home");
for model in [
"text-embedding-ada-002",
"gpt-4evil",
"o123",
"gpt-5-embedding",
"codex-",
] {
let args = CliArgs::parse_from([
"magi-code",
"sessions",
"measure-compression",
"--model",
model,
]);
let error = run(args).unwrap_err();
assert_eq!(error.exit_code(), std::process::ExitCode::from(2));
assert!(
error
.to_string()
.contains("supported openai-codex text model"),
"{model}: {error}"
);
}
}
#[test]
fn api_key_help_warns_but_still_parses_for_compatibility() {
let help = <CliArgs as clap::CommandFactory>::command()
.render_long_help()
.to_string();
assert!(help.contains("--api-key"));
assert!(help.contains("Deprecated"));
let args = CliArgs::parse_from(["magi-code", "--api-key", "placeholder"]);
assert_eq!(args.api_key.as_deref(), Some("placeholder"));
}
#[test]
fn herdr_startup_reports_ready_metadata_then_session_and_release_cleanup() {
let temp = TempDir::new().unwrap();
let manager = SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap();
crate::sessions::record_session_title(
&session,
temp.path(),
"Initial title",
"provider",
"model",
)
.unwrap();
let snapshot = HerdrStartupSnapshot::from_session(
Some(&session),
Some(HerdrSessionStartSource::Startup),
);
let (owner, lines) = HerdrOwner::new_for_test();
run_with_herdr_lifecycle(Some(owner), Some(snapshot), || Ok::<_, anyhow::Error>(()))
.unwrap();
let lines = lines.lock().unwrap();
let reports = lines
.iter()
.map(|line| serde_json::from_str::<serde_json::Value>(line).unwrap())
.collect::<Vec<_>>();
assert_eq!(
reports
.iter()
.map(|report| report["method"].as_str().unwrap())
.collect::<Vec<_>>(),
vec![
"pane.report_agent",
"pane.report_metadata",
"pane.report_agent_session",
"pane.report_metadata",
"pane.release_agent",
]
);
assert_eq!(reports[1]["params"]["title"], "Initial title");
assert_eq!(reports[1]["params"]["tokens"]["summary"], "ready");
assert_eq!(reports[2]["params"]["agent_session_id"], session.id());
assert_eq!(reports[2]["params"]["session_start_source"], "startup");
assert_eq!(reports[3]["params"]["clear_title"], true);
}
#[test]
fn herdr_startup_source_mapping_distinguishes_startup_resume_and_no_session() {
let temp = TempDir::new().unwrap();
let session = SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
for (source, expected) in [
(HerdrSessionStartSource::Startup, "startup"),
(HerdrSessionStartSource::Resume, "resume"),
] {
let snapshot = HerdrStartupSnapshot::from_session(Some(&session), Some(source));
assert_eq!(snapshot.session_id.as_deref(), Some(session.id()));
assert_eq!(snapshot.session_start_source, Some(source));
let serialized = serde_json::to_value(source).unwrap();
assert_eq!(serialized, expected);
}
let no_session =
HerdrStartupSnapshot::from_session(None, Some(HerdrSessionStartSource::Resume));
assert_eq!(no_session.session_id, None);
assert_eq!(no_session.session_start_source, None);
assert_eq!(no_session.initial_title, None);
}
#[test]
fn herdr_release_happens_after_mode_return_and_preserves_success_or_error() {
let (owner, lines) = HerdrOwner::new_for_test();
let reporter = owner.reporter();
let success = run_with_herdr_lifecycle(Some(owner), None, || {
reporter.report_thinking();
Ok::<_, anyhow::Error>("mode success")
})
.unwrap();
assert_eq!(success, "mode success");
let lines = lines.lock().unwrap();
assert_eq!(lines.len(), 3);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&lines[0]).unwrap()["method"],
"pane.report_agent"
);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&lines[1]).unwrap()["method"],
"pane.report_metadata"
);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&lines[2]).unwrap()["method"],
"pane.release_agent"
);
drop(lines);
let (owner, lines) = HerdrOwner::new_for_test();
let reporter = owner.reporter();
let error = run_with_herdr_lifecycle(Some(owner), None, || {
reporter.report_done();
Err::<(), _>(anyhow::anyhow!("mode failure"))
})
.unwrap_err();
assert_eq!(error.to_string(), "mode failure");
let lines = lines.lock().unwrap();
assert_eq!(lines.len(), 3);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&lines[2]).unwrap()["method"],
"pane.release_agent"
);
}
}