
    jAh                     ,   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mZmZ ddlm	Z	m
Z
 ddlmZmZmZ ddlmZ ddlmZmZmZ ddlmZ  ej        e          Z e            Zed	z  Zed
z  Zde	fdZde	fdZdeeef         fdZ deeef         fdZ!de	dedefdZ"de	deeee	f                  fdZ#de	de	de	fdZ$de	defdZ%de	de	defdZ&de	dee         fdZ'de	defdZ(deeeeee	f         f         fdZ)de	d e	defd!Z*d"d#d$ed%e+de,fd&Z-dBd'e+dee         fd(Z.dBd'e+de,fd)Z/dBd$ed%e+de,fd*Z0ed+k    rW e1d,            e/d"-          Z2 e3e2d.                    d/ e3e2d0                    d1e2d2          d3gZ4e2d4         rne2d4         Z5d5Z6d67                    e5de6                   Z8 e3e5          e6k    re8d7 e3e5          e6z
   d8z  Z8e49                     e3e5           d9e8            e2d:         r'e49                     e3e2d:                    d;           e2:                    d<          r'e49                     e3e2d<                    d=            e1d>d67                    e4           d?e2d@          dA           dS dS )Cu  
Skills Sync -- Manifest-based seeding and updating of bundled skills.

Copies bundled skills from the repo's skills/ directory into ~/.hermes/skills/
and uses a manifest to track which skills have been synced and their origin hash.

Manifest format (v2): each line is "skill_name:origin_hash" where origin_hash
is the MD5 of the bundled skill at the time it was last synced to the user dir.
Old v1 manifests (plain names without hashes) are auto-migrated.

Update logic:
  - NEW skills (not in manifest): copied to user dir, origin hash recorded.
  - EXISTING skills (in manifest, present in user dir):
      * If user copy matches origin hash: user hasn't modified it → safe to
        update from bundled if bundled changed. New origin hash recorded.
      * If user copy differs from origin hash: user customized it → SKIP.
  - DELETED by user (in manifest, absent from user dir): respected, not re-added.
  - REMOVED from bundled (in manifest, gone from repo): cleaned from manifest.

The manifest lives at ~/.hermes/skills/.bundled_manifest.
    N)datetimetimezone)PathPurePosixPath)get_bundled_skills_dirget_hermes_homeget_optional_skills_dir)is_excluded_skill_path)DictListTuple)atomic_replaceskillsz.bundled_manifestreturnc                  ^    t          t          t                    j        j        dz            S )zLocate the bundled skills/ directory.

    Checks HERMES_BUNDLED_SKILLS env var first (set by Nix wrapper),
    then a wheel-installed data dir, then falls back to the relative
    path from this source file.
    r   )r   r   __file__parent     0/usr/local/lib/hermes-agent/tools/skills_sync.py_get_bundled_dirr   ,   s#     "$x.."7">"IJJJr   c                  ^    t          t          t                    j        j        dz            S )z/Locate the official optional-skills/ directory.optional-skills)r	   r   r   r   r   r   r   _get_optional_dirr   6   s"    "4>>#8#?BS#STTTr   c                     t                                           si S 	 i } t                               d                                          D ]e}|                                }|sd|v rC|                    d          \  }}}|                                | |                                <   `d| |<   f| S # t          t          f$ r i cY S w xY w)z
    Read the manifest as a dict of {skill_name: origin_hash}.

    Handles both v1 (plain names) and v2 (name:hash) formats.
    v1 entries get an empty hash string which triggers migration on next sync.
    utf-8encoding: )MANIFEST_FILEexists	read_text
splitlinesstrip	partitionOSErrorIOError)resultlinename_hash_vals        r   _read_manifestr.   ;   s     !! 	!++W+==HHJJ 
	" 
	"D::<<D d{{$(NN3$7$7!a'/~~'7'7tzz||$$  "tW   			s   BB4 4C
	C
entriesc                 @   ddl }t          j                            dd           d                    d t          |                                           D                       dz   }	 |                    t          t          j                  dd	          \  }}	 t          j
        |d
