
    iY              
          d Z ddlZddlZddlZddlZddlZddlZddlZddl	m
Z
 ddlmZ ddlmZ ddlmZ ddlmZ dZd	Zd
ZdZdefdZdee         fdZdee         ddfdZefdee         deddfdZd9dee         de eef         fdZ!d:dZ"dZ#dZ$dedee         fdZ%dede&fdZ'efdee         defdZ(dedefdZ)dedefdZ*d;ded!edefd"Z+d;ded!edefd#Z,e
 G d$ d%                      Z-d&edee         fd'Z.d&edee         fd(Z/ed)d&ed*ed+ede-fd,Z0d-edeee-f         fd.Z1defd/Z2d0d1dd2d-ed3ed4eeee-f                  defd5Z3d6 Z4d7 Z5d8 Z6dS )<u   ``hermes debug`` — debug tools for Hermes Agent.

Currently supports:
    hermes debug share    Upload debug report (system info + logs) to a
                          paste service and print a shareable URL.
    N)	dataclass)Path)Optionalget_hermes_home)atomic_replacehttps://paste.rs/zhttps://dpaste.com/api/i  i`T  returnc                  *    t                      dz  dz  S )aZ  Path to ``~/.hermes/pastes/pending.json``.

    Each entry: ``{"url": "...", "expire_at": <unix_ts>}``.  Scheduled
    DELETEs used to be handled by spawning a detached Python process per
    paste that slept for 6 hours; those accumulated forever if the user
    ran ``hermes debug share`` repeatedly.

    Deletion is now driven by the gateway's cron ticker
    (``gateway/run.py::_start_cron_ticker``) which calls
    ``_sweep_expired_pastes`` once per hour.  ``hermes debug share`` also
    runs an opportunistic sweep on entry as a fallback for CLI-only users
    who never start the gateway.
    pasteszpending.jsonr        5/home/ubuntu/.hermes/hermes-agent/hermes_cli/debug.py_pending_filer   *   s     x'.88r   c                  (   t                      } |                                 sg S 	 t          j        |                     d                    }t          |t                    rd |D             S n"# t          t          t          j	        f$ r Y nw xY wg S )Nutf-8encodingc                 L    g | ]!}t          |t                    r
d |v d|v |"S url	expire_at)
isinstancedict.0es     r   
<listcomp>z!_load_pending.<locals>.<listcomp>C   sG       a&&+0A::+:J:J :J:J:Jr   )
r   existsjsonloads	read_textr   listOSError
ValueErrorJSONDecodeError)pathdatas     r   _load_pendingr)   ;   s    ??D;;== 		z$..'.::;;dD!! 	    	 Z!56   Is   AA0 0BBentriesc                    t                      }	 |j                            dd           |                    d          }|                    t          j        | d          d           t          ||           d S # t          $ r Y d S w xY w)NT)parentsexist_okz	.json.tmp   )indentr   r   )	r   parentmkdirwith_suffix
write_textr    dumpsr   r$   )r*   r'   tmps      r   _save_pendingr6   L   s    ??D$666{++tz'!444wGGGsD!!!!!    	s   A,A> >
BBurlsdelay_secondsc                 8   d | D             }|sdS t                      }d |D             }t          j                    |z   }|D ])}t          ||                    |d                    ||<   *d |                                D             }t          |           dS )zRecord *urls* for deletion at ``now + delay_seconds``.

    Only paste.rs URLs are recorded (dpaste.com auto-expires).  Entries
    are merged into any existing pending.json.
    c                 0    g | ]}t          |          |S r   _extract_paste_id)r   us     r   r   z#_record_pending.<locals>.<listcomp>_   s&    ===1(9!(<(<=Q===r   Nc                 F    i | ]}|d          t          |d                   S r   )floatr   s     r   
