1515from tempfile import TemporaryDirectory
1616from typing import Any , Literal
1717
18- from .anonymize import TraceAnonymizer , anonymize_file
18+ from .anonymize import AnonymizeFileReport , AnonymizeProgress , TraceAnonymizer , anonymize_file
1919from .tool_schema import CURSOR_BUILTIN_TOOLS
2020
2121ExtractProvider = Literal ["claude" , "codex" , "cursor" , "hermes" , "pi" ]
@@ -50,22 +50,32 @@ def count(self) -> int:
5050class _InlineAnonymizer :
5151 """Anonymize traces as they are written, one TraceAnonymizer per file."""
5252
53- def __init__ (self ) -> None :
53+ def __init__ (self , progress : AnonymizeProgress | None = None ) -> None :
5454 self .totals : dict [str , int ] = {"email" : 0 , "username" : 0 , "api_key" : 0 }
55+ self .progress = progress
56+ self .files_done = 0
5557
5658 def _add (self , counts : dict [str , int ]) -> None :
5759 for key , count in counts .items ():
5860 self .totals [key ] = self .totals .get (key , 0 ) + count
5961
60- def copy_file (self , source : Path , destination : Path ) -> None :
61- report = anonymize_file (source , destination )
62+ def _record (self , report : AnonymizeFileReport ) -> None :
6263 self ._add (report .replacements )
64+ self .files_done += 1
65+ if self .progress is not None :
66+ self .progress (report , self .files_done , None )
67+
68+ def copy_file (self , source : Path , destination : Path ) -> None :
69+ self ._record (anonymize_file (source , destination ))
6370
64- def anonymize_events (self , events : list [dict [str , Any ]]) -> list [dict [str , Any ]]:
71+ def anonymize_events (self , events : list [dict [str , Any ]]) -> tuple [ list [dict [str , Any ]], dict [ str , int ]]:
6572 anonymizer = TraceAnonymizer ()
6673 events = [anonymizer .anonymize_value (event ) for event in events ]
67- self ._add (anonymizer .counts )
68- return events
74+ replacements = {key : value for key , value in anonymizer .counts .items () if value }
75+ return events , replacements
76+
77+ def record_events_file (self , path : Path , replacements : dict [str , int ]) -> None :
78+ self ._record (AnonymizeFileReport (path = path , output_path = path , replacements = replacements ))
6979
7080 def anonymize_remaining_files (self , root : Path , excluded : Iterable [Path ]) -> None :
7181 """Scrub pre-existing output files without re-reading newly written traces."""
@@ -125,6 +135,7 @@ def extract_local_sessions(
125135 clear_destination : bool = False ,
126136 progress : ProgressCallback | None = None ,
127137 anonymize : bool = False ,
138+ anonymize_progress : AnonymizeProgress | None = None ,
128139) -> ExtractResult :
129140 """Extract local sessions for provider into output_dir."""
130141 source_candidates = list (sources ) if sources is not None else default_session_sources (provider , home )
@@ -133,7 +144,7 @@ def extract_local_sessions(
133144 destination_dir .mkdir (parents = True , exist_ok = True )
134145 if clear_destination :
135146 _clear_extract_destination (destination_dir )
136- anonymizer = _InlineAnonymizer () if anonymize else None
147+ anonymizer = _InlineAnonymizer (anonymize_progress ) if anonymize else None
137148 if provider == "hermes" :
138149 copied_files = _extract_hermes_state_dbs (
139150 resolved_sources , destination_dir , model_filter = model_filter , anonymizer = anonymizer
@@ -574,14 +585,17 @@ def _write_jsonl_dict_events(
574585 events : list [dict [str , Any ]],
575586 anonymizer : _InlineAnonymizer | None = None ,
576587) -> None :
588+ replacements : dict [str , int ] = {}
577589 if anonymizer is not None :
578- events = anonymizer .anonymize_events (events )
590+ events , replacements = anonymizer .anonymize_events (events )
579591 path .parent .mkdir (parents = True , exist_ok = True )
580592 with path .open ("w" , encoding = "utf-8" ) as handle :
581593 for event in events :
582594 line = json .dumps (event , ensure_ascii = False , separators = ("," , ":" ))
583595 line = line .replace ("\u0085 " , "\\ u0085" ).replace ("\u2028 " , "\\ u2028" ).replace ("\u2029 " , "\\ u2029" )
584596 handle .write (line + "\n " )
597+ if anonymizer is not None :
598+ anonymizer .record_events_file (path , replacements )
585599
586600
587601def _write_individual_jsonl_rows (
0 commit comments