
    `gj+N                       U d Z ddlmZ ddlZddlZddlZddlmZmZ ddlm	Z	 ddl
mZmZmZmZmZ 	 ddlmZ  ej(                  e      Zd!d	Zd!d
Zd!dZd"dZd#dZd$dZd%dZh dZ eh d      Z  eh d      Z! e"       Z#de$d<   d&dZ%d'dZ&d(d)dZ'd*dZ(d+dZ)d,dZ*	 d-	 	 	 d.dZ+d,dZ,d/dZ-dZ.dZ/d0d Z0y# e$ r ddlZd!dZY w xY w)1uN  disk_cleanup — ephemeral file cleanup for Hermes Agent.

Library module wrapping the deterministic cleanup rules written by
@LVT382009 in PR #12212. The plugin ``__init__.py`` wires these
functions into ``post_tool_call`` and ``on_session_end`` hooks so
tracking and cleanup happen automatically — the agent never needs to
call a tool or remember a skill.

Rules:
  - test files    → delete immediately at task end (age >= 0)
  - temp files    → delete after 7 days
  - cron-output   → delete after 14 days
  - empty dirs    → always delete (under HERMES_HOME)
  - research      → keep 10 newest, prompt for older (deep only)
  - chrome-profile→ prompt after 14 days (deep only)
  - >500 MB files → prompt always (deep only)

Scope: strictly HERMES_HOME and /tmp/hermes-*
Never touches: ~/.hermes/logs/ or any system directory.
    )annotationsN)datetimetimezone)Path)AnyDictListOptionalTupleget_hermes_homec                     t         j                  j                  d      xs dj                         } | rt	        |       j                         S t	        j                         dz  j                         S )NHERMES_HOME z.hermes)osenvirongetstripr   resolvehome)vals    T/root/.hermes/venv/lib/python3.12/site-packages/plugins/disk-cleanup/disk_cleanup.pyr   r   $   sP    zz~~m,299;&)tCy  "R		i0G/P/P/RR    c                     t               dz  S )u3   State dir — separate from ``$HERMES_HOME/logs/``.disk-cleanupr    r   r   get_state_dirr   0   s    ~--r   c                     t               dz  S )Nztracked.jsonr   r   r   r   get_tracked_filer    5   s    ?^++r   c                     t               dz  S )u=   Audit log — intentionally NOT under ``$HERMES_HOME/logs/``.zcleanup.logr   r   r   r   get_log_filer"   9   s    ?]**r   c                    t               }	 | j                         j                  |       y# t        t        f$ r Y nw xY w| j
                  }t        |      dk\  r|d   dk(  r|d   j                  d      ryy)zAccept only paths under HERMES_HOME or ``/tmp/hermes-*``.

    Rejects Windows mounts (``/mnt/c`` etc.) and any system directory.
    T      tmp   zhermes-F)r   r   relative_to
ValueErrorOSErrorpartslen
startswith)pathhermes_homer+   s      r   is_safe_pathr0   B   sw    
 "#K"";/   JJE
5zQ58u,q1D1DY1Os   , >>c                L   	 t               }|j                  j                  dd       t        j                  t
        j                        j                  d      }t        |dd      5 }|j                  d| d|  d	       d d d        y # 1 sw Y   y xY w# t        $ r Y y w xY w)
NTparentsexist_okz%Y-%m-%d %H:%M:%Sazutf-8)encoding[z] 
)r"   parentmkdirr   nowr   utcstrftimeopenwriter*   )messagelog_filetsfs       r   _logrD   X   s    >dT:\\(,,'001DE(C'2 	+aGGat2gYb)*	+ 	+ 	+ s0   A'B )BB BB B 	B#"B#c                    t               } | j                  j                  dd       | j                         sg S 	 t	        j
                  | j                               S # t        j                  t        f$ rs | j                  d      }|j                         rB	 t	        j
                  |j                               }t        d       |cY S # t        $ r Y nw xY wt        d       g cY S w xY w)z9Load tracked.json.  Restores from ``.bak`` on corruption.Tr2   	.json.baku3   WARN: tracked.json corrupted — restored from .baku:   WARN: tracked.json corrupted, no backup — starting fresh)r    r9   r:   existsjsonloads	read_textJSONDecodeErrorr)   with_suffixrD   	Exception)tfbakdatas      r   load_trackedrQ   h   s    		BIIOOD4O099;	zz",,.))  *- 
nn[)::<zz#--/2JK IJ	
s5   "A :C*/CC*	CC*CC*)C*c                L   t               }|j                  j                  dd       |j                  d      }|j	                  t        j                  | d             |j                         r%t        j                  ||j                  d             |j                  |       y)u1   Atomic write: ``.tmp`` → backup old → rename.Tr2   z	.json.tmpr'   )indentrF   N)r    r9   r:   rL   
write_textrH   dumpsrG   shutilcopy2replace)trackedrN   r&   s      r   save_trackedrZ      sn    		BIIOOD4O0
..
%CNN4::ga01	yy{R45KKOr   >   temptestotherdownloadresearchcron-outputchrome-profile>   
.worktreesoptional-skillscronlogscacheskillsbackupspluginscronjobsmemoriesprofilessessionsr   hermes-agent>   .git.venvsite-packagesvenv__pycache__node_moduleszset[str]_PROTECTED_CRON_PATHSc                ~   t         st               }dD ]  }||z  }t         j                  t        |             t         j                  t        |dz               t         j                  t        |dz               t         j                  t        |dz                t        | j	                               }|t         v S )uD  Return True if *p* is a cron control-plane file/directory that must
    never be deleted.

    This matches, by EXACT path only, the ``cron/`` directory itself, known
    control-plane files (``jobs.json``, ``.tick.lock``), and the ``output/``
    root directory. It does NOT (and must not be "simplified" to) blanket-match
    everything under ``cron/output/`` — those run artifacts are disposable and
    are cleaned by retention policy; only the ``output/`` root itself is
    protected, because deleting it wholesale erases every job's retained run
    history at once.
    )rd   rj   outputz	jobs.jsonz
.tick.lock)ru   r   addstrr   )pr/   r9   baseresolveds        r   _is_protected_cron_pathr}      s     !%'* 	@F'D!%%c$i0!%%c$/&:;!%%c$*<&=>!%%c$*=&>?	@ 199;H,,,r   c                D    dD ]  }| dk  r
| dd| c S | dz  }  | ddS )N)BKBMBGBTBi   z.1f z PBr   )nunits     r   fmt_sizer      sD    - t8WAdV$$	T	 WC=r   c           
        |t         vrt        d| d       d}t        |       j                         j	                         st        d d       yt              st        d d       yj                         rj                         j                  nd	}t               }t        fd
|D              ry|j                  t              t        j                  t        j                         j#                         ||d       t%        |       t        d d| dt'        |       d       |st)        d d| dt'        |       d       y)z<Register a file for tracking. Returns True if newly tracked.zWARN: unknown category 'z', using 'other'r]   zSKIP: z (does not exist)FzREJECT: z (outside HERMES_HOME)r   c              3  @   K   | ]  }|d    t              k(    yw)r.   N)ry   ).0itemr.   s     r   	<genexpr>ztrack.<locals>.<genexpr>   s     
94<3t9$
9   )r.   	timestampcategorysizez	TRACKED:  (, )z	Tracked: T)ALLOWED_CATEGORIESrD   r   r   rG   r0   is_filestatst_sizerQ   anyappendry   r   r;   r   r<   	isoformatrZ   r   print)path_strr   silentr   rY   r.   s        @r   trackr      s1   ))'z1ABC>!!#D;;=vdV,-.xv345"&,,.499;aDnG 
9
99NND	\\(,,/99;	  9TF"XJb$(8	:;	$r(2htn-=Q?@r   c                $   t        |       j                         }t               }t        |      }|D cg c]$  }t        |d         j                         |k7  s#|& }}|t        |      z
  }|rt	        |       t        d| d| d       |S c c}w )z6Remove a path from tracking without deleting the file.r.   zFORGOT: r   z	 entries))r   r   rQ   r,   rZ   rD   )r   rz   rY   beforeiremoveds         r   forgetr      s    X AnG\F!DQT!F)_%<%<%>!%CqDGDs7|#GWxs"WIY/0N Es   $BBc                    t               } t        j                  t        j                        }g }g }| D ]  }t        |d         }|j                         s#|t        j                  |d         z
  j                  }|d   }|d   }|dk(  rt        |      }	|	dk7  rh|dk(  r|j                  |       |dk(  r|dkD  r|j                  |       |dk(  r|d	kD  r|j                  |       |d
k(  r|dkD  r|j                  |       |dk(  r|d	kD  r|j                  |       |dkD  s|j                  |        ||fS )zDReturn (auto_delete_list, needs_prompt_list) without touching files.r.   r   r   r   r`   r\   r[         r_      ra     @)rQ   r   r;   r   r<   r   rG   fromisoformatdaysguess_categoryr   )
rY   r;   autopromptr   rz   agecatr   re_cats
             r   dry_runr      s>   nG