<dictcomp>z#_record_pending.<locals>.<dictcomp>e   s*    QQQA%%+*?*?QQQr   g        c                     g | ]
\  }}||d S )r   r   )r   r=   tss      r   r   z#_record_pending.<locals>.<listcomp>i   s$    FFFeaab))FFFr   )r)   timemaxgetitemsr6   )r7   r8   paste_rs_urlsr*   by_urlr   r=   mergeds           r   _record_pendingrJ   Y   s     >====M ooGQQQQQF	m+I 7 7	6::a#5#566q		FFv||~~FFFF&r   nowc                    t                      }|sdS | t          j                    n| }d}g }|D ]}	 t          |                    dd                    }n# t          t
          f$ r Y :w xY w||k    r|                    |           Z|                    dd          }	 t          |          r|dz  }n# t          $ r Y nw xY w|dz   |k    r|                    |           |dz  }|rt          |           |t          |          fS )	u9  Synchronously DELETE any pending pastes whose ``expire_at`` has passed.

    Returns ``(deleted, remaining)``.  Best-effort: failed deletes stay in
    the pending file and will be retried on the next sweep.  Silent —
    intended to be called from every ``hermes debug`` invocation with
    minimal noise.
    )r   r   Nr   r   r       iQ )r)   rC   r?   rE   	TypeErrorr%   appenddelete_paste	Exceptionr6   len)rK   r*   currentdeleted	remainingentryr   r   s           r   _sweep_expired_pastesrX   m   sg    ooG v [dikkkcGGI  	eiiQ7788II:& 	 	 	H	wU###iir""	C   1  	 	 	 D	 uw&&U####qLGG !i   S^^$$s#   #AA+*A+!B77
CCc                  F    	 t                       dS # t          $ r Y dS w xY w)zBAttempt pending-paste cleanup without letting /debug fail offline.N)rX   rR   r   r   r   !_best_effort_sweep_expired_pastesrZ      s;       s    
  u  ⚠️  This will upload the following to a public paste service:
  • System info (OS, Python version, Hermes version, provider, which API keys
    are configured — NOT the actual keys)
  • Recent log lines (agent.log, errors.log, gateway.log — may contain
    conversation fragments and file paths)
  • Full agent.log and gateway.log (up to 512 KB each — likely contains
    conversation content, tool outputs, and file paths)

Pastes auto-delete after 6 hours.
u  ⚠️ **Privacy notice:** This uploads system info + recent log tails (may contain conversation fragments) to a public paste service. Full logs are NOT included from the gateway — use `hermes debug share` from the CLI for full log uploads.
Pastes auto-delete after 6 hours.r   c                     |                                                      d          } dD ]0}|                     |          r| t          |          d         c S 1dS )zExtract the paste ID from a paste.rs or dpaste.com URL.

    Returns the ID string, or None if the URL doesn't match a known service.
    /)r	   zhttp://paste.rs/N)striprstrip
startswithrS   )r   prefixs     r   r<   r<      sg    
 ))++

S
!
!C; % %>>&!! 	%s6{{||$$$$	%4r   c                 F   t          |           }|st          d|            t           | }t          j                            |dddi          }t          j                            |d          5 }d|j        cxk    od	k     nc cd
d
d
           S # 1 swxY w Y   d
S )zDelete a paste from paste.rs.  Returns True on success.

    Only paste.rs supports unauthenticated DELETE.  dpaste.com pastes
    expire automatically but cannot be deleted via API.
    z7Cannot delete: only paste.rs URLs are supported.  Got: DELETE
User-Agenthermes-agent/debug-share)methodheaders   timeout   i,  N)r<   r%   _PASTE_RS_URLurllibrequestRequesturlopenstatus)r   paste_idtargetreqresps        r   rQ   rQ      s    !%%H 
KcKK
 
 	
 )x))F