d          5 }|                    |           |                                 t          j        |                                           ddd           n# 1 swxY w Y   t          |t                     dS # t           $ r( 	 t          j        |           n# t$          $ r Y nw xY w w xY w# t&          $ r.}t(                              dt          |d           Y d}~dS d}~ww xY w)zWrite the manifest file atomically in v2 format (name:hash).

    Uses a temp file + os.replace() to avoid corruption if the process
    crashes or is interrupted mid-write.
    r   NTparentsexist_ok
c              3   *   K   | ]\  }}| d | V  dS )r   Nr   ).0r+   r-   s      r   	<genexpr>z"_write_manifest.<locals>.<genexpr>_   s4      XXndH))x))XXXXXXr   z.bundled_manifest_.tmpdirprefixsuffixwr   r   z&Failed to write skills manifest %s: %s)exc_info)tempfiler!   r   mkdirjoinsorteditemsmkstempstrosfdopenwriteflushfsyncfilenor   BaseExceptionunlinkr'   	Exceptionloggerdebug)r/   r?   datafdtmp_pathfes          r   _write_manifestrV   V   s    OOOtd;;;99XXw}}@W@WXXXXX[__D`''M())' ( 
 
H
	2sW555 %			$$$% % % % % % % % % % % % % % % 8]33333 	 	 		(####   	  ` ` `=}aZ^_________`ss   (2E% D0 2ADD0 DD0 DD0 0
E";EE"
EE"EE""E% %
F/#FFskill_mdfallbackc                    	 |                      dd          dd         }n# t          $ r |cY S w xY wd}|                    d          D ]}|                                }|dk    r|r nbd	}#|r\|                    d
          rG|                    dd          d                                                             d          }|r|c S |S )zORead the name field from SKILL.md YAML frontmatter, falling back to *fallback*.r   replace)r   errorsNi  Fr4   z---Tzname:r      z"')r#   r'   splitr%   
startswith)rW   rX   contentin_frontmatterr*   strippedvalues          r   _read_skill_namerc   w   s   $$gi$HH$O   Nd## 
 
::<<u !N 	h11':: 	NN3**1-3355;;EBBE Os   " 11bundled_dirc                     g }|                                  s|S |                     d          D ]E}t          |          r|j        }t	          ||j                  }|                    ||f           F|S )zz
    Find all SKILL.md files in the bundled directory.
    Returns list of (skill_name, skill_directory_path) tuples.
    SKILL.md)r"   rglobr
   r   rc   r+   append)rd   r   rW   	skill_dir
skill_names        r   _discover_bundled_skillsrk      s    
 F %%j11 / /!(++ 	O	%h	??
z9-....Mr   ri   c                 @    |                      |          }t          |z  S )z
    Compute the destination path in SKILLS_DIR preserving the category structure.
    e.g., bundled/skills/mlops/axolotl -> ~/.hermes/skills/mlops/axolotl
    )relative_to
SKILLS_DIR)ri   rd   rels      r   _compute_relative_destrp      s"    
 


,
,Cr   	directoryc                    t          j                    }	 t          |                     d                    D ]}|                                rq|                    |           }|                    t          |                              d                     |                    |	                                           n# t          t          f$ r Y nw xY w|                                S )zHCompute a hash of all file contents in a directory for change detection.*r   )hashlibmd5rB   rg   is_filerm   updaterE   encode
read_bytesr'   r(   	hexdigest)rq   hasherfpathro   s       r   	_dir_hashr}      s    []]FIOOC0011 	2 	2E}} 2''	22c#hhoog66777e..00111		2
 W   s   B*C   CCpathbasec                 @   |                      |          }|                                }t          |          }d |j        D             }|                                s|rt          d |D                       rt          d|           d                    |          S )zLReturn a normalized relative POSIX path, rejecting traversal/absolute paths.c                     g | ]}|d v|	S )>   r    .r   r6   parts     r   