,,x||
$CDF  fxxzX++D,=>>DD:F| -#A&F& &=KKF]sQwKKM!cBhKKJ38MM$$$rMM$%%MM$7 : <r   c                    t               } t        j                  t        j                        }d}d}g }g }| D ]H  }t        |d         }|d   }|j                         st        d| d       7|t        j                  |d         z
  j                  }	|dk(  r#t        |      }
|
dk7  rt        d| d	|
d
       t        |      rt        d|        |dk(  xs |dk(  xr |	dkD  xs |dk(  xr |	dkD  }|rw	 |j                         r|j                          n%|j                         rt        j                   |       ||d   z  }|dz  }t        d| d| dt#        |d          d
       8|j'                  |       K t)               }d}g }	 |j+                         D ]]  }|j                         s|j-                         r%|j.                  t0        vs8|j.                  t2        vsK|j'                  |df       _ 	 |r|j5                         \  }}|r?	 t7        |j+                               s$|j9                          |dz  }t        d| d       V|j'                  |df       	 |j+                         D ]J  }|j                         s|j-                         r%|j.                  t2        vs8|j'                  |df       L 	 |rt;        |       t        d| d| dt#        |              ||||dS # t$        $ rC}t        d| d|        |j'                  | d|        |j'                  |       Y d}~d}~ww xY w# t$        $ r g }Y Ww xY w# t$        $ r Y w xY w# t$        $ r Y w xY w)u   Safe deterministic cleanup — no prompts.

    Returns: ``{"deleted": N, "empty_dirs": N, "freed": bytes,
               "errors": [str, ...]}``.
    r   r.   r   zSTALE: z (removed from tracking)r   r`   zSKIP stale cron-output entry: z (re-classified as r   zSKIP protected cron path: r\   r[   r   r   r   r%   	DELETED: r   r   ERROR deleting : NFz (empty dir)TzQUICK_SUMMARY: z files, z dirs, )deleted
empty_dirsfreederrors)rQ   r   r;   r   r<   r   rG   rD   r   r   r   r}   r   unlinkis_dirrV   rmtreer   r*   r   r   iterdir
is_symlinkname_EMPTY_DIR_PROTECTED_TOP_LEVEL_EMPTY_DIR_SWEEP_PRUNE_DIRSpopr   rmdirrZ   )rY   r;   r   r   new_trackedr   r   rz   r   r   r   should_deleteer/   empty_removedsweep_stacktopdirpathvisitedchilds                       r   quickr   ,  s    nG
,,x||
$CGE KF 4%f:xxz71#567X++D,=>>DD -#A&F&4QC 8))/
!5
  #1%-aS12 6M 3v)#'3}$1r 	 )99;HHJXXZMM!$f%1y2cU"Xd6l-C,DAFG t$i4%t "#KM+-K
&&( 	1C

(HH$BBHH$??""C<0	1 &??,7??,-MMO!Q&M9WI\:; GT?+		 * 7LLN!,,.

*EE&&u~67 0 
'(=/E?
	
 #	 o  )qcA3/02aSk*""4(().      		sy   :A4K>#M 7M M M .M =M .#M/ M/ #M/ 6M/ >	M
8MM
MM	M,+M,/	M;:M;c                R   t               }| |dddS t               }t        j                  t        j
                        }g g g }}}|D ]  }t        |d         }|j                         s"|t        j                  |d         z
  j                  }	|d   }
|
dk(  r|	dkD  r|j                  |       h|
d	k(  r|	d
kD  r|j                  |       |d   dkD  s|j                  |        |j                  d d       |dd }d\  }}g }|||fD ]  }|D ]  } | |      s	 t        |d         }|j                         r|j                          n%|j                         rt        j                   |       |j                  |       ||d   z  }|dz  }t#        d| d|d    dt%        |d          d         |r3|D ch c]  }|d   	 }}t)        |D cg c]  }|d   |vs| c}       |||dS # t&        $ r}t#        d|d    d|        Y d}~d}~ww xY wc c}w c c}w )a3  Deep cleanup.

    Runs :func:`quick` first, then asks the *confirm* callable for each
    risky item (research > 30d beyond 10 newest, chrome-profile > 14d,
    any file > 500 MB).  *confirm(item)* must return True to delete.

    Returns: ``{"quick": {...}, "deep_deleted": N, "deep_freed": bytes}``.
    Nr   )r   deep_deleted
deep_freedr.   r   r   r_   r   ra   r   r   r   c                    | d   S )Nr   r   xs    r   <lambda>zdeep.<locals>.<lambda>  s
    + r   Tkeyreverse
   )r   r   r%   r   r   r   r   r   r   )r   rQ   r   r;   r   r<   r   rG   r   r   r   sortr   r   r   rV   r   rD   r   r*   rZ   )confirmquick_resultrY   r;   r_   chromelarger   rz   r   r   old_researchr   count	to_removegroupr   r   remove_pathss                      r   deepr     sQ    7L%qJJnG
,,x||
$C "befH fxxzX++D,=>>DD:*rOOD!$$rMM$&\--LL MM.M=BC=LLE5I. @ 	@Dt}@T&\*Ayy{
a($$T*T&\)EQJE#A3bj)9(:"#DL12!5	@@& +45a&	55JAAfI\,IaJK!5NN  @?4<.1#>??@ 6Js+   BG4?HH$#H$4	H=HHc                 f   t               } i }| D ]?  }|d   }|j                  |ddd       ||   dxx   dz  cc<   ||   dxx   |d   z  cc<   A | D cg c],  }t        |d         j                         r|d   |d   |d   f. }}|j	                  d d	
       ||dd t        |       dS c c}w )z?Return per-category breakdown and top 10 largest tracked files.r   r   )r   r   r   r%   r   r.   c                    | d   S )Nr%   r   r   s    r   r   zstatus.<locals>.<lambda>   s
    ! r   Tr   Nr   )
categoriestop10total_tracked)rQ   
setdefaultr   rG   r   r,   )rY   catsr   cr   existings         r   statusr     s    nGD (Q23QAQ4<'	( D6O224 
6AfIq}-H  MMndM3 #2W s   1B.c                
   ddddddddd	g}| d
   }t        |j                         d d      D ]0  \  }}|j                  |dd|d   ddt        |d         d       2 |s|j                  d       |j                  d       |j                  d       | d   s|j                  d       nCt	        | d   d      D ]1  \  }\  }}}|j                  d|ddt        |      dd| d|        3 dj                  |      S )z8Human-readable status string (for slash command output).Categoryz<20r   Filesz>6z  Sizez>10z(----------------------------------------r   c                    | d   d   S )Nr%   r   r   r   s    r   r   zformat_status.<locals>.<lambda>  s    QqT&\ r   Tr   r   r   z(nothing tracked yet)r   zTop 10 largest tracked files:r   z  (none)r%   z>2z. z>8z  [z]  r8   )sorteditemsr   r   	enumeratejoin)slinesr   r   drankr.   r   s           r   format_statusr   	  s&   3qBvcl;XFE\?D+A4P NQCy!G*R8AfI3Fs2KLMN ,-	LL	LL01W:Z '07Q'? 	O#D#4sLL2d2Yb$(;3se3tfMN	O99Ur   )test_tmp_)z.test.pyz.test.jsz.test.tsz.test.mdc                   t        |       syt               }	 | j                         j                  |      }|j                  r|j                  d   nd}|dv ry|dk(  s|dk(  r,t        |j                        dk\  r|j                  d   d	k(  ry
y|dk(  ry	 | j                  j                  t              ryt        fdt        D              ryy# t        $ r Y Gw xY w)zReturn a category label for *path*, or None if we shouldn't track it.

    Used by the ``post_tool_call`` hook to auto-track ephemeral files.
    Nr   r   >   .envSOUL.mdUSER.md	MEMORY.md	auth.jsonconfig.yamlre   rg   ri   rk   rm   r   rn   rd   rj   r$   r%   rw   r`   rf   r[   r\   c              3  @   K   | ]  }j                  |        y wN)endswith)r   sfxr   s     r   r   z!guess_category.<locals>.<genexpr>J  s     