.
 
 x9: !  C 
		R		0	0 (Ddk''''C''''( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (s   4BBBc                 (    t          | |           dS )u  Record *urls* for deletion ``delay_seconds`` from now.

    Previously this spawned a detached Python subprocess per call that slept
    for 6 hours and then issued DELETE requests.  Those subprocesses leaked —
    every ``hermes debug share`` invocation added ~20 MB of resident Python
    interpreters that never exited until the sleep completed.

    The replacement is stateless: we append to ``~/.hermes/pastes/pending.json``
    and the gateway's cron ticker sweeps expired entries once per hour.
    ``hermes debug share`` also runs an opportunistic sweep as a fallback
    for CLI-only users.  If neither runs again, paste.rs's own retention
    policy handles cleanup.
    )r8   N)rJ   )r7   r8   s     r   _schedule_auto_deleterv      s     D666666r   c                 2    t          |           }|rd|  S dS )z:Return a one-liner delete command for the given paste URL.zhermes debug delete z$(auto-expires per dpaste.com policy)r;   )r   rq   s     r   _delete_hintrx      s+     %%H ,+c+++11r   contentc                    |                      d          }t          j                            t          |dddd          }t          j                            |d          5 }|                                                    d                                          }d	d	d	           n# 1 swxY w Y   |	                    d
          st          d|d	d                    |S )zvUpload to paste.rs.  Returns the paste URL.

    paste.rs accepts a plain POST body and returns the URL directly.
    r   POSTztext/plain; charset=utf-8rd   zContent-Typerc   r(   re   rf   rg   rh   Nhttpz#Unexpected response from paste.rs: rj   )encoderl   rm   rn   rk   ro   readdecoder]   r_   r%   )ry   r(   rs   rt   r   s        r   _upload_paste_rsr      s   
 >>'""D
.
 
 D74
 
 !  C 
		R		0	0 2Diikk  ))//112 2 2 2 2 2 2 2 2 2 2 2 2 2 2>>&!! LJs4C4yJJKKKJs   ":B((B,/B,   expiry_daysc                 z   ddt           dt           dt           ffd} |d|            |dd          z    |d	t          |                    z   d
 dz                       d          }t          j                            t
          |dd dd          }t          j                            |d          5 }|                                                    d          	                                }ddd           n# 1 swxY w Y   |
                    d          st          d|dd                    |S )z\Upload to dpaste.com.  Returns the paste URL.

    dpaste.com uses multipart form data.
    z----HermesDebugBoundary9f3cnamevaluer
   c                     d d|  d| dS )N--z(
Content-Disposition: form-data; name="z"

z
r   )r   r   boundarys     r   _fieldz"_upload_dpaste_com.<locals>._field  s:      59    	
r   ry   syntaxtextr   r   z--
r   r{   zmultipart/form-data; boundary=rd   r|   r}   rg   rh   Nr~   z%Unexpected response from dpaste.com: rj   )strr   rl   rm   rn   _DPASTE_COM_URLro   r   r   r]   r_   r%   )ry   r   r   bodyrs   rt   r   r   s          @r   _upload_dpaste_comr     s   
 -H
S 
 
 
 
 
 
 
 
 	y'""
&6
"
"	#
&K 0 0
1
1	2  x


	  fWoo 	 .
 
 d6GXGG4
 
 !  C 
		R		0	0 2Diikk  ))//112 2 2 2 2 2 2 2 2 2 2 2 2 2 2>>&!! NLTcTLLMMMJs   ;:DDDc                 P   g }	 t          |           S # t          $ r"}|                    d|            Y d}~nd}~ww xY w	 t          | |          S # t          $ r"}|                    d|            Y d}~nd}~ww xY wt	          dd                    |          z             )zUpload *content* to a paste service, trying paste.rs then dpaste.com.

    Returns the paste URL on success, raises on total failure.
    z
paste.rs: Nr   zdpaste.com: z)Failed to upload to any paste service:
  z
  )r   rR   rP   r   RuntimeErrorjoin)ry   r   errorsexcs       r   upload_to_pastebinr   0  s    
 F*((( * * *(3(())))))))*,!'{CCCC , , ,*S**++++++++, 4v{{67J7JJ  s(    
