Skip to content

Commit 1368545

Browse files
committed
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
1 parent ea87c48 commit 1368545

10 files changed

Lines changed: 150 additions & 9 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: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1613,6 +1613,14 @@ pub struct WaitArgs {
16131613
#[arg(long, default_value = "http://127.0.0.1:3000")]
16141614
pub server_url: String,
16151615

1616+
/// Wait for a TCP port to be available.
1617+
#[arg(long)]
1618+
pub port: Option<u16>,
1619+
1620+
/// Host to check when using --port (default: 127.0.0.1).
1621+
#[arg(long, default_value = "127.0.0.1")]
1622+
pub host: String,
1623+
16161624
/// Timeout in seconds.
16171625
#[arg(long, default_value = "30")]
16181626
pub timeout: u64,
@@ -1698,9 +1706,38 @@ async fn run_wait(args: WaitArgs) -> Result<()> {
16981706
error = Some(format!("Timeout waiting for server at {}", args.server_url));
16991707
}
17001708

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

17061743
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
@@ -149,7 +149,7 @@ impl ExportCommand {
149149
// Parse conversation ID
150150
let conversation_id: ConversationId = session_id.parse().map_err(|_| {
151151
anyhow::anyhow!(
152-
"Invalid session ID: {session_id}. Expected full UUID or 8-character prefix."
152+
"Invalid session ID format: {session_id}. Expected full UUID or 8-character prefix."
153153
)
154154
})?;
155155

cortex-cli/src/login.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ pub fn read_api_key_from_stdin() -> String {
201201
if stdin.is_terminal() {
202202
eprintln!(
203203
"--with-api-key expects the API key on stdin. Try piping it, e.g. \
204-
`printenv OPENAI_API_KEY | fabric login --with-api-key`."
204+
`printenv OPENAI_API_KEY | cortex login --with-api-key`."
205205
);
206206
std::process::exit(1);
207207
}

cortex-cli/src/main.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,17 @@ struct ConfigCommand {
453453
}
454454

455455
/// Sandbox debug commands.
456+
///
457+
/// Run shell commands in a sandboxed environment with restricted permissions.
458+
///
459+
/// Available sandbox modes (via CORTEX_SANDBOX_MODE or --sandbox flag):
460+
/// read-only - Read-only filesystem access, no write permissions anywhere
461+
/// workspace-write - Read access everywhere + write access limited to workspace directory
462+
/// danger-full-access - Full filesystem access with no restrictions (use with caution)
463+
///
464+
/// Examples:
465+
/// cortex sandbox linux -- ls -la /tmp
466+
/// cortex sandbox macos -- cat /etc/passwd
456467
#[derive(Args)]
457468
struct SandboxArgs {
458469
#[command(subcommand)]
@@ -733,6 +744,11 @@ async fn run_exec(exec_cli: ExecCommand) -> Result<()> {
733744
use cortex_protocol::{EventMsg, Op, Submission, UserInput};
734745
use std::io::Write;
735746

747+
// Validate prompt is not empty before any other processing (including authentication)
748+
if exec_cli.prompt.trim().is_empty() {
749+
bail!("Prompt cannot be empty");
750+
}
751+
736752
let is_json = exec_cli.format == "json" || exec_cli.format == "jsonl";
737753

738754
let mut config = cortex_engine::Config::default();
@@ -913,6 +929,57 @@ fn generate_completions(shell: Shell) {
913929
let stdout = io::stdout();
914930
let mut writer = BrokenPipeIgnorer { inner: stdout };
915931
generate(shell, &mut cmd, "cortex", &mut writer);
932+
933+
// Print dynamic completion helper for agent names
934+
// Users can regenerate completions to get this functionality
935+
let agent_completion_note = match shell {
936+
Shell::Bash => {
937+
r#"
938+
# Dynamic agent completion for --agent flag
939+
# Add this to your .bashrc after sourcing completions:
940+
_cortex_agent_completion() {
941+
local agents
942+
agents=$(cortex agent list --names-only 2>/dev/null)
943+
COMPREPLY=($(compgen -W "$agents" -- "${COMP_WORDS[COMP_CWORD]}"))
944+
}
945+
complete -F _cortex_agent_completion cortex run --agent
946+
"#
947+
}
948+
Shell::Zsh => {
949+
r#"
950+
# Dynamic agent completion for --agent flag
951+
# Add this to your .zshrc after sourcing completions:
952+
_cortex_agents() {
953+
local agents
954+
agents=(${(f)"$(cortex agent list --names-only 2>/dev/null)"})
955+
_describe 'agent' agents
956+
}
957+
compdef _cortex_agents cortex run -a --agent
958+
"#
959+
}
960+
Shell::Fish => {
961+
r#"
962+
# Dynamic agent completion for --agent flag
963+
complete -c cortex -n "__fish_seen_subcommand_from run" -l agent -xa "(cortex agent list --names-only 2>/dev/null)"
964+
"#
965+
}
966+
Shell::PowerShell => {
967+
r#"
968+
# Dynamic agent completion for --agent flag
969+
# Add this to your PowerShell profile:
970+
Register-ArgumentCompleter -CommandName cortex -ParameterName agent -ScriptBlock {
971+
param($commandName, $parameterName, $wordToComplete)
972+
cortex agent list --names-only 2>$null | Where-Object { $_ -like "$wordToComplete*" }
973+
}
974+
"#
975+
}
976+
_ => "",
977+
};
978+
979+
// Write the agent completion note as a comment
980+
if !agent_completion_note.is_empty() {
981+
let _ = writer.write_all(agent_completion_note.as_bytes());
982+
}
916983
}
917984

918985
fn generate_man_page(output_dir: Option<PathBuf>) -> Result<()> {
@@ -979,7 +1046,7 @@ async fn run_resume(resume_cli: ResumeCommand) -> Result<()> {
9791046
}
9801047
} else {
9811048
anyhow::bail!(
982-
"Invalid session ID: {id_str}. Expected full UUID or 8-character prefix."
1049+
"Invalid session ID format: {id_str}. Expected full UUID or 8-character prefix."
9831050
);
9841051
}
9851052
}

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)