Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion cortex-cli/src/debug_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1999,6 +1999,19 @@ struct WaitResult {
}

async fn run_wait(args: WaitArgs) -> Result<()> {
// Validate that at least one condition is specified (#2949)
// This check is done upfront to provide a clear error message
if !args.lsp_ready && !args.server_ready && args.port.is_none() {
bail!(
"Error: At least one wait condition required.\n\n\
Use one of the following:\n \
--lsp-ready Wait for LSP server to be available\n \
--server-ready Wait for HTTP server at --server-url\n \
--port <PORT> Wait for TCP port to be open\n\n\
Example: cortex debug wait --server-ready --timeout 30"
);
}

let start = std::time::Instant::now();
let timeout = Duration::from_secs(args.timeout);

Expand Down Expand Up @@ -2091,7 +2104,8 @@ async fn run_wait(args: WaitArgs) -> Result<()> {

(condition, success, error)
} else {
bail!("No condition specified. Use --lsp-ready, --server-ready, or --port");
// This should be unreachable due to the early validation (#2949)
unreachable!("No condition specified - validation should have caught this");
};

let waited_ms = start.elapsed().as_millis() as u64;
Expand Down
69 changes: 69 additions & 0 deletions cortex-cli/src/github_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ pub enum GitHubSubcommand {

/// Update the Cortex GitHub workflow to the latest version.
Update(UpdateArgs),

/// List supported GitHub event types (#2948).
Events(EventsArgs),
}

/// Arguments for install command.
Expand Down Expand Up @@ -142,6 +145,14 @@ pub struct UpdateArgs {
pub issue_automation: bool,
}

/// Arguments for events command (#2948).
#[derive(Debug, Parser)]
pub struct EventsArgs {
/// Output as JSON.
#[arg(long)]
pub json: bool,
}

impl GitHubCli {
/// Run the GitHub command.
pub async fn run(self) -> Result<()> {
Expand All @@ -151,10 +162,68 @@ impl GitHubCli {
GitHubSubcommand::Status(args) => run_status(args).await,
GitHubSubcommand::Uninstall(args) => run_uninstall(args).await,
GitHubSubcommand::Update(args) => run_update(args).await,
GitHubSubcommand::Events(args) => run_events(args).await,
}
}
}

/// List supported GitHub event types (#2948).
async fn run_events(args: EventsArgs) -> Result<()> {
let events = vec![
(
"issue_comment",
"Triggered when an issue comment is created, edited, or deleted",
"Comment on issues or PRs to invoke Cortex commands",
),
(
"pull_request",
"Triggered on pull request activity (opened, closed, synchronize, etc.)",
"Auto-review PRs, welcome contributors, suggest improvements",
),
(
"pull_request_review",
"Triggered when a PR review is submitted",
"Respond to review comments, update code based on feedback",
),
(
"issues",
"Triggered when an issue is opened, edited, labeled, etc.",
"Auto-triage issues, provide initial responses",
),
];

if args.json {
let json_events: Vec<_> = events
.iter()
.map(|(name, description, use_case)| {
serde_json::json!({
"name": name,
"description": description,
"use_case": use_case,
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&json_events)?);
} else {
println!("Supported GitHub Event Types");
println!("{}", "=".repeat(60));
println!();
for (name, description, use_case) in &events {
println!("\x1b[1;32m{}\x1b[0m", name);
println!(" Description: {}", description);
println!(" Use case: {}", use_case);
println!();
}
println!("Usage: cortex github run --event <EVENT_TYPE> --token <TOKEN> ...");
println!();
println!(
"For more details, see: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows"
);
}

Ok(())
}

/// Install GitHub Actions workflow.
async fn run_install(args: InstallArgs) -> Result<()> {
use cortex_engine::github::{WorkflowConfig, generate_workflow};
Expand Down
201 changes: 185 additions & 16 deletions cortex-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,28 @@ struct CompletionCommand {
/// (e.g., ~/.bashrc, ~/.zshrc) to enable tab completion.
#[arg(long = "install")]
install: bool,

/// Subcommand for completion management (#2933)
#[command(subcommand)]
action: Option<CompletionSubcommand>,
}

/// Completion subcommands (#2933).
#[derive(Subcommand)]
enum CompletionSubcommand {
/// Install completions automatically for your shell.
/// Detects your shell from $SHELL environment variable if not specified.
/// Installs to the appropriate config file (e.g., ~/.bashrc, ~/.zshrc).
Install(CompletionInstallArgs),
}

/// Arguments for completion install subcommand (#2933).
#[derive(Args)]
struct CompletionInstallArgs {
/// Shell to install completions for.
/// If not specified, attempts to detect from $SHELL environment variable.
#[arg(value_enum)]
shell: Option<Shell>,
}

/// Init command - initialize AGENTS.md.
Expand Down Expand Up @@ -653,13 +675,40 @@ struct SessionsCommand {
#[arg(long, short)]
search: Option<String>,

/// Filter sessions by tag (#2936)
/// Can be specified multiple times to filter by multiple tags (OR logic).
#[arg(long = "tag", action = clap::ArgAction::Append, value_name = "TAG")]
tags: Vec<String>,

/// Maximum number of sessions to show
#[arg(long, short)]
limit: Option<usize>,

/// Output in JSON format
#[arg(long)]
json: bool,

/// Subcommand for session management (#2937)
#[command(subcommand)]
action: Option<SessionsSubcommand>,
}

/// Sessions subcommands (#2937).
#[derive(Subcommand)]
enum SessionsSubcommand {
/// Delete a session by ID.
Delete(SessionDeleteArgs),
}

/// Arguments for session delete command (#2937).
#[derive(Args)]
struct SessionDeleteArgs {
/// Session ID to delete (full UUID or 8-character prefix).
session_id: String,

/// Skip confirmation prompt.
#[arg(short = 'f', long = "force")]
force: bool,
}

/// Config command.
Expand Down Expand Up @@ -741,12 +790,27 @@ enum FeaturesSubcommand {
}

/// Serve command - runs HTTP API server.
///
/// The server exposes a health check endpoint for monitoring and container orchestration:
/// - GET /health - Returns 200 OK with JSON body {"status": "ok"} when healthy (#2945)
///
/// This endpoint can be used for:
/// - Kubernetes liveness/readiness probes
/// - Load balancer health checks
/// - Docker HEALTHCHECK instructions
/// - Monitoring system integration
#[derive(Args)]
struct ServeCommand {
/// Port to listen on
#[arg(short, long, default_value = "3000")]
port: u16,

/// Custom path for health check endpoint (#2945).
/// The health endpoint returns {"status": "ok"} with HTTP 200 when the server is healthy.
/// Useful for container orchestration and load balancers.
#[arg(long = "health-check-path", default_value = "/health")]
health_check_path: String,

/// Host address to bind the server to.
/// Examples:
/// 127.0.0.1 - Local only (default, most secure)
Expand Down Expand Up @@ -993,12 +1057,23 @@ async fn main() -> Result<()> {
);
}
Some(Commands::Completion(completion_cli)) => {
let shell = completion_cli.shell.unwrap_or_else(detect_shell_from_env);
if completion_cli.install {
install_completions(shell)
// Handle subcommand first (#2933)
if let Some(action) = completion_cli.action {
match action {
CompletionSubcommand::Install(args) => {
let shell = args.shell.unwrap_or_else(detect_shell_from_env);
install_completions(shell)
}
}
} else {
generate_completions(shell);
Ok(())
// Original behavior: generate or install based on flag
let shell = completion_cli.shell.unwrap_or_else(detect_shell_from_env);
if completion_cli.install {
install_completions(shell)
} else {
generate_completions(shell);
Ok(())
}
}
}
Some(Commands::Sandbox(sandbox_args)) => match sandbox_args.cmd {
Expand All @@ -1014,17 +1089,27 @@ async fn main() -> Result<()> {
},
Some(Commands::Resume(resume_cli)) => run_resume(resume_cli).await,
Some(Commands::Sessions(sessions_cli)) => {
list_sessions(
sessions_cli.all,
sessions_cli.days,
sessions_cli.since.as_deref(),
sessions_cli.until.as_deref(),
sessions_cli.favorites,
sessions_cli.search.as_deref(),
sessions_cli.limit,
sessions_cli.json,
)
.await
// Handle subcommand first (#2937)
if let Some(action) = sessions_cli.action {
match action {
SessionsSubcommand::Delete(args) => {
delete_session(args.session_id, args.force).await
}
}
} else {
list_sessions(
sessions_cli.all,
sessions_cli.days,
sessions_cli.since.as_deref(),
sessions_cli.until.as_deref(),
sessions_cli.favorites,
sessions_cli.search.as_deref(),
&sessions_cli.tags,
sessions_cli.limit,
sessions_cli.json,
)
.await
}
}
Some(Commands::Export(export_cli)) => export_cli.run().await,
Some(Commands::Import(import_cli)) => import_cli.run().await,
Expand Down Expand Up @@ -1538,13 +1623,87 @@ fn filter_sessions_by_date(
.collect())
}

/// Delete a session by ID (#2937).
async fn delete_session(session_id: String, force: bool) -> Result<()> {
use cortex_engine::rollout::get_rollout_path;
use cortex_protocol::ConversationId;
use std::io::{self, Write};

let config = cortex_engine::Config::default();

// Resolve session ID (supports full UUID or 8-char prefix)
let conversation_id: ConversationId = match session_id.parse() {
Ok(id) => id,
Err(_) => {
if session_id.len() == 8 {
let sessions = cortex_engine::list_sessions(&config.cortex_home)?;
let matching: Vec<_> = sessions
.iter()
.filter(|s| s.id.starts_with(&session_id))
.collect();

match matching.len() {
0 => bail!("No session found with ID prefix: {session_id}"),
1 => matching[0].id.parse().map_err(|_| {
anyhow::anyhow!("Internal error: invalid session ID format")
})?,
_ => bail!(
"Ambiguous session ID prefix '{}' matches {} sessions. Please provide more characters.",
session_id,
matching.len()
),
}
} else {
bail!(
"Invalid session ID: {session_id}. Expected full UUID or 8-character prefix."
);
}
}
};

let rollout_path = get_rollout_path(&config.cortex_home, &conversation_id);

if !rollout_path.exists() {
bail!("Session not found: {session_id}");
}

// Confirm deletion unless --force
if !force {
print!(
"Delete session {}? This cannot be undone. [y/N]: ",
&conversation_id.to_string()[..8]
);
io::stdout().flush()?;

let mut input = String::new();
io::stdin().read_line(&mut input)?;

if !input.trim().eq_ignore_ascii_case("y") {
println!("Cancelled.");
return Ok(());
}
}

// Delete the session rollout file
std::fs::remove_file(&rollout_path)
.with_context(|| format!("Failed to delete session: {}", rollout_path.display()))?;

print_success(&format!(
"Session {} deleted successfully",
&conversation_id.to_string()[..8]
));

Ok(())
}

async fn list_sessions(
show_all: bool,
days: Option<u32>,
since: Option<&str>,
until: Option<&str>,
favorites_only: bool,
search: Option<&str>,
tags: &[String],
limit: Option<usize>,
json_output: bool,
) -> Result<()> {
Expand Down Expand Up @@ -1597,6 +1756,16 @@ async fn list_sessions(
print_info("The --favorites filter requires session metadata from cortex-storage.");
}

// Note: tags filter requires session metadata storage (#2936)
// For now, log a message if tags are specified but filtering isn't implemented
if !tags.is_empty() {
// TODO: Implement tag-based filtering when session tags are stored
print_info(&format!(
"Tag filtering requested ({}). Tag storage is pending implementation.",
tags.join(", ")
));
}

if sessions.is_empty() {
if json_output {
println!("[]");
Expand Down
Loading