?:?A 
B A;;B c                   J    e Zd ZU dZee         ed<   eed<   ee         ed<   dS )LogSnapshotz7Single-read snapshot of a log file used by debug-share.r'   	tail_text	full_textN)__name__
__module____qualname____doc__r   r   __annotations__r   r   r   r   r   r   M  sA         AA
4.NNN}r   r   log_namec                 h    ddl m} |                    |           }|rt                      dz  |z  ndS )z@Where *log_name* would live if present. Doesn't check existence.r   )	LOG_FILESlogsN)hermes_cli.logsr   rE   r   )r   r   filenames      r   _primary_log_pathr   V  sE    ))))))}}X&&H6>HO&11DHr   c                    t          |           }|dS |                                r|                                j        dk    r|S |j        |j         dz  }|                                r|                                j        dk    r|S dS )zFind the log file for *log_name*, falling back to the .1 rotation.

    Returns the first non-empty candidate (primary, then .1), or None.
    Callers distinguish 'empty primary' from 'truly missing' via
    :func:`_primary_log_path`.
    Nr   z.1)r   r   statst_sizer0   r   )r   primaryrotateds      r   _resolve_log_pathr   ^  s      ))Gt~~ GLLNN2Q66n',2222G~~ GLLNN2Q664r   )	max_bytes
tail_linesr   c                   t          |           }|;t          |           }|r|                                rdnd}t          d|d          S 	 |                                j        }|dk    rt          |dd          S t          |d          5 }||k    r|                                }d}	nd}
|}g }d}d}|dk    r||k     s	||d	z   k    r||d
z  k     rt          |
|          }||z  }|	                    |           |                    |          }|
                    d|           |t          |          z  }||                    d          z  }t          |
d
z  d          }
|dk    r||k     s	||d	z   k    r	||d
z  k     d                    |          }|dk    }	ddd           n# 1 swxY w Y   |}|	rht          |          |k    rUt          |          |z
  }|dk    o||d	z
  |         dk    }||d         }|s d|v r|                    dd	          d	         }|                    dd          }d                    |                    d          | d                                       d          }|                    dd          }|	rd|dz   d| }t          |||          S # t$          $ r }t          |d| dd          cY d}~S d}~ww xY w)a  Capture a log once and derive summary/full-log views from it.

    The report tail and standalone log upload must come from the same file
    snapshot. Otherwise a rotation/truncate between reads can make the report
    look newer than the uploaded ``agent.log`` paste.
    Nz(file empty)z(file not found))r'   r   r   r   rbFi    rN   r.      
i   r   r   replace)r   rM   T)keepends
u!   [... truncated — showing last ~i   zKB ...]
z(error reading: ))r   r   r   r   r   r   openr   minseekinsertrS   countr   splitr   
splitlinesr^   rR   )r   r   r   log_pathr   tailsizefraw	truncated
chunk_sizeposchunkstotalnewline_count	read_sizechunkfull_rawcuton_boundaryall_textr   r   r   s                           r   _capture_log_snapshotr   s  s    !**H#H--!(UW^^-=-=U~~CUEEEE6_}}&199HRVWWWW(D!! 	$Qy  ffhh!		
 "