<listcomp>z*_safe_rel_install_path.<locals>.<listcomp>   s"    BBBdD	,A,AT,A,A,Ar   c              3   "   K   | ]
}|d k    V  dS )z..Nr   r   s     r   r7   z)_safe_rel_install_path.<locals>.<genexpr>   s&      -M-Mtddl-M-M-M-M-M-Mr   zUnsafe optional skill path: /)rm   as_posixr   partsis_absoluteany
ValueErrorrA   )r~   r   ro   posixpurer   s         r   _safe_rel_install_pathr      s    


4
 
 CLLNNEDBBdjBBBE A A#-M-Mu-M-M-M*M*M A???@@@88E??r   c                     g }t          |                     d                    D ]P}|                                r:|                    |                    |                                                      Q|S )z8List files inside a skill directory in lock-file format.rs   )rB   rg   rv   rh   rm   r   )ri   filesr|   s      r   _skill_file_listr      so    E	,,-- B B==?? 	BLL**955>>@@AAALr   c                 d    	 ddl m}  ||           S # t          $ r t          |           cY S w xY w)zJReturn the same hash style the skills hub lock uses, falling back locally.r   )content_hash)tools.skills_guardr   rN   r}   )rq   r   s     r   _content_hashr      sZ    $333333|I&&& $ $ $ #####$s    //c                  Z   t                      } i }|                                 s|S t          |                     d                    D ]a}t	          |          r|j        }	 t          ||           }n# t          $ r Y 7w xY w|j        }t          ||          }|||f}|||<   |||<   b|S )a%  Return official optional skills keyed by folder name and frontmatter name.

    Values are ``(folder_name, install_path, source_dir)``. Multiple keys may
    point to the same skill so callers can accept either the folder slug used
    by the hub lock or the user-facing frontmatter name.
    rf   )
r   r"   rB   rg   r
   r   r   r   r+   rc   )optional_dirindexrW   srcinstall_pathfolder_namefrontmatter_namerb   s           r   _optional_skill_indexr      s     %&&L.0E   <--j99:: ( (!(++ 	o	1#|DDLL 	 	 	H	h+HkBBlC0"k"'Ls   #A44
B Bbackup_rootc                 6   |                      t                    }||z  }|j                            dd           |                                rd}|                    |j         d|                                           r6|dz  }|                    |j         d|                                           6|                    |j         d|           }t          j        t          |           t          |                     |
                                S )zLMove an existing skill directory into a restore backup, preserving rel path.Tr1   r\   -)rm   rn   r   r@   r"   	with_namer+   shutilmoverE   r   )r~   r   ro   targetr<   s        r   _move_to_restore_backupr      s   