8#4==
8r   )r0   r   r   r(   r+   r,   r)   r   r-   _TEST_PATTERNSr   _TEST_SUFFIXES)r.   r/   relr   r   s       @r   r   r   %  s    
  "#Klln((5!iiciilR 
 

 &=C:- 399~"syy|x'?$'>  99D~&

8
88  s   A C 4C C 	CC)returnr   )r.   r   r  bool)r@   ry   r  None)r  List[Dict[str, Any]])rY   r  r  r  )rz   r   r  r  )r   floatr  ry   )F)r   ry   r   ry   r   r  r  r  )r   ry   r  int)r  zTuple[List[Dict], List[Dict]])r  Dict[str, Any]r  )r   zOptional[callable]r  r  )r   r  r  ry   )r.   r   r  zOptional[str])1__doc__
__future__r   rH   loggingrV   r   r   pathlibr   typingr   r   r	   r
   r   hermes_constantsr   rM   r   	getLogger__name__loggerr   r    r"   r0   rD   rQ   rZ   r   	frozensetr   r   setru   __annotations__r}   r   r   r   r   r   r   r   r   r	  r
  r   r   r   r   <module>r     s  * #    '  3 3S0 
		8	$.
,+,	 . 
 "+ , "  ( )   #&% x '-4!H
"%Xx@ #'@O@O@ON.0 #A'I  SSSs   B> >CC