66from typing import Dict , List , Optional
77
88from cecli import models
9+ from cecli import session_crypto
910from cecli .helpers .conversation import ConversationService , MessageTag
1011
1112
@@ -22,6 +23,67 @@ def _get_session_directory(self) -> Path:
2223 os .makedirs (session_dir , exist_ok = True )
2324 return session_dir
2425
26+ def _session_encrypt_settings (self ) -> tuple [bool , bytes | None ]:
27+ args = getattr (self .coder , "args" , None )
28+ if not args or not getattr (args , "session_encrypt" , False ):
29+ return False , None
30+ key_file = getattr (args , "session_key_file" , None )
31+ return True , session_crypto .resolve_key (key_file = key_file )
32+
33+ def _read_session_file (self , session_file : Path ) -> dict | None :
34+ try :
35+ data = session_file .read_bytes ()
36+ except OSError as e :
37+ self .io .tool_error (f"Error reading session: { e } " )
38+ return None
39+ try :
40+ if session_crypto .is_encrypted_payload (data ):
41+ args = getattr (self .coder , "args" , None )
42+ key_file = getattr (args , "session_key_file" , None ) if args else None
43+ key = session_crypto .resolve_key (key_file = key_file )
44+ if not key :
45+ self .io .tool_error (
46+ "Session is encrypted but no key is configured "
47+ f"({ session_crypto .KEY_ENV } or --session-key-file)."
48+ )
49+ return None
50+ return session_crypto .decrypt_session_bytes (data , key )
51+ parsed = json .loads (data .decode ("utf-8" ))
52+ if not isinstance (parsed , dict ):
53+ self .io .tool_error ("Invalid session format." )
54+ return None
55+ return parsed
56+ except session_crypto .SessionCryptoError as e :
57+ self .io .tool_error (str (e ))
58+ return None
59+ except json .JSONDecodeError as e :
60+ self .io .tool_error (f"Error loading session: { e } " )
61+ return None
62+
63+ def _write_session_file (self , session_file : Path , session_data : dict ) -> bool :
64+ encrypt_enabled , key = self ._session_encrypt_settings ()
65+ try :
66+ if encrypt_enabled :
67+ if not key :
68+ self .io .tool_error (
69+ "Session encryption is enabled but no key is configured "
70+ f"({ session_crypto .KEY_ENV } or --session-key-file)."
71+ )
72+ return False
73+ session_file .write_bytes (
74+ session_crypto .encrypt_session_dict (session_data , key )
75+ )
76+ else :
77+ with open (session_file , "w" , encoding = "utf-8" ) as f :
78+ json .dump (session_data , f , indent = 2 )
79+ return True
80+ except session_crypto .SessionCryptoError as e :
81+ self .io .tool_error (str (e ))
82+ return False
83+ except OSError as e :
84+ self .io .tool_error (f"Error saving session: { e } " )
85+ return False
86+
2587 def save_session (self , session_name : str , output = True ) -> bool :
2688 """Save the current chat session to a named file."""
2789 if not session_name :
@@ -39,11 +101,12 @@ def save_session(self, session_name: str, output=True) -> bool:
39101
40102 try :
41103 session_data = self ._build_session_data (session_name )
42- with open (session_file , "w" , encoding = "utf-8" ) as f :
43- json . dump ( session_data , f , indent = 2 )
104+ if not self . _write_session_file (session_file , session_data ) :
105+ return False
44106
45107 if output :
46- self .io .tool_output (f"Session saved: { session_file } " )
108+ suffix = " (encrypted)" if self ._session_encrypt_settings ()[0 ] else ""
109+ self .io .tool_output (f"Session saved: { session_file } { suffix } " )
47110
48111 return True
49112
@@ -63,8 +126,27 @@ def list_sessions(self) -> List[Dict]:
63126 sessions = []
64127 for session_file in sorted (session_files , key = lambda x : x .stat ().st_mtime , reverse = True ):
65128 try :
66- with open (session_file , "r" , encoding = "utf-8" ) as f :
67- session_data = json .load (f )
129+ raw = session_file .read_bytes ()
130+ if session_crypto .is_encrypted_payload (raw ):
131+ _ , key = self ._session_encrypt_settings ()
132+ if not key :
133+ sessions .append (
134+ {
135+ "name" : session_file .stem ,
136+ "file" : session_file ,
137+ "model" : "encrypted" ,
138+ "edit_format" : "—" ,
139+ "num_messages" : 0 ,
140+ "num_files" : 0 ,
141+ "encrypted" : True ,
142+ }
143+ )
144+ continue
145+ session_data = session_crypto .decrypt_session_bytes (raw , key )
146+ else :
147+ session_data = json .loads (raw .decode ("utf-8" ))
148+ if not isinstance (session_data , dict ):
149+ raise ValueError ("not a session object" )
68150
69151 session_info = {
70152 "name" : session_file .stem ,
@@ -80,6 +162,7 @@ def list_sessions(self) -> List[Dict]:
80162 + len (session_data .get ("files" , {}).get ("read_only" , []))
81163 + len (session_data .get ("files" , {}).get ("read_only_stubs" , []))
82164 ),
165+ "encrypted" : session_crypto .is_encrypted_payload (raw ),
83166 }
84167 sessions .append (session_info )
85168
@@ -99,15 +182,12 @@ async def load_session(self, session_identifier: str, switch=True) -> bool:
99182 if not session_file :
100183 return False
101184
102- try :
103- with open (session_file , "r" , encoding = "utf-8" ) as f :
104- session_data = json .load (f )
105- except Exception as e :
106- self .io .tool_error (f"Error loading session: { e } " )
185+ session_data = self ._read_session_file (session_file )
186+ if session_data is None :
107187 return False
108188
109189 # Verify session format
110- if not isinstance ( session_data , dict ) or "version" not in session_data :
190+ if "version" not in session_data :
111191 self .io .tool_error ("Invalid session format." )
112192 return False
113193
0 commit comments