&( !Agg59#4#4VW8W8W]benqrer]r]r #J 4 4I9$CFF3KKKFF9--EMM!U+++SZZ'E!U[[%7%77M!$Z!^U!;!;J Agg59#4#4VW8W8W]benqrer]r]r hhv&&!G	3	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$6  		7X22h--)+C
 'FhsQw}&=&FK~H 75H#4#4#>>%33A6::gi:88GGH///>>
{||LMMTTUYZZ	OOGIO>>	 	dcI<MccXaccIISSSS _ _ _4Ms4M4M4MY]^^^^^^^^^_sJ   0J5 ?J5 DF, J5 ,F00J5 3F04D J5 5
K?KKK	log_linesc                     t          | d          }t          d|           t          d|          t          d|          dS )z2Capture all logs used by debug-share exactly once.d   agent)r   r   gateway)r   r   r   )r   r   )r   errors_liness     r   _capture_default_log_snapshotsr     sP    y#&&L&w9EEE'\JJJ(|LLL  r   c                  ,   ddl m}   G d d          }t          j        }t	          j                    xt          _        }	  |  |                       n# t          $ r Y nw xY w|t          _        n# |t          _        w xY w|                                S )z6Run ``hermes dump`` and return its stdout as a string.r   )run_dumpc                       e Zd ZdZdS ) _capture_dump.<locals>._FakeArgsFN)r   r   r   	show_keysr   r   r   	_FakeArgsr     s        			r   r   )hermes_cli.dumpr   sysstdoutioStringIO
SystemExitgetvalue)r   r   
old_stdoutcaptures       r   _capture_dumpr     s    ((((((        J;==(CJ      

Z
s*    A A1 
A!A1  A!!A1 1A?rj   rM   r   	dump_textlog_snapshotsr   r   c                    t          j                    }|st                      }|                    |           |t	          |           }|                    d           |                    d|  d           |                    |d         j                   |                    d           t          | d          }|                    d| d           |                    |d         j                   |                    d           |                    d	| d           |                    |d
         j                   |                    d           |                                S )aI  Build the summary debug report: system dump + log tails.

    Parameters
    ----------
    log_lines
        Number of recent lines to include per log file.
    dump_text
        Pre-captured dump output.  If empty, ``hermes dump`` is run
        internally.

    Returns the report as a plain-text string ready for upload.
    N

z--- agent.log (last z lines) ---
r   r   z--- errors.log (last r   z--- gateway.log (last r   r   )r   r   r   writer   r   r   r   )r   r   r   bufr   s        r   collect_debug_reportr     sQ   $ +--C $!OO	IIi6yAA IIfII=Y===>>>IImG$.///IIfy#&&LIIAlAAABBBIImH%/000IIfIIB|BBBCCCIImI&0111IIdOOO<<>>r   c                    t                       t          | dd          }t          | dd          }t          | dd          }|st          t                     t          d           t	                      }t          |          }t          |||          }|d	         j        }|d
         j        }|r|dz   |z   }|r|dz   |z   }|rt          |           |rBt          dd            t          d           t          d d           t          |           |rBt          dd            t          d           t          d d           t          |           dS t          d           i }	g }
	 t          ||          |	d<   ng# t          $ rZ}t          d| t          j                   t          d           t          |           t          j        d           Y d}~nd}~ww xY w|rE	 t          ||          |	d<   n/# t          $ r"}|
                    d|            Y d}~nd}~ww xY w|rE	 t          ||          |	d<   n/# t          $ r"}|
                    d|            Y d}~nd}~ww xY wt          d |	D                       }t          d           |	                                D ]\  }}t          d |d!| d |            |
r&t          d"d#                    |
           d$           t%          t'          |	                                                     t          d%           t          d&           t          d'           dS )(z:Collect debug report + full logs, upload each, print URLs.linesrj   expirer   localFzCollecting debug report...r   r   r   z

--- full agent.log ---
z

--- full gateway.log ---
r   z<============================================================zFULL agent.logr   zFULL gateway.logNzUploading...r   Reportz
Upload failed: )fileu7   
Full report printed below — copy-paste it manually:
rN   z	agent.logzagent.log: zgateway.logzgateway.log: c              3   4   K   | ]}t          |          V  d S N)rS   )r   ks     r   	<genexpr>z"run_debug_share.<locals>.<genexpr>Y  s(      ++c!ff++++++r   z
Debug report uploaded:z  <z
  (failed to upload: z, r   u)   
⏱  Pastes will auto-delete in 6 hours.z)To delete now:  hermes debug delete <url>z4
Share these links with the Hermes team for support.)rZ   getattrprint_PRIVACY_NOTICEr   r   r   r   r   r   r   stderrexitrR   rP   rD   rF   r   rv   r#   values)argsr   expiry
local_onlyr   r   report	agent_loggateway_logr7   failuresr   label_widthlabelr   s                  r   run_debug_sharer
    s#   %'''gs++IT8Q''Fw..J o	