:
&
&C3F
Mt444}} =&+888899@@BB 	aKF &+888899@@BB 	!!V[";";6";";<<
KD		3v;;'''<<>>r   F)restorer+   r   c                   t                      }|sddg g g dS | dv r1t          t          |                                          d           ng }|s%|                    |           }|dd|  g g g dS |g}g }g }t          j        t          j                  	                    d	          }t          d
z  d| z  }|D ]\  }	}
}t          t          |
                    d           z  }t          |          }|                                ot          |          |k    }t          |dz  |	          }g }t                                          rt          t                              d                    D ]}t#          |          r|j        }	 |                    t                     n# t(          $ r Y Aw xY wt          ||j                  }||k    ra|j        |	k    s||	|hv r|                    |           |r|D ]9}|                                r#|                    t/          ||                     :|                                r%|s#|                    t/          ||                     |                                sF|j                            dd           t3          j        ||           |                    |	           |st7          d          }dd||||rt9          |          nddS )a3  Restore one or all official optional skills from repo source.

    ``restore=False`` only performs exact-match provenance backfill. ``restore=True``
    repairs already-mutated/reorganized skills by backing up matching active
    copies and copying the official optional source into its canonical path.
    Fz,No official optional skills directory found.)okmessagerestored
backfilled	backed_up>   rs   allc                     | d         S )Nr\   r   )items    r   <lambda>z1restore_official_optional_skill.<locals>.<lambda>
  s
    47 r   )keyNz#Official optional skill not found: z%Y%m%d-%H%M%Sz.restore-backupszofficial-optional-r   rf   Tr1   quietz(Official optional skill repair complete.r    )r   r   r   r   r   
backup_dir)r   rB   setvaluesgetr   nowr   utcstrftimern   r   r]   r}   r"   rc   rg   r
   r   rm   r   r+   rh   r   r@   r   copytree_backfill_optional_provenancerE   )r+   r   r   targetsr   r   r   	timestampr   r   r   r   destsrc_hashcanonical_oksrc_frontmattermatchesrW   	candidatecandidate_namematchr   s                         r   restore_official_optional_skillr      s    "##E K(Vdfvx  HJ  K  K  	KGK|G[G[fS((.B.BCCCCacG 4>,XRV,X,Xfhxz  JL  M  M  M(HIX\**33ODDI114T4T4TTK*1 # #&\3D,"4"4S"9"9::S>>{{}}D4H)D +3+;[II  	.":#3#3J#?#?@@ . .)(33 $O	))*5555!   H!1(IN!K!K$$>[00N{TcFd4d4dNN9--- 	  R R<<>> R$$%<UK%P%PQQQ{{}} M\ M  !8{!K!KLLL;;== -!!$!>>>T***,,, 		 /T:::J= *3;c+&&&  s   F))
F65F6r   c                 <   t                      }|                                sg S t          dz  dz  }	 |                                r&t          j        |                                          ndi d}n!# t          j        t          f$ r di d}Y nw xY w|                    di           }d |	                                D             }g }d}t          |                    d                    D ]}}t          |          r|j        }		 t          |	|          }
n3# t          $ r&}t                               d	|	|           Y d
}~Wd
}~ww xY wt          t%          |
                    d           z  }|                                r|                                st+          |          t+          |	          k    r|	j        }||v s|
|v rt/          j        t2          j                                                  }dd|
 ddt9          |          |
t;          |          ddi||d
||<   |                    |
           |                    |           d}| stA          d| d           |r,|j        !                    dd           dd
l"}t          j#        |dd          dz   }|$                    tK          |j                  dd          \  }}	 tM          j'        |dd           5 }|(                    |           |)                                 tM          j*        |+                                           d
d
d
           n# 1 swxY w Y   tY          ||           n5# tZ          $ r( 	 tM          j.        |           n# t          $ r Y nw xY w w xY w|S )!a  Mark already-present official optional skills as hub-installed.

    This covers the migration case where a skill used to be bundled (or was
    manually copied into the active skills tree) and later lives under
    optional-skills/. If the active copy is byte-identical to the official
    optional source, record official hub provenance without copying or
    reinstalling anything. Modified/local skills are left alone.
    z.hubz	lock.jsonr\   )version	installedr   c                 b    h | ],}t          |t                    |                    d           -S )r   )
isinstancedictr   )r6   entrys     r   	<setcomp>z0_backfill_optional_provenance.<locals>.<setcomp>Y  sD       eT""		.!!  r   Frf   z/Skipping optional skill with unsafe path %s: %sNr   officialz	official/builtinr   backfilled_fromr   )
source
identifiertrust_levelscan_verdictr   r   r   metadatainstalled_at
updated_atTz  = z* (official optional provenance backfilled)r1   r      )indentensure_asciir4   z.lock_r8   r9   r=   r   r   )/r   r"   rn   jsonloadsr#   JSONDecodeErrorr'   
setdefaultr   rB   rg   r
   r   r   r   rO   rP   r   r]   is_dirr}   r+   r   r   r   r   	isoformatr   r   addrh   printr@   r?   dumpsrD   rE   rF   rG   rH   rI   rJ   rK   r   rL   rM   )r   r   	lock_pathrQ   r   existing_pathsr   changedrW   r   r   rU   r   	lock_namer   r?   payloadrR   rS   rT   s                       r   r   r   F  sZ    %&&L   	V#k1I/4=4D4D4F4Fktz)--//000XYhjLkLk '* / / /2../R00I %%''  N JG<--j99:: $P $P!(++ 	o	1#|DDLL 	 	 	LLJCQRSSSHHHH	 D,"4"4S"9"9::{{}} 	DKKMM 	T??inn,,H		!!\^%C%CL..88::	 4l44$()$//(%d++*,=>%# 
  
	) 	<((()$$$ 	PNNNNOOO td;;; 	*T!%@@@4G''I$%% ( 
 
H
	2sW555 %   			$$$% % % % % % % % % % % % % % % 8Y//// 	 	 		(####   	 s~   ?A3 3BB	D
E
$EE
M' /AM?M' MM' MM' '
N2NN
NNNNc                 	   t                      }|                                s
g g dg g dg dS t                              dd           t	                      }t          |          }d |D             }g }g }g }d}|D ]\  }	}
t          |
|          }t          |
          }|	|vr	 |                                r7|dz  }t          |          |k    r|||	<   nx| st          d|	 d|	 d	           n_|j	                            dd           t          j        |
|           |                    |	           |||	<   | st          d
|	            # t          t          f$ r"}| st          d|	 d|            Y d}~d}~ww xY w|                                r|                    |	d          }t          |          }|s|||	<   ||k    r|dz  }n|dz  }_||k    r,|                    |	           | st          d|	 d           ||k    rF	 |                    d          }t          j        t%          |          t%          |                     	 t          j        |
|           |||	<   |                    |	           | st          d|	 d           t          j        |d           nm# t          t          f$ rY |                                rC|                                s/t          j        t%          |          t%          |                      w xY w# t          t          f$ r"}| st          d|	 d|            Y d}~d}~ww xY w|dz  }|dz  }t)          t+          |                                          |z
            }|D ]}||= |                    d          D ]}|                    |          }t          |z  }|                                sm	 |j	                            dd           t          j        ||           h# t          t          f$ r&}t4                              d||           Y d}~d}~ww xY wt9          |           t;          |           }|||||t=          |          |dS )z
    Sync bundled skills into ~/.hermes/skills/ using the manifest.

    Returns:
        dict with keys: copied (list), updated (list), skipped (int),
                        user_modified (list), cleaned (list), total_bundled (int)
    r   )copiedupdatedskippeduser_modifiedcleanedtotal_bundledoptional_provenance_backfilledTr1   c                     h | ]\  }}|S r   r   )r6   r+   r,   s      r   r   zsync_skills.<locals>.<setcomp>  s    888gdAT888r   r\   u     ⚠ uw   : bundled version shipped but you already have a local skill by this name — yours was kept. Run `hermes skills reset z)` to replace it with the bundled version.z  + z  ! Failed to copy : Nr    z  ~ z (user-modified, skipping)z.baku     ↑ z
 (updated))ignore_errorsz  ! Failed to update zDESCRIPTION.mdzCould not copy %s: %sr   )r   r"   rn   r@   r.   rk   rp   r}   r   r   r   r   rh   r'   r(   r   with_suffixr   rE   rmtreerB   r   keysrg   rm   copy2rO   rP   rV   r   len)r   rd   manifestbundled_skillsbundled_namesr   r   r   r   rj   	skill_srcr   bundled_hashrU   origin_hash	user_hashbackupr   r+   desc_mdro   	dest_descr   s                          r   sync_skillsr    s    #$$K 
RAB.0
 
 	
 TD111H-k::N88888MFGMG!/ Y Y
I%i== ++X%%C;;== 3 qLG ,66/;,," GZ G GBLG G G   K%%dT%BBBOIt444MM*---+7HZ(  31Z11222W% C C C CA
AAaAABBBC
 [[]] 2	",,z266K!$I 	 (1$,,qLGG qLGK''$$Z000 IGGGGHHH {**I!--f55FKD		3v;;777	4888/;,z222$ C!"A:"A"A"ABBBfDAAAAA#W-   !==?? @4;;== @"KFSYY???	 B  ) I I I  IGjGGAGGHHHI 1 qLGG S))M9::G  TNN $$%566 B B!!+..$	!! 	BB &&td&CCCWi0000W% B B B4gqAAAAAAAAB		B H%B%O%O%O" &^,,*H  sd   &B*EF"E??FAL$AJ54L$5A*LL$$M5MM,1PQ/QQc           	      <   t                      }t                      }t          |          }t          |          }| |v }| |v }|s|sddd|  dddS |r|| = t	          |           d}|r}|sddd|  dddS t          ||          |          }	|	                                rF	 t          j        |	           d	}n.# t          t          f$ r}
dd
d|  d|	 d|
 ddcY d}
~
S d}
~
ww xY wt          d	          }|r|r	d}d|  d}n|r	d}d|  d}nd
}d|  d}d	|||dS )u;  
    Reset a bundled skill's manifest tracking so future syncs work normally.

    When a user edits a bundled skill, subsequent syncs mark it as
    ``user_modified`` and skip it forever — even if the user later copies
    the bundled version back into place, because the manifest still holds
    the *old* origin hash. This function breaks that loop.

    Args:
        name: The skill name (matches the manifest key / skill frontmatter name).
        restore: If True, also delete the user's copy in SKILLS_DIR and let
                 the next sync re-copy the current bundled version. If False
                 (default), only clear the manifest entry — the user's
                 current copy is preserved but future updates work again.

    Returns:
        dict with keys:
          - ok: bool, whether the reset succeeded
          - action: one of "manifest_cleared", "restored", "not_in_manifest",
                    "bundled_missing"
          - message: human-readable description
          - synced: dict from sync_skills() if a sync was triggered, else None
    Fnot_in_manifest'zi' is not a tracked bundled skill. Nothing to reset. (Hub-installed skills use `hermes skills uninstall`.)N)r   actionr   syncedbundled_missingup   ' has no bundled source — manifest entry cleared but cannot restore from bundled (skill was removed upstream).Tmanifest_clearedzCleared manifest entry for 'z$' but could not delete user copy at r   r   r   z
Restored 'z' from bundled source.z/' (no prior user copy, re-copied from bundled).zf'. Future `hermes update` runs will re-baseline against your current copy and accept upstream changes.)r.   r   rk   r   rV   rp   r"   r   r   r'   r(   r  )r+   r   r   rd   r   bundled_by_namein_manifest
is_bundleddeleted_user_copyr   rU   r  r
  r   s                 r   reset_bundled_skillr  6  sL   0 H"$$K-k::N>**O("K(J 	
z 	
'ID I I I 
 
 	
  "TN!!!   		+U U U U    &od&;[II;;== 	d###$(!!W% 	 	 	0;t ; ;/3; ;78; ; #       	 t$$$F 
$ 
;t;;;	 	
TtTTT#W4 W W W 	
 &WOOOs   "B9 9C$
CC$C$__main__z1Syncing bundled skills into ~/.hermes/skills/ ...r   r   z newr   z updatedr   z
 unchangedr      z, z, +z morez user-modified (kept): r   z cleaned from manifestr   z official optional backfilledz
Done: z. r   z total bundled.)F);__doc__rt   r   loggingrF   r   r   r   pathlibr   r   hermes_constantsr   r   r	   agent.skill_utilsr
   typingr   r   r   utilsr   	getLogger__name__rO   HERMES_HOMErn   r!   r   r   rE   r.   rV   rc   rk   rp   r}   r   r   r   r   r   boolr   r   r   r  r  r   r)   r   r   namesMAX_SHOWrA   shownrh   r   r   r   r   <module>r#     s   ,    				  ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ] ] ] ] ] ] ] ] ] ] 4 4 4 4 4 4 $ $ $ $ $ $ $ $ $ $            		8	$	$ o8#
00K$ K K K KU4 U U U U
S#X    6`T#s(^ ` ` ` `Bt s s    *$ 4c4i8H3I    &d  $     #     T c     c    	$T 	$c 	$ 	$ 	$ 	$tCsC~)>$>?    6$ T c     CH D D D# D4 DD D D D DNZ Z Z$s) Z Z Z ZzP Pt P P P P Pf_P _Pc _PD _PT _P _P _P _PD z	E
=>>>[u%%%F3vh  &&&3vi !!+++)(((E
 o D'		%		*++3u::  733u::07777EE

BB5BBCCCi HF9-..FFFGGGzz233 fF#CDEEdddeee	E
QTYYu%%
Q
Q)@
Q
Q
QRRRRR' r   