@@ -298,14 +298,67 @@ struct ResumeCommand {
298298 config_overrides : CliConfigOverrides ,
299299}
300300
301- /// Sessions command.
301+ /// Sessions command with subcommands .
302302#[ derive( Args ) ]
303303struct SessionsCommand {
304+ #[ command( subcommand) ]
305+ action : Option < SessionsSubcommand > ,
306+
307+ /// Show all sessions including from other directories (for list subcommand)
308+ #[ arg( long) ]
309+ all : bool ,
310+ }
311+
312+ /// Sessions subcommands.
313+ #[ derive( Subcommand ) ]
314+ enum SessionsSubcommand {
315+ /// List all sessions (default if no subcommand given)
316+ List ( SessionsListArgs ) ,
317+
318+ /// Delete a specific session by ID
319+ #[ command( visible_alias = "rm" ) ]
320+ Delete ( SessionsDeleteArgs ) ,
321+
322+ /// Delete old sessions (garbage collection)
323+ #[ command( visible_alias = "gc" ) ]
324+ Prune ( SessionsPruneArgs ) ,
325+ }
326+
327+ /// Arguments for sessions list subcommand.
328+ #[ derive( Args ) ]
329+ struct SessionsListArgs {
304330 /// Show all sessions including from other directories
305331 #[ arg( long) ]
306332 all : bool ,
307333}
308334
335+ /// Arguments for sessions delete subcommand.
336+ #[ derive( Args ) ]
337+ struct SessionsDeleteArgs {
338+ /// Session ID to delete (first 8 characters are sufficient)
339+ session_id : String ,
340+
341+ /// Skip confirmation prompt
342+ #[ arg( short, long) ]
343+ force : bool ,
344+ }
345+
346+ /// Arguments for sessions prune subcommand.
347+ #[ derive( Args ) ]
348+ struct SessionsPruneArgs {
349+ /// Delete sessions older than this many days
350+ #[ arg( long, default_value = "30" ) ]
351+ older_than : u64 ,
352+
353+ /// Skip confirmation prompt
354+ #[ arg( short, long) ]
355+ force : bool ,
356+
357+ /// Show what would be deleted without actually deleting
358+ #[ arg( long) ]
359+ dry_run : bool ,
360+ }
361+
309362/// Config command.
310363#[ derive( Args ) ]
311364struct ConfigCommand {
@@ -478,7 +531,7 @@ async fn main() -> Result<()> {
478531 }
479532 } ,
480533 Some ( Commands :: Resume ( resume_cli) ) => run_resume ( resume_cli) . await ,
481- Some ( Commands :: Sessions ( sessions_cli) ) => list_sessions ( sessions_cli. all ) . await ,
534+ Some ( Commands :: Sessions ( sessions_cli) ) => run_sessions ( sessions_cli) . await ,
482535 Some ( Commands :: Export ( export_cli) ) => export_cli. run ( ) . await ,
483536 Some ( Commands :: Import ( import_cli) ) => import_cli. run ( ) . await ,
484537 Some ( Commands :: Config ( config_cli) ) => show_config ( config_cli) . await ,
@@ -676,6 +729,19 @@ async fn run_resume(resume_cli: ResumeCommand) -> Result<()> {
676729 Ok ( ( ) )
677730}
678731
732+ /// Handle sessions command with subcommands.
733+ async fn run_sessions ( sessions_cli : SessionsCommand ) -> Result < ( ) > {
734+ match sessions_cli. action {
735+ None => {
736+ // Default: list sessions (backwards compatible)
737+ list_sessions ( sessions_cli. all ) . await
738+ }
739+ Some ( SessionsSubcommand :: List ( args) ) => list_sessions ( args. all ) . await ,
740+ Some ( SessionsSubcommand :: Delete ( args) ) => delete_session ( args) . await ,
741+ Some ( SessionsSubcommand :: Prune ( args) ) => prune_sessions ( args) . await ,
742+ }
743+ }
744+
679745async fn list_sessions ( show_all : bool ) -> Result < ( ) > {
680746 let config = cortex_engine:: Config :: default ( ) ;
681747 let sessions = cortex_engine:: list_sessions ( & config. cortex_home ) ?;
@@ -741,6 +807,162 @@ async fn list_sessions(show_all: bool) -> Result<()> {
741807 if !show_all {
742808 println ! ( " Cortex sessions --all (show all directories)" ) ;
743809 }
810+ println ! ( "\n To delete: Cortex sessions delete <session-id>" ) ;
811+ println ! ( "To cleanup: Cortex sessions prune --older-than 30" ) ;
812+ Ok ( ( ) )
813+ }
814+
815+ /// Delete a specific session by ID.
816+ async fn delete_session ( args : SessionsDeleteArgs ) -> Result < ( ) > {
817+ use std:: io:: { BufRead , Write } ;
818+
819+ let config = cortex_engine:: Config :: default ( ) ;
820+ let sessions = cortex_engine:: list_sessions ( & config. cortex_home ) ?;
821+
822+ // Find the session (support partial ID match)
823+ let session = sessions
824+ . iter ( )
825+ . find ( |s| s. id . starts_with ( & args. session_id ) || s. id == args. session_id ) ;
826+
827+ let session = match session {
828+ Some ( s) => s,
829+ None => {
830+ eprintln ! ( "No session found matching '{}'" , args. session_id) ;
831+ eprintln ! ( "\n Use 'Cortex sessions' to list available sessions." ) ;
832+ return Ok ( ( ) ) ;
833+ }
834+ } ;
835+
836+ // Confirm deletion unless --force is used
837+ if !args. force {
838+ let date = if session. timestamp . len ( ) >= 19 {
839+ session. timestamp [ ..19 ] . replace ( 'T' , " " )
840+ } else {
841+ session. timestamp . clone ( )
842+ } ;
843+ println ! ( "Session to delete:" ) ;
844+ println ! ( " ID: {}" , session. id) ;
845+ println ! ( " Date: {}" , date) ;
846+ println ! ( " Messages: {}" , session. message_count) ;
847+ println ! ( " CWD: {}" , session. cwd. display( ) ) ;
848+ println ! ( ) ;
849+ print ! ( "Delete this session? [y/N]: " ) ;
850+ io:: stdout ( ) . flush ( ) ?;
851+
852+ let mut input = String :: new ( ) ;
853+ io:: stdin ( ) . lock ( ) . read_line ( & mut input) ?;
854+
855+ if !input. trim ( ) . eq_ignore_ascii_case ( "y" ) {
856+ println ! ( "Cancelled." ) ;
857+ return Ok ( ( ) ) ;
858+ }
859+ }
860+
861+ // Delete the session files
862+ let sessions_dir = config. cortex_home . join ( "sessions" ) ;
863+ let session_file = sessions_dir. join ( format ! ( "{}.jsonl" , session. id) ) ;
864+
865+ if session_file. exists ( ) {
866+ std:: fs:: remove_file ( & session_file) ?;
867+ println ! (
868+ "Deleted session: {}" ,
869+ & session. id[ ..8 . min( session. id. len( ) ) ]
870+ ) ;
871+ } else {
872+ eprintln ! ( "Session file not found: {}" , session_file. display( ) ) ;
873+ }
874+
875+ Ok ( ( ) )
876+ }
877+
878+ /// Prune (garbage collect) old sessions.
879+ async fn prune_sessions ( args : SessionsPruneArgs ) -> Result < ( ) > {
880+ use std:: io:: { BufRead , Write } ;
881+
882+ let config = cortex_engine:: Config :: default ( ) ;
883+ let sessions = cortex_engine:: list_sessions ( & config. cortex_home ) ?;
884+
885+ if sessions. is_empty ( ) {
886+ println ! ( "No sessions found." ) ;
887+ return Ok ( ( ) ) ;
888+ }
889+
890+ // Calculate cutoff date
891+ let cutoff = chrono:: Utc :: now ( ) - chrono:: Duration :: days ( args. older_than as i64 ) ;
892+ let cutoff_str = cutoff. to_rfc3339 ( ) ;
893+
894+ // Find sessions older than cutoff
895+ let to_delete: Vec < _ > = sessions
896+ . iter ( )
897+ . filter ( |s| s. timestamp < cutoff_str)
898+ . collect ( ) ;
899+
900+ if to_delete. is_empty ( ) {
901+ println ! ( "No sessions older than {} days found." , args. older_than) ;
902+ return Ok ( ( ) ) ;
903+ }
904+
905+ println ! (
906+ "Found {} session(s) older than {} days:" ,
907+ to_delete. len( ) ,
908+ args. older_than
909+ ) ;
910+ println ! ( "{:-<80}" , "" ) ;
911+
912+ for session in & to_delete {
913+ let date = if session. timestamp . len ( ) >= 19 {
914+ session. timestamp [ ..19 ] . replace ( 'T' , " " )
915+ } else {
916+ session. timestamp . clone ( )
917+ } ;
918+ println ! (
919+ " {} | {} | {} msgs" ,
920+ & session. id[ ..8 . min( session. id. len( ) ) ] ,
921+ date,
922+ session. message_count
923+ ) ;
924+ }
925+ println ! ( ) ;
926+
927+ if args. dry_run {
928+ println ! ( "(Dry run - no sessions were deleted)" ) ;
929+ return Ok ( ( ) ) ;
930+ }
931+
932+ // Confirm deletion unless --force is used
933+ if !args. force {
934+ print ! ( "Delete {} session(s)? [y/N]: " , to_delete. len( ) ) ;
935+ io:: stdout ( ) . flush ( ) ?;
936+
937+ let mut input = String :: new ( ) ;
938+ io:: stdin ( ) . lock ( ) . read_line ( & mut input) ?;
939+
940+ if !input. trim ( ) . eq_ignore_ascii_case ( "y" ) {
941+ println ! ( "Cancelled." ) ;
942+ return Ok ( ( ) ) ;
943+ }
944+ }
945+
946+ // Delete the sessions
947+ let sessions_dir = config. cortex_home . join ( "sessions" ) ;
948+ let mut deleted_count = 0 ;
949+
950+ for session in & to_delete {
951+ let session_file = sessions_dir. join ( format ! ( "{}.jsonl" , session. id) ) ;
952+ if session_file. exists ( ) {
953+ if let Err ( e) = std:: fs:: remove_file ( & session_file) {
954+ eprintln ! (
955+ "Failed to delete session {}: {}" ,
956+ & session. id[ ..8 . min( session. id. len( ) ) ] ,
957+ e
958+ ) ;
959+ } else {
960+ deleted_count += 1 ;
961+ }
962+ }
963+ }
964+
965+ println ! ( "Deleted {} session(s)." , deleted_count) ;
744966 Ok ( ( ) )
745967}
746968
@@ -860,13 +1082,13 @@ async fn run_serve(serve_cli: ServeCommand) -> Result<()> {
8601082 let result = cortex_app_server:: run ( config) . await ;
8611083
8621084 // Stop mDNS advertising on shutdown
863- if let Some ( ref mut mdns) = mdns_service {
864- if let Err ( e) = mdns. stop_advertising ( ) . await {
865- eprintln ! ( "Warning: Failed to stop mDNS advertising: {e}" ) ;
866- }
1085+ if let Some ( ref mut mdns) = mdns_service
1086+ && let Err ( e) = mdns. stop_advertising ( ) . await
1087+ {
1088+ eprintln ! ( "Warning: Failed to stop mDNS advertising: {e}" ) ;
8671089 }
8681090
869- result. map_err ( Into :: into )
1091+ result
8701092}
8711093
8721094async fn run_servers ( servers_cli : ServersCommand ) -> Result < ( ) > {
0 commit comments