Skip to content
9 changes: 8 additions & 1 deletion src/cortex-cli/src/agent_cmd/handlers/list.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Handler for the `agent list` command.

use anyhow::Result;
use anyhow::{Result, bail};

use crate::agent_cmd::cli::ListArgs;
use crate::agent_cmd::loader::load_all_agents;
Expand All @@ -9,6 +9,13 @@ use crate::agent_cmd::utils::matches_pattern;

/// List agents command.
pub async fn run_list(args: ListArgs) -> Result<()> {
// Validate mutually exclusive flags
if args.primary && args.subagents {
bail!(
"Cannot specify both --primary and --subagents. Choose one filter or use neither for all agents."
);
}

// Handle --remote flag
if args.remote {
println!("Remote agent registry:");
Expand Down
16 changes: 12 additions & 4 deletions src/cortex-cli/src/alias_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,18 @@ async fn run_remove(args: AliasRemoveArgs) -> Result<()> {
async fn run_show(args: AliasShowArgs) -> Result<()> {
let config = load_aliases()?;

let alias = config
.aliases
.get(&args.name)
.ok_or_else(|| anyhow::anyhow!("Alias '{}' does not exist.", args.name))?;
let alias = match config.aliases.get(&args.name) {
Some(a) => a,
None => {
if args.json {
let error = serde_json::json!({
"error": format!("Alias '{}' does not exist", args.name)
});
println!("{}", serde_json::to_string_pretty(&error)?);
}
bail!("Alias '{}' does not exist.", args.name);
}
};

if args.json {
println!("{}", serde_json::to_string_pretty(alias)?);
Expand Down
46 changes: 37 additions & 9 deletions src/cortex-cli/src/cli/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub async fn dispatch_command(cli: Cli) -> Result<()> {
Some(Commands::Login(login_cli)) => handle_login(login_cli).await,
Some(Commands::Logout(logout_cli)) => handle_logout(logout_cli).await,
Some(Commands::Whoami) => {
run_whoami().await;
run_whoami().await?;
Ok(())
}
Some(Commands::Mcp(mcp_cli)) => mcp_cli.run().await,
Expand Down Expand Up @@ -182,6 +182,23 @@ async fn run_init(init_cli: InitCommand) -> Result<()> {

/// Handle login command.
async fn handle_login(login_cli: LoginCommand) -> Result<()> {
// Validate mutually exclusive authentication methods
let auth_methods_count = [
login_cli.token.is_some(),
login_cli.use_sso,
login_cli.use_device_code,
login_cli.with_api_key,
]
.iter()
.filter(|&&x| x)
.count();

if auth_methods_count > 1 {
bail!(
"Cannot specify multiple authentication methods. Choose one: --token, --sso, --device-auth, or --with-api-key."
);
}

match login_cli.action {
Some(LoginSubcommand::Status) => run_login_status(login_cli.config_overrides).await,
None => {
Expand Down Expand Up @@ -512,7 +529,7 @@ fn install_completions(shell: Shell) -> Result<()> {
// ============================================================================

/// Show current logged-in user.
pub async fn run_whoami() {
pub async fn run_whoami() -> Result<()> {
use cortex_login::{AuthMode, load_auth_with_fallback, safe_format_key};

let cortex_home = dirs::home_dir()
Expand All @@ -527,7 +544,7 @@ pub async fn run_whoami() {
"Authenticated via CORTEX_AUTH_TOKEN: {}",
safe_format_key(&token)
);
return;
return Ok(());
}

if let Ok(token) = std::env::var("CORTEX_API_KEY")
Expand All @@ -537,7 +554,7 @@ pub async fn run_whoami() {
"Authenticated via CORTEX_API_KEY: {}",
safe_format_key(&token)
);
return;
return Ok(());
}

// Load stored credentials
Expand Down Expand Up @@ -565,9 +582,10 @@ pub async fn run_whoami() {
println!("Not logged in. Run 'cortex login' to authenticate.");
}
Err(e) => {
eprintln!("Error checking login status: {}", e);
return Err(anyhow::anyhow!("Error checking login status: {}", e));
}
}
Ok(())
}

/// Resume a previous session.
Expand All @@ -585,6 +603,16 @@ pub async fn run_resume(resume_cli: ResumeCommand) -> Result<()> {
let config = cortex_engine::Config::default();

let id_str = match (resume_cli.session_id, resume_cli.last, resume_cli.pick) {
// Support "last" as SESSION_ID as documented in help text (Issue #3646)
(Some(id), _, _) if id.to_lowercase() == "last" => {
let sessions = cortex_engine::list_sessions(&config.cortex_home)?;
if sessions.is_empty() {
print_info("No sessions found to resume.");
return Ok(());
}
print_info("Resuming most recent session...");
sessions[0].id.clone()
}
(Some(id), _, _) => id,
(None, true, _) => {
let sessions = cortex_engine::list_sessions(&config.cortex_home)?;
Expand Down Expand Up @@ -810,10 +838,10 @@ pub async fn show_config(config_cli: ConfigCommand) -> Result<()> {
if let Some(value) = config.get(&args.key) {
println!("{}", value);
} else {
println!("Key '{}' not found.", args.key);
bail!("Key '{}' not found in configuration", args.key);
}
} else {
println!("No configuration file found.");
bail!("No configuration file found");
}
}
Some(ConfigSubcommand::Set(args)) => {
Expand Down Expand Up @@ -841,10 +869,10 @@ pub async fn show_config(config_cli: ConfigCommand) -> Result<()> {
std::fs::write(&config_path, content)?;
print_success(&format!("Removed key: {}", args.key));
} else {
print_warning(&format!("Key '{}' not found.", args.key));
bail!("Key '{}' not found in configuration", args.key);
}
} else {
print_warning("No configuration file found.");
bail!("No configuration file found");
}
}
None => {
Expand Down
4 changes: 2 additions & 2 deletions src/cortex-cli/src/cli/styles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ pub const AFTER_HELP: &str = color_print::cstr!(
<dim>Agents</> ~/.cortex/agents/ (personal), .cortex/agents/ (project)

<cyan,bold>🔗 LEARN MORE</>
<blue,underline>https://docs.cortex.foundation</> Documentation
<blue,underline>https://github.com/CortexLM/cortex-cli</> Source & Issues"#
<blue,underline>https://docs.cortex.foundation</> Documentation
<blue,underline>https://github.com/CortexLM/cortex</> Source & Issues"#
);

/// Before-help section with ASCII art banner.
Expand Down
51 changes: 25 additions & 26 deletions src/cortex-cli/src/compact_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,33 +407,32 @@ async fn run_compact(args: CompactRunArgs) -> Result<()> {
}

if args.dry_run {
println!("Dry run mode - no changes will be made.");
println!();
let log_files_count = dir_stats(&logs_dir).0;
let session_files_count = dir_stats(&sessions_dir).0;
let history_files_count = dir_stats(&history_dir).0;
let orphaned_history_count = count_orphaned_history(&sessions_dir, &history_dir);

let status = CompactionStatus {
data_dir: data_dir.clone(),
logs_dir: logs_dir.clone(),
sessions_dir: sessions_dir.clone(),
history_dir: history_dir.clone(),
log_files_count: dir_stats(&logs_dir).0,
log_files_size: dir_stats(&logs_dir).1,
log_files_size_human: format_size(dir_stats(&logs_dir).1),
session_files_count: dir_stats(&sessions_dir).0,
history_files_count: dir_stats(&history_dir).0,
orphaned_history_count: count_orphaned_history(&sessions_dir, &history_dir),
total_data_size: 0,
total_data_size_human: String::new(),
lock_held: false,
};

println!("Would process:");
println!(" Log files: {}", status.log_files_count);
println!(" Session files: {}", status.session_files_count);
println!(" History files: {}", status.history_files_count);
println!(
" Orphaned files to clean: {}",
status.orphaned_history_count
);
if args.json {
let output = serde_json::json!({
"dry_run": true,
"would_process": {
"log_files": log_files_count,
"session_files": session_files_count,
"history_files": history_files_count,
"orphaned_files_to_clean": orphaned_history_count
},
"message": "Dry run completed - no changes made"
});
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
println!("Dry run mode - no changes will be made.");
println!();
println!("Would process:");
println!(" Log files: {}", log_files_count);
println!(" Session files: {}", session_files_count);
println!(" History files: {}", history_files_count);
println!(" Orphaned files to clean: {}", orphaned_history_count);
}
return Ok(());
}

Expand Down
47 changes: 39 additions & 8 deletions src/cortex-cli/src/dag_cmd/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ pub async fn run_create(args: DagCreateArgs) -> Result<()> {

/// Execute a DAG.
pub async fn run_execute(args: DagRunArgs) -> Result<()> {
// Validate --jobs argument (Issue #3716)
if args.max_concurrent == 0 {
bail!("--jobs must be at least 1. Use --jobs 1 for sequential execution.");
}

let spec = load_spec(&args.file)?;
let specs = convert_specs(&spec);

Expand All @@ -86,14 +91,33 @@ pub async fn run_execute(args: DagRunArgs) -> Result<()> {

if matches!(args.strategy, ExecutionStrategy::DryRun) {
// Dry run - just validate
dag.topological_sort()
let order = dag
.topological_sort()
.map_err(|e| anyhow::anyhow!("DAG validation failed: {}", e))?;
print_success(&format!(
"✓ DAG is valid ({} tasks in execution order)",
dag.len()
));
println!();
print_execution_order(&dag)?;

match args.format {
DagOutputFormat::Json => {
let task_names: Vec<String> = order
.iter()
.filter_map(|id| dag.get_task(*id).map(|t| t.name.clone()))
.collect();
let output = serde_json::json!({
"dry_run": true,
"valid": true,
"task_count": dag.len(),
"execution_order": task_names
});
println!("{}", serde_json::to_string_pretty(&output)?);
}
_ => {
print_success(&format!(
"✓ DAG is valid ({} tasks in execution order)",
dag.len()
));
println!();
print_execution_order(&dag)?;
}
}
return Ok(());
}

Expand Down Expand Up @@ -251,7 +275,14 @@ pub async fn run_list(args: DagListArgs) -> Result<()> {
let ids = store.list().await?;

if ids.is_empty() {
print_info("No DAGs found");
match args.format {
DagOutputFormat::Json => {
println!("[]");
}
_ => {
print_info("No DAGs found");
}
}
return Ok(());
}

Expand Down
11 changes: 11 additions & 0 deletions src/cortex-cli/src/dag_cmd/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ pub fn load_spec(path: &PathBuf) -> Result<DagSpecInput> {
serde_yaml::from_str(&content).context("Failed to parse YAML")?
};

// Check for duplicate task IDs (Issue #3815)
let mut seen_names = std::collections::HashSet::new();
for task in &spec.tasks {
if !seen_names.insert(&task.name) {
anyhow::bail!(
"Duplicate task ID '{}' found in DAG specification. Task IDs must be unique.",
task.name
);
}
}

Ok(spec)
}

Expand Down
Loading