Skip to content

Commit 59f5a37

Browse files
echobtfactorydroid
andauthored
feat(cortex-cli): implement 10 open PR features (#186) (#189)
* feat(cortex-cli): implement 10 open PR features This commit implements the following features from open PRs: - PR #98: Improve stats command message for new users with tracking info - PR #99: Implement --share flag to display share URL using cortex-share - PR #100: Warn when showing hidden agent details in 'agent show' - PR #101: Standardize session ID error message format to 'Invalid session ID format:' - PR #102: Add --port and --host options to debug wait command for TCP port checks - PR #103: Validate empty prompt before authentication in exec mode - PR #104: Add --names-only flag for dynamic shell completion of agent names - PR #105: Document sandbox modes in help text with examples - PR #106: Improve PR checkout confirmation to show source branch info - PR #107: Correct help message to use lowercase 'cortex' command Changes include: - stats_cmd.rs: Added informative output explaining what stats will track - run_cmd.rs: Added cortex-share integration for session sharing - agent_cmd.rs: Added hidden agent warning and --names-only flag - debug_cmd.rs: Added TCP port wait functionality with --port/--host - export_cmd.rs: Standardized session ID error message - main.rs: Added empty prompt validation, sandbox docs, dynamic completion - pr_cmd.rs: Enhanced checkout message with source branch info - login.rs: Fixed command name in help text - Cargo.toml: Added cortex-share dependency * fix: resolve merge conflicts and fix API changes - Fixed duplicate import of get_cortex_home in login.rs - Updated fabric_home to cortex_home in debug_cmd.rs - Fixed run_logout call to include skip_confirmation parameter - Re-added yes flag to LogoutCommand struct --------- Co-authored-by: Droid Agent <droid@factory.ai>
1 parent dd19dff commit 59f5a37

10 files changed

Lines changed: 159 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cortex-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ cortex-process-hardening = { workspace = true }
3434
cortex-app-server = { workspace = true }
3535
cortex-update = { workspace = true }
3636
cortex-mcp-server = { workspace = true }
37+
cortex-share = { path = "../cortex-share" }
3738

3839
clap = { workspace = true }
3940
clap_complete = { workspace = true }

cortex-cli/src/agent_cmd.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ pub struct ListArgs {
5353
/// Show all agents including hidden ones.
5454
#[arg(long)]
5555
pub all: bool,
56+
57+
/// Output only agent names (one per line) for shell completion.
58+
#[arg(long, hide = true)]
59+
pub names_only: bool,
5660
}
5761

5862
/// Arguments for show command.
@@ -700,6 +704,14 @@ async fn run_list(args: ListArgs) -> Result<()> {
700704
})
701705
.collect();
702706