&''' I29==M!#  F
 g&0I	*4K  K >>J	 Q"BB[P f 	###$$$"###X///""") 	###$$$$%%%X///"""+	.DH+FGGGX   '#''cj9999IJJJf	  1	1 29& Q Q QD 	1 	1 	1OO/#//00000000	1  3	3"4[f"U"U"UD 	3 	3 	3OO1C1122222222	3 ++d+++++K	
%&&&jjll 2 2
s050;00003001111 @>		((;(;>>>??? $t{{}}--...	
7888 

6777	
BCCCCCsI   3F 
G,AG''G,2H 
H3H..H39I 
I:I55I:c                    t          | dg           }|s t          d           t          d           dS |D ]}	 t          |          }|rt          d|            nt          d| d           ;# t          $ r}t          d|            Y d}~\d}~wt          $ r}t          d	| d
|            Y d}~d}~ww xY wdS )z1Delete one or more paste URLs uploaded by /debug.r7   z,Usage: hermes debug delete <url> [<url> ...]z;  Deletes paste.rs pastes uploaded by 'hermes debug share'.Nu     ✓ Deleted: u     ✗ Failed to delete: z (unexpected response)u     ✗ u     ✗ Could not delete z: )r   r   rQ   r%   rR   )r  r7   r   okr   s        r   run_debug_deleter  k  s-   4$$D <===KLLL 
: 
:		:c""B N---....LLLLMMM 	" 	" 	".3..!!!!!!!! 	: 	: 	:8C8838899999999	:
: 
:s#   7A11
B>;BB>B99B>c                 D   	 t                       n# t          $ r Y nw xY wt          | dd          }|dk    rt          |            dS |dk    rt	          |            dS t          d           t                       t          d           t          d           t          d           t                       t          d	           t          d
           t          d           t          d           t                       t          d           t          d           dS )zRoute debug subcommands.debug_commandNsharedeletezUsage: hermes debug <command>z	Commands:z?  share    Upload debug report to a paste service and print URLz-  delete   Delete a previously uploaded pastezOptions (share):z<  --lines N    Number of log lines to include (default: 200)z0  --expire N   Paste expiry in days (default: 7)z8  --local      Print report locally instead of uploadingzOptions (delete):z/  <url> ...    One or more paste URLs to delete)rX   rR   r   r
  r  r   )r  subcmds     r   	run_debugr    s?       T?D11F	8		 	-...kOPPP=>>> !!!LMMM@AAAHIII!"""?@@@@@s    
r   )r
   N)r   )7r   r   r    r   rC   urllib.errorrl   urllib.parseurllib.requestdataclassesr   pathlibr   typingr   hermes_constantsr   utilsr   rk   r   _MAX_LOG_BYTES_AUTO_DELETE_SECONDSr   r#   r   r)   r6   r   intrJ   r?   tuplerX   rZ   r   _GATEWAY_PRIVACY_NOTICEr<   boolrQ   rv   rx   r   r   r   r   r   r   r   r   r   r   r
  r  r  r   r   r   <module>r"     s    
			  



              ! ! ! ! ! !             , , , , , ,             $+   9t 9 9 9 9"tDz    "
4: 
$ 
 
 
 
 ;O  $s) C SW    (,% ,%x ,%%S/ ,% ,% ,% ,%^   
( 	3 	8C= 	 	 	 	(c (d ( ( ( (* AU 7 7S	 7# 7 7 7 7"2c 2c 2 2 2 2c c    (! ! !# !c ! ! ! !H  # c    :        I I I I I I     2 $	H_ H_ H_H_ H_ 	H_
 H_ H_ H_ H_Vc d3;K6L    s    * 6:	* * ** * Dk!123	*
 	* * * *bXD XD XDv: : :*A A A A Ar   