707+
// Output names only for shell completion
708+
if args.names_only {
709+
for agent in &filtered {
710+
println!("{}", agent.name);
711+
}
712+
return Ok(());
713+
}
714+
703715
if args.json {
704716
let json = serde_json::to_string_pretty(&filtered)?;
705717
println!("{json}");
@@ -791,6 +803,15 @@ async fn run_show(args: ShowArgs) -> Result<()> {
791803
.find(|a| a.name == args.name)
792804
.ok_or_else(|| anyhow::anyhow!("Agent '{}' not found", args.name))?;
793805

806+
// Warn if the agent is hidden
807+
if agent.hidden {
808+
eprintln!(
809+
"Note: '{}' is a hidden agent (not shown in default listings).",
810+
agent.name
811+
);
812+
eprintln!();
813+
}
814+
794815
if args.json {
795816
let json = serde_json::to_string_pretty(&agent)?;
796817
println!("{json}");

cortex-cli/src/debug_cmd.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ async fn run_config(args: ConfigArgs) -> Result<()> {
227227
println!("Config Diff (Global vs Local)");
228228
println!("{}", "=".repeat(50));
229229

230-
let global_path = config.fabric_home.join("config.toml");
230+
let global_path = config.cortex_home.join("config.toml");
231231
let local_path = std::env::current_dir()
232232
.ok()
233233
.map(|d| d.join(".cortex/config.toml"));
@@ -1607,6 +1607,14 @@ pub struct WaitArgs {
16071607
#[arg(long, default_value = "http://127.0.0.1:3000")]
16081608
pub server_url: String,
16091609

1610+
/// Wait for a TCP port to be available.
1611+
#[arg(long)]
1612+
pub port: Option<u16>,
1613+
1614+
/// Host to check when using --port (default: 127.0.0.1).
1615+
#[arg(long, default_value = "127.0.0.1")]
1616+
pub host: String,
1617+
16101618
/// Timeout in seconds.
16111619
#[arg(long, default_value = "30")]
16121620
pub timeout: u64,
@@ -1692,9 +1700,38 @@ async fn run_wait(args: WaitArgs) -> Result<()> {
16921700
error = Some(format!("Timeout waiting for server at {}", args.server_url));
16931701
}
16941702

1703+
(condition, success, error)
1704+
} else if let Some(port) = args.port {
1705+
// Wait for TCP port to be available
1706+
let condition = format!("port_ready ({}:{})", args.host, port);
1707+
let mut success = false;
1708+
let mut error = None;
1709+
1710+
let addr = format!("{}:{}", args.host, port);
1711+
1712+
while start.elapsed() < timeout {
1713+
match tokio::net::TcpStream::connect(&addr).await {
1714+
Ok(_) => {
1715+
// Port is open and accepting connections
1716+
success = true;
1717+
break;
1718+
}
1719+
Err(_) => {
1720+
tokio::time::sleep(interval).await;
1721+
}
1722+
}
1723+
}
1724+
1725+
if !success {
1726+
error = Some(format!(
1727+
"Timeout waiting for port {} on {}",
1728+
port, args.host
1729+
));
1730+
}
1731+
16951732
(condition, success, error)
16961733
} else {
1697-
bail!("No condition specified. Use --lsp-ready or --server-ready");
1734+
bail!("No condition specified. Use --lsp-ready, --server-ready, or --port");
16981735
};
16991736

17001737
let waited_ms = start.elapsed().as_millis() as u64;

cortex-cli/src/export_cmd.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ impl ExportCommand {
130130
}
131131
} else {
132132
bail!(
133-
"Invalid session ID: {session_id}. Expected full UUID or 8-character prefix."
133+
"Invalid session ID format: {session_id}. Expected full UUID or 8-character prefix."
134134
);
135135
}
136136
}

cortex-cli/src/login.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
//! Login command handlers.
22
3-
use cortex_common::{CliConfigOverrides, get_cortex_home};
3+
use cortex_common::CliConfigOverrides;
44
use cortex_login::{
55
AuthMode, CredentialsStoreMode, SecureAuthData, load_auth_with_fallback, logout,
66
safe_format_key, save_auth_with_fallback,
77
};
88
use std::collections::HashSet;
9-
use std::io::{BufRead, IsTerminal, Read};
9+
use std::io::{IsTerminal, Read};
1010
use std::path::PathBuf;
1111

1212
/// Check for duplicate config override keys and warn the user.
@@ -33,7 +33,7 @@ fn check_duplicate_config_overrides(config_overrides: &CliConfigOverrides) {
3333

3434
/// Get the cortex home directory using unified directory management.
3535
fn get_cortex_home() -> PathBuf {
36-
get_cortex_home().unwrap_or_else(|| {
36+
cortex_common::get_cortex_home().unwrap_or_else(|| {
3737
dirs::home_dir()
3838
.map(|h| h.join(".cortex"))
3939
.unwrap_or_else(|| PathBuf::from(".cortex"))

cortex-cli/src/main.rs

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,11 @@ enum LoginSubcommand {
347347
struct LogoutCommand {
348348
#[clap(skip)]
349349
config_overrides: CliConfigOverrides,
350+
351+
/// Skip confirmation prompt and log out immediately.
352+
/// Useful for scripting and automation.
353+
#[arg(short = 'y', long = "yes")]
354+
yes: bool,
350355
}
351356

352357
/// Completion command.
@@ -409,6 +414,17 @@ struct ConfigCommand {
409414
}
410415

411416
/// Sandbox debug commands.
417+
///
418+
/// Run shell commands in a sandboxed environment with restricted permissions.
419+
///
420+
/// Available sandbox modes (via CORTEX_SANDBOX_MODE or --sandbox flag):
421+
/// read-only - Read-only filesystem access, no write permissions anywhere
422+
/// workspace-write - Read access everywhere + write access limited to workspace directory
423+
/// danger-full-access - Full filesystem access with no restrictions (use with caution)
424+
///
425+
/// Examples:
426+
/// cortex sandbox linux -- ls -la /tmp
427+
/// cortex sandbox macos -- cat /etc/passwd
412428
#[derive(Args)]
413429
struct SandboxArgs {
414430
#[command(subcommand)]
@@ -564,7 +580,7 @@ async fn main() -> Result<()> {
564580
Ok(())
565581
}
566582
Some(Commands::Logout(logout_cli)) => {
567-
run_logout(logout_cli.config_overrides).await;
583+
run_logout(logout_cli.config_overrides, logout_cli.yes).await;
568584
Ok(())
569585
}
570586
Some(Commands::Mcp(mcp_cli)) => mcp_cli.run().await,
@@ -673,6 +689,11 @@ async fn run_exec(exec_cli: ExecCommand) -> Result<()> {
673689
use cortex_protocol::{EventMsg, Op, Submission, UserInput};
674690
use std::io::Write;
675691

692+
// Validate prompt is not empty before any other processing (including authentication)
693+
if exec_cli.prompt.trim().is_empty() {
694+
bail!("Prompt cannot be empty");
695+
}
696+
676697
let is_json = exec_cli.format == "json" || exec_cli.format == "jsonl";
677698

678699
let mut config = cortex_engine::Config::default();
@@ -855,6 +876,57 @@ fn generate_completions(shell: Shell) {
855876
let stdout = io::stdout();
856877
let mut writer = BrokenPipeIgnorer { inner: stdout };
857878
generate(shell, &mut cmd, "cortex", &mut writer);
879+
880+
// Print dynamic completion helper for agent names
881+
// Users can regenerate completions to get this functionality
882+
let agent_completion_note = match shell {
883+
Shell::Bash => {
884+
r#"
885+
# Dynamic agent completion for --agent flag
886+
# Add this to your .bashrc after sourcing completions:
887+
_cortex_agent_completion() {
888+
local agents
889+
agents=$(cortex agent list --names-only 2>/dev/null)
890+
COMPREPLY=($(compgen -W "$agents" -- "${COMP_WORDS[COMP_CWORD]}"))
891+
}
892+
complete -F _cortex_agent_completion cortex run --agent
893+
"#
894+
}
895+
Shell::Zsh => {
896+
r#"
897+
# Dynamic agent completion for --agent flag
898+
# Add this to your .zshrc after sourcing completions:
899+
_cortex_agents() {
900+
local agents
901+
agents=(${(f)"$(cortex agent list --names-only 2>/dev/null)"})
902+
_describe 'agent' agents
903+
}
904+
compdef _cortex_agents cortex run -a --agent
905+
"#
906+
}
907+
Shell::Fish => {
908+
r#"
909+
# Dynamic agent completion for --agent flag
910+
complete -c cortex -n "__fish_seen_subcommand_from run" -l agent -xa "(cortex agent list --names-only 2>/dev/null)"
911+
"#
912+
}
913+
Shell::PowerShell => {
914+
r#"
915+
# Dynamic agent completion for --agent flag
916+
# Add this to your PowerShell profile:
917+
Register-ArgumentCompleter -CommandName cortex -ParameterName agent -ScriptBlock {
918+
param($commandName, $parameterName, $wordToComplete)
919+
cortex agent list --names-only 2>$null | Where-Object { $_ -like "$wordToComplete*" }
920+
}
921+
"#
922+
}
923+
_ => "",
924+
};
925+
926+
// Write the agent completion note as a comment
927+
if !agent_completion_note.is_empty() {
928+
let _ = writer.write_all(agent_completion_note.as_bytes());
929+
}
858930
}
859931

860932
async fn run_resume(resume_cli: ResumeCommand) -> Result<()> {
@@ -901,7 +973,7 @@ async fn run_resume(resume_cli: ResumeCommand) -> Result<()> {
901973
}
902974
} else {
903975
anyhow::bail!(
904-
"Invalid session ID: {id_str}. Expected full UUID or 8-character prefix."
976+
"Invalid session ID format: {id_str}. Expected full UUID or 8-character prefix."
905977
);
906978
}
907979
}

cortex-cli/src/pr_cmd.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,10 @@ async fn run_pr_checkout(args: PrCli) -> Result<()> {
243243
}
244244

245245
println!();
246-
println!("✅ Successfully checked out PR #{}", pr_number);
247-
println!(" Branch: {}", branch_name);
246+
println!(
247+
"✅ Checked out PR #{} to branch '{}' from '{}:{}'",
248+
pr_number, branch_name, pr_info.author, pr_info.head_branch
249+
);
248250
println!();
249251
println!("Commands:");
250252
// SECURITY: Validate base_branch before displaying in commands

cortex-cli/src/run_cmd.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -796,9 +796,15 @@ impl RunCli {
796796

797797
// Share session if requested
798798
if self.share {
799-
// TODO: Implement session sharing when API is available
800-
if self.verbose {
801-
eprintln!("Session sharing requested but not yet implemented");
799+
use cortex_share::ShareManager;
800+
let share_manager = ShareManager::new();
801+
match share_manager.share(&session_id).await {
802+
Ok(shared) => {
803+
eprintln!("✓ Session shared: {}", shared.url);
804+
}
805+
Err(e) => {
806+
eprintln!("Warning: Failed to share session: {}", e);
807+
}
802808
}
803809
}
804810

cortex-cli/src/stats_cmd.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ impl StatsCli {
131131
println!("{json_output}");
132132
} else {
133133
println!("No sessions found. Start using Cortex to generate statistics!");
134+
println!();
135+
println!("The stats command will track:");
136+
println!(" - Session counts and message totals");
137+
println!(" - Token usage (input and output tokens)");
138+
println!(" - Estimated costs by provider and model");
139+
println!(" - Tool call frequency");
134140
}
135141
return Ok(());
136142
}

0 commit comments

Comments
 (0)