
    `gjm                        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mZmZ ddlmZ  ej2                  e      Z e       Zed	z  Zed
z  ZdZde	fdZ de	fdZ!dee"   fdZ#dee"e"f   fdZ$de%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$Z0d%d&d'e"d(e1de2fd)Z3dQd*e1dee"   fd+Z4dQd*e1de2fd,Z5de	ddfd-Z6dQd'e"d(e1de2fd.Z7d/e"d0e"de1fd1Z8dee2   fd2Z9de	deee:   ee"   f   fd3Z;d'e"de2fd4Z<d5e1de2fd6Z=de1fd7Z>dQd8e1de2fd9Z?ed:k(  r e@d;        e5d%<      ZA eBeAd=          d> eBeAd?          d@eAdA    dBgZCeAdC   rTeAdC   ZDdDZEdEj                  eDdeE       ZG eBeD      eEkD  reGdF eBeD      eEz
   dGz  ZGeCj                   eBeD       dHeG        eAdI   reCj                   eBeAdI          dJ       eAj                  dK      reCj                   eBeAdK          dL        e@dMdEj                  eC       dNeAdO    dP       yy)Ru  
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OptionalSetTuple)atomic_replaceskillsz.bundled_manifestz.no-bundled-skills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     D/root/.hermes/venv/lib/python3.12/site-packages/tools/skills_sync.py_get_bundled_dirr   5   s$     "$x."7"7">">"I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   ?   s#    "4>#8#8#?#?BS#STTr   c                  V   	 ddl m} m}  |        t	               } |        D ]k  }|j                  d      D ]U  }t        |      r|j                  }|j                  |j                         t        |d      }|sE|j                  |       W m |S # t        $ r t	               cY S w xY w)zIndex every skill available in external_dirs by name and frontmatter name.

    Returns a set of skill names that are already provided by external dirs.
    Used to prevent sync_skills from shadowing externally-delegated skills.
    r   )get_external_skills_dirs_external_dirs_cache_clearSKILL.md )agent.skill_utilsr   r   ImportErrorsetrglobr
   r   addname_read_skill_name)r   r   external_namesext_dirskill_md	skill_dirfrontmatter_names          r   _build_external_skill_indexr.   D   s    Z
  "uN+- 
5j1 		5H%h/ Iy~~./"=""#34		5
5 %  us   B B('B(c                  ^   t         j                         si S 	 i } t         j                  d      j                         D ]U  }|j	                         }|sd|v r7|j                  d      \  }}}|j	                         | |j	                         <   Qd| |<   W | 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:r!   )MANIFEST_FILEexists	read_text
splitlinesstrip	partitionOSErrorIOError)resultliner'   _hash_vals        r   _read_manifestr@   a   s     !	!++W+=HHJ 
	"D::<Dd{$(NN3$7!a'/~~'7tzz|$  "t
	" W 	s   A?B B,+B,c                  v   	 ddl m}   |        S # t        $ r t        dz  }|j	                         st               cY S t               }	 |j                  d      j                         D ]8  }|j                         }|s|j                  d      r(|j                  |       : n# t        $ r Y nw xY w|cY S w xY w)u  Built-in skills the curator pruned — must NOT be re-seeded on sync.

    Delegates to ``tools.skill_usage`` (single source of truth) and falls back
    to reading ``~/.hermes/skills/.curator_suppressed`` directly if that import
    is unavailable in a packaged/update context.
    r   )read_suppressed_namesz.curator_suppressedr0   r1   #)tools.skill_usagerB   	Exception
SKILLS_DIRr5   r$   r6   r7   r8   
startswithr&   r:   )rB   pathnamesr=   s       r   _read_suppressed_namesrJ   |   s    ;$&& 11{{}5L	8CCE $zz| 4IIdO$  		sD    ,B8
B85B$>B$B$#B8$	B0-B8/B00B87B8entriesc                    ddl }t        j                  j                  dd       dj	                  d t        | j                               D              dz   }	 |j                  t        t        j                        dd	      \  }}	 t        j                  |d
d      5 }|j                  |       |j                          t        j                  |j                                ddd       t        |t               y# 1 sw Y   xY w# t         $ r' 	 t        j"                  |        # t$        $ r Y  w xY ww xY w# t&        $ r'}t(        j+                  dt        |d       Y d}~y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   0   K   | ]  \  }}| d |   yw)r3   Nr   ).0r'   r?   s      r   	<genexpr>z"_write_manifest.<locals>.<genexpr>   s     XndHvQxj)Xs   z.bundled_manifest_.tmpdirprefixsuffixwr0   r1   z&Failed to write skills manifest %s: %sexc_info)tempfiler4   r   mkdirjoinsorteditemsmkstempstrosfdopenwriteflushfsyncfilenor   BaseExceptionunlinkr:   rE   loggerdebug)rK   r\   datafdtmp_pathfes          r   _write_manifestrr      s?    td;99Xw}}@WXX[__D`''M(()' ( 
H
	2sW5 %	$% 8]3	% %
  			(#   	  `=}aZ^__`sg   .D= D
  AC>%D
 >DD
 
	D:D*)D:*	D63D:5D66D::D= =	E-E((E-r+   fallbackc                 Z   	 | j                  dd      dd }d}|j                  d      D ]l  }|j                         }|dk(  r|r |S d	} |s#|j	                  d
      s5|j                  dd      d   j                         j                  d      }|sj|c S  |S # t        $ r |cY S w xY w)zORead the name field from SKILL.md YAML frontmatter, falling back to *fallback*.r0   replace)r2   errorsNi  FrP   z---Tzname:r3      z"')r6   r:   splitr8   rG   )r+   rs   contentin_frontmatterr=   strippedvalues          r   r(   r(      s    $$gi$H$O Nd# 
::<u O "Nh11':NN3*1-335;;EBE
 O  s   B B*)B*bundled_dirc                     g }| j                         s|S | j                  d      D ]C  }t        |      r|j                  }t	        ||j
                        }|j                  ||f       E |S )zz
    Find all SKILL.md files in the bundled directory.
    Returns list of (skill_name, skill_directory_path) tuples.
    r    )r5   r%   r
   r   r(   r'   append)r}   r   r+   r,   
skill_names        r   _discover_bundled_skillsr      sp    
 F%%j1 /!(+OO	%h	?
z9-./ Mr   r,   c                 6    | j                  |      }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_torF   )r,   r}   rels      r   _compute_relative_destr      s    
 


,Cr   	directoryc                    t        j                         }	 t        | j                  d            D ]l  }|j	                         s|j                  |       }|j                  t        |      j                  d             |j                  |j                                n 	 |j                         S # t        t        f$ r Y |j                         S w xY w)zHCompute a hash of all file contents in a directory for change detection.*r0   )hashlibmd5r_   r%   is_filer   updaterb   encode
read_bytesr:   r;   	hexdigest)r   hasherfpathr   s       r   	_dir_hashr      s    [[]FIOOC01 	2E}}''	2c#hoog67e..01		2  W s   -B0 AB0 0CCrH   basec                 &   | j                  |      }|j                         }t        |      }|j                  D cg c]	  }|dvs| }}|j	                         s|rt        d |D              rt        d|       dj                  |      S c c}w )zLReturn a normalized relative POSIX path, rejecting traversal/absolute paths.>   r!   .c              3   &   K   | ]	  }|d k(    yw)z..Nr   )rR   parts     r   rS   z)_safe_rel_install_path.<locals>.<genexpr>   s     -Mtddl-Ms   zUnsafe optional skill path: /)r   as_posixr   partsis_absoluteany
ValueErrorr^   )rH   r   r   posixpurer   r   s          r   _safe_rel_install_pathr      s    


4
 CLLNED"jjBdD	,ATBEB#-Mu-M*M7w?@@88E? Cs   	BBc                     g }t        | j                  d            D ]A  }|j                         s|j                  |j	                  |       j                                C |S )z8List files inside a skill directory in lock-file format.r   )r_   r%   r   r   r   r   )r,   filesr   s      r   _skill_file_listr     sV    E	,- B==?LL**95>>@AB Lr   c                 T    	 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   rE   r   )r   r   s     r   _content_hashr   
  s1    $3I&& $ ##$s    ''c                  6   t               } i }| j                         s|S t        | j                  d            D ]N  }t	        |      r|j
                  }	 t        ||       }|j                  }t        ||      }|||f}|||<   |||<   P |S # t        $ r Y ^w xY w)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.
    r    )
r   r5   r_   r%   r
   r   r   r   r'   r(   )optional_dirindexr+   srcinstall_pathfolder_namer-   r|   s           r   _optional_skill_indexr     s     %&L.0E <--j9: (!(+oo	1#|DL hh+HkBlC0"k"'( L  		s   B	BBbackup_rootc                    | j                  t              }||z  }|j                  j                  dd       |j	                         rd}|j                  |j                   d|       j	                         r4|dz  }|j                  |j                   d|       j	                         r4|j                  |j                   d|       }t        j                  t        |       t        |             |j                         S )zLMove an existing skill directory into a restore backup, preserving rel path.TrM   rw   -)r   rF   r   r]   r5   	with_namer'   shutilmoverb   r   )rH   r   r   targetrX   s        r   _move_to_restore_backupr   1  s    


:
&C3F
MMt4}}&++ax89@@BaKF &++ax89@@B!!V[[M6(";<
KKD	3v;'<<>r   F)restorer'   r   c                R   t               }|sddg g g dS | dv r%t        t        |j                               d       ng }|s!|j	                  |       }|dd|  g g g dS |g}g }g }t        j                  t        j                        j                  d      }t        d	z  d
| z  }|D ]  \  }	}
}t        t        |
j                  d       z  }t        |      }|j                         xr t        |      |k(  }t        |dz  |	      }g }t        j                         rt        t        j!                  d            D ]s  }t#        |      r|j$                  }	 |j'                  t               t        ||j*                        }||k(  rM|j*                  |	k(  s||	|hv sc|j-                  |       u |r|D ].  }|j                         s|j-                  t/        ||             0 |j                         r|s|j-                  t/        ||             |j                         r~|j$                  j1                  dd       t3        j4                  ||       |j-                  |	       |rȐ t7        d      }dd||||rt9        |      dS ddS # t(        $ r Y gw xY w)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>   r   allc                     | d   S )Nrw   r   )items    r   <lambda>z1restore_official_optional_skill.<locals>.<lambda>J  s
    47 r   keyz#Official optional skill not found: z%Y%m%d-%H%M%Sz.restore-backupszofficial-optional-r   r    TrM   quietz(Official optional skill repair complete.r!   )r   r   r   r   r   
backup_dir)r   r_   r$   valuesgetr   nowr   utcstrftimerF   r   rx   r   r5   r(   r%   r
   r   r   r   r'   r   r   r]   r   copytree_backfill_optional_provenancerb   )r'   r   r   targetsr   r   r   	timestampr   r   r   r   destsrc_hashcanonical_oksrc_frontmattermatchesr+   	candidatecandidate_namematchr   s                         r   restore_official_optional_skillr   ?  s    "#E(Vdfvx  HJ  K  	KGK|G[fS(.BCacG4>.QRVQW,Xfhxz  JL  M  M(HIX\\*33ODI116H4TTK*1 #&\3D,"4"4S"9::S>{{}D4H)D +3+;[I ":#3#3J#?@ .)(3$OO	))*5 "2(INN!K$>>[0N{TcFd4dNN9-.   R<<>$$%<UK%PQR {{}\  !8{!KL;;=!!$!>T*,G#J /T:J= *3c+&  :< - " s   J	J&%J&r   c                    t               }|j                         sg S t        dz  dz  }	 |j                         r#t        j                  |j                               ndi d}|j                  di       }|j                         D ch c]#  }t        |t              r|j                  d      % }}g }d}t        |j                  d            D ]#  }	t        |	      r|	j                   }
	 t#        |
|      }t        t+        |j-                  d       z  }|j                         r|j/                         sht1        |      t1        |
      k7  r|
j2                  }||v s||v rt5        j6                  t8        j:                        j=                         }dd| ddt?        |      |tA        |      ddi||d
||<   |jC                  |       |jE                  |       d}| rtG        d| d       & |r|j                   jI                  dd       dd
l%}t        jL                  |dd      dz   }|jO                  tQ        |j                         dd      \  }}	 tS        jT                  |dd       5 }|jW                  |       |jY                          tS        jZ                  |j]                                d
d
d
       t_        ||       |S |S # t        j                  t        f$ r	 di d}Y w xY wc c}w # t$        $ r"}t&        j)                  d	|
|       Y d
}~Ud
}~ww xY w# 1 sw Y   qxY w# t`        $ r' 	 tS        jb                  |        # t        $ r Y  w xY ww xY w)!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.jsonrw   )version	installedr   r   Fr    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)rM   r      )indentensure_asciirP   z.lock_rT   rU   rY   r0   r1   )2r   r5   rF   jsonloadsr6   JSONDecodeErrorr:   
setdefaultr   
isinstancedictr   r_   r%   r
   r   r   r   rk   rl   r   rx   is_dirr   r'   r   r   r   r   	isoformatr   r   r&   r   printr]   r\   dumpsra   rb   rc   rd   re   rf   rg   rh   r   ri   rj   )r   r   	lock_pathrm   r   entryexisting_pathsr   changedr+   r   r   rq   r   	lock_namer   r\   payloadrn   ro   rp   s                        r   r   r     sR    %&L 	V#k1I/4=4D4D4Ftzz)--/0XYhjLk R0I %%'eT" 			.!N  JG<--j9: $P!(+oo	1#|DL D,"4"4S"9::{{}DKKMT?in,HH		!\^%CLL.88:	 %l^4$()$/(%d+*,=>%# 
	) 	<()$D#MNOI$PL td; 	**T!%@4G''I$$% ( 
H
	2sW5 % 	$% 8Y/ :U   '* /2./  	LLJCQRS	\% %
  			(#   	sx   8K (K8+K=L7 6AL+;L7 K54K5=	L(L##L(+L40L7 7	M'MM'	M# M'"M##M'c           
      T   t         t        z  j                         r| st        d       g g dg g dg ddS t	               }|j                         sg g dg g g dg dS t
        j                  dd       t               }t        |      }|D ch c]  \  }}|	 }}}t               }t               }g }	g }
g }g }g }d}|D ]  \  }}||v r|j                  |       t        ||      }t        |      }|j                  d      }|j                         rl|j                         s\	 |j                  j                  dd       t!        j"                  t%        |      t%        |             t&        j)                  d|       ||v rt|	j                  |       |dz  }| st        d| d       |j                         r;t        |      |k(  r-t1        |       | st        d|        |j3                  |d       9||vr	 |j                         r.|dz  }t        |      |k(  r|||<   nn| slt        d| d| d       nY|j                  j                  dd       t!        j4                  ||       |
j                  |       |||<   | st        d|        |j                         r|j7                  |d      }t        |      }|s|||<   ||k(  r|dz  }n|dz  }t9        ||      r$|j                  |       | st        d| d       N||k7  r	 |j                  d      }|j                         rt1        |       t!        j"                  t%        |      t%        |             	 t!        j4                  ||       |||<   |j                  |       | st        d| d       	 t1        |       |dz  }|dz  } t=        t?        |jA                               |z
        }|D ]  }||=  |jC                  d      D ]a  }|jE                  |      }t
        |z  }|j                         r.	 |j                  j                  dd       t!        jF                  ||       c tI        |       tK        |        }|
|||||tM        |      ||	d!	S c c}}w # t*        t,        f$ r t&        j/                  d	|d
       Y Bw xY w# t*        t,        f$ r}| st        d| d|        Y d}~/d}~ww xY w# t*        t,        f$ r t&        j;                  d|d
       Y ow xY w# t*        t,        f$ r |j                         r|j                         r:	 t1        |       n-# t*        t,        f$ r t&        j/                  d|d
       Y nw xY w|j                         s(t!        j"                  t%        |      t%        |              w xY w# t*        t,        f$ r}| st        d| d|        Y d}~3d}~ww xY w# t*        t,        f$ r"}t&        j;                  d||       Y d}~$d}~ww xY w)"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)
    uJ     (skipped — profile opted out of bundled skills via .no-bundled-skills)r   T)copiedupdatedskippeduser_modifiedcleanedtotal_bundledoptional_provenance_backfilledskipped_opt_out)r   r   r   r   r   
suppressedr   r   rM   z.bakz#Recovered orphaned skill backup: %sz*Could not recover orphaned skill backup %srZ   rw   u     ⇢ z7 (deferred to external_dirs, not written to local tree)u     ✓ removed stale shadow of Nu     ⚠ 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 : r!   z  ~ z (user-modified, skipping)u     ↑ z
 (updated)zCould not remove backup %sz.Could not clear partial copy %s during restorez  ! Failed to update zDESCRIPTION.mdzCould not copy %s: %sr   )	r   r   r   r   r   r  r   r   shadowed_by_external)'HERMES_HOMENO_BUNDLED_SKILLS_MARKERr5   r   r   rF   r]   r@   r   rJ   r.   r   r   r   with_suffixr   r   r   rb   rk   infor:   r;   warning_rmtree_writablepopr   r   _is_tracked_user_modificationrl   r_   r$   keysr%   r   copy2rr   r   len)r   r}   manifestbundled_skillsr'   r>   bundled_namesr  external_indexr  r   r   r   suppressed_skippedr   r   	skill_srcr   bundled_hash_orphanrq   origin_hash	user_hashbackupr   desc_mdr   	dest_descr   s                                r   sync_skillsr    sL    	..668^_RAB.0T
 	
 #$KRABbST.0
 	
 TD1H-k:N)78gdAT8M8')J02N&(FGM$&G!/ a
I #%%j1%i= + ""6*>>DKKM!!$!>CL#d)4A7K '
 !''
3qLGZL )1 1 {{}4L!@ &::,GHZ.X%C;;= qLG ,6/;,"$ZL 1BBL NFG KK%%dT%BOOIt4MM*-+7HZ( ZL12 [[]",,z26K!$I (1$,qLG qLG,[)D$$Z0D,FGH {*'I!--f5F
 }}(0KKD	3v;7	48/;,z2$!F:,j"AB^,V4. 1 qLGCaH S)M9:G TN $$%56 B!!+.$	!B  &&td&CWi0B H%B%O" &(^,*H 4
 
E 9J W% @'!  z W% C/
|2aSABCb !(1 ^"LL)EvX\L]^#W-  "==?#{{}!&$4T$:(/'9 !&$*NN(X(,t %3 %&!&
 $(;;= &CKT C   ) I  5j\A3GHI. W% B4gqAABs   
R!AR9BSAW$=T "S2-3W6'R?>R?S/S**S/2'TT TT  /WUW'VWV=WWW3W..W36X'X""X'c                     t        |       j                         }t        j                         }||j                  vrt	        d|d|d      ddlfd}t        j                  | |       y)a  Remove a directory tree, making read-only entries writable first.

    Handles immutable package sources (Nix store, deb/rpm installs) that
    preserve read-only permissions on copied files *and* directories
    (``r-xr-xr-x``).  Removing a child requires write permission on its
    parent directory, so the retry handler makes the failing path **and its
    parent** writable before re-attempting.  See #34860, #34972.
    zrefusing to rmtree z: not strictly under u    (scope guard — see #48200)r   Nc                     t         j                  j                  |      |fD ]#  }	 t        j                  |j                         %  | |       y # t
        $ r Y :w xY w)N)rc   rH   dirnamechmodS_IRWXUr:   )funcr   r[   r   stats       r   	_on_errorz#_rmtree_writable.<locals>._on_error  sZ     wwu-u5 	F.	
 	U  s    A	AA)onerror)r   resolverF   rN   r   r$  r   rmtree)rH   r   skills_rootr%  r$  s       @r   r
  r
    ss    ( $Z!F$$&K &..(!&+@ P+ ,
 	
  MM$	*r   c                    t               }t               }t        |      }t        |      }| |v }| |v }|s|sddd|  dddS d}|r:|sddd|  dddS t	        ||    |      }	|	j                         r	 t        |	       d	}|r|| = t        |       t        d	      }|r|r	d}d|  d}n|r	d}d|  d}nd}d|  d}d	|||dS # t        t        f$ r}
dd
d|	 d|
 dddcY d}
~
S d}
~
ww xY w)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_missingur   ' has no bundled source — manifest entry preserved but cannot restore from bundled (skill was removed upstream).T	not_resetzCould not delete user copy at r  u3   . Manifest entry preserved — nothing was changed.r   r   z
Restored 'z' from bundled source.z/' (no prior user copy, re-copied from bundled).manifest_clearedzCleared manifest entry for 'zf'. Future `hermes update` runs will re-baseline against your current copy and accept upstream changes.)r@   r   r   r   r   r5   r
  r:   r;   rr   r  )r'   r   r  r}   r  bundled_by_namein_manifest
is_bundleddeleted_user_copyr   rq   r.  r-  r   s                 r   reset_bundled_skillr6    s   0 H"$K-k:N>*O("K(Jz'D6 H I 
 	
 +v T U   &od&;[I;;= &$(! TN! t$F$tf$:;	tf$ST#*4& 1V W 	
 &WOOC W% 	)8b DL M # 	s   3C C*C%C*%C*r  r  c                 &    t        |       xr || k7  S )a  Whether an on-disk skill counts as a user modification ``hermes update`` keeps.

    Shared by the sync loop (which decides what to skip) and
    ``list_user_modified_bundled_skills`` (which surfaces the names) so the two
    can never drift. A skill is a tracked modification only when it has a
    recorded origin hash (an un-baselined / v1 entry with an empty hash is not)
    and its current content hash differs from that origin.
    )bool)r  r  s     r   r  r  g  s     9k!99r   c                  @   t               } | sg S t               }g }t        |      D ]b  \  }}| j                  |d      }|st	        ||      }|j                         s8t        |t        |            sN|j                  |||d       d |j                  d        |S )u  Return the bundled skills that ``hermes update`` keeps because the user
    edited them locally.

    A skill counts as user-modified when its on-disk copy no longer matches the
    origin hash recorded in the manifest the last time it was synced — the exact
    same test the sync loop uses to decide what to skip. This is the discovery
    half of that behavior, so a user can find the names the ``~ N user-modified
    (kept)`` notice only counts.

    Returns a list (sorted by name) of dicts:
        ``{"name": str, "dest": Path, "bundled_src": Path}``
    where ``dest`` is the user's copy and ``bundled_src`` is the current stock
    copy (so callers can diff or restore).
    r!   )r'   r   bundled_srcc                     | d   S )Nr'   r   )rq   s    r   r   z3list_user_modified_bundled_skills.<locals>.<lambda>  s
    &	 r   r   )
r@   r   r   r   r   r5   r  r   r   sort)r  r}   modifiedr   r,   r  r   s          r   !list_user_modified_bundled_skillsr>  s  s     H	"$KH!9+!F 
Ill:r2 %i={{}(ioFOO#T)L MM)M*Or   c                     	 | j                         }d|v r|dfS 	 ||j                  d      fS # t        $ r Y yw xY w# t        $ r |dfcY S w xY w)zRead a file once for diffing.

    Returns ``(raw_bytes, text)`` where ``text`` is ``None`` if the file is
    binary; ``(None, None)`` if it could not be read. Returning the raw bytes
    lets the caller compare binary files without re-reading them.
    NN    Nr0   )r   r:   decodeUnicodeDecodeError)rH   rm   s     r   _read_for_diffrD    sm      $TzT[[)))    Tzs   . = 	::AAc                    ddl }t               }t        t        |            }|j	                  |       }|d| ddg d|  ddS t        ||      }|j                         sd| ddg d|  d	| d
dS t        t        |            }t        t        |            }g }t        ||z        D ]  }	|	|v }
|	|v }|
rt        ||	z        nd\  }}|rt        ||	z        nd\  }}|
r|r||||k7  r|j                  |	ddd       X||k(  r^dj                  |j                  |j                  d      |j                  d      d|	 d|	             }|j                  |	d|d       |
r|j                  |	dd|	 d       |j                  |	dd|	 d        t        |      }d| d|||sd|  ddS d|  dt!        |       ddS )u8  Diff a user's copy of a bundled skill against the current stock version.

    Lets a user see exactly what diverged before deciding whether to keep their
    edits or ``hermes skills reset`` back to upstream.

    Returns a dict:
        ``ok`` (bool), ``name`` (str), ``found`` (bool — bundled source exists),
        ``modified`` (bool), ``message`` (str),
        ``diffs``: list of ``{"path": str, "status": str, "diff": str}`` where
        status is one of ``modified`` / ``added`` (only in user copy) /
        ``removed`` (only in bundled) / ``binary``.
    r   NFr,  zv' is not a tracked bundled skill (no stock version to diff against). Hub-installed skills use `hermes skills inspect`.)r   r'   foundr=  diffsr   TzNo local copy of 'z' found at r   r@  binaryz<binary file differs>)rH   statusdiffr!   )keependszstock/zyours/)fromfiletofiler=  addedz+ only in your copy: removedz- only in stock: z' matches the stock version.z$' differs from the stock version in z	 file(s).)difflibr   r   r   r   r   r5   r$   r   r_   rD  r   r^   unified_diffr7   r8  r  )r'   rP  r}   r2  r:  r   
user_filesstock_filesrG  r   in_userin_stock
user_bytes	user_textstock_bytes
stock_texttextr=  s                     r   diff_bundled_skillr[    sy    "$K3K@AO!%%d+KD6 S T

 
	
 "+{;D;;=+D6TF!D
 	
 %d+,J&{34KEj;./ %#+%*1N4#:&| 	
I 2:N;,-| 	 Z x J$6 ,LL!$BYZ J&77$$))4)8(($(7%cU^#C5>	 % D LL#TJKLL;PQTPU9VW LL	=Nse;TUG%N E{H  v12  TF>s5zl)T r   enabledc                 V   t         t        z  }|j                         }	 | r4t         j                  dd       |j	                  dd       | }|rdnd}n|r|j                          |}|rdnd	}d|t        |      |dS # t        $ r }d
d
t        |      d| d| dcY d}~S d}~ww xY w)aj  Toggle the .no-bundled-skills opt-out marker for the active profile.

    When ``enabled`` is True, writes HERMES_HOME/.no-bundled-skills so the
    installer, ``hermes update``, and any direct sync stop seeding bundled
    skills. When False, removes the marker so seeding resumes on the next
    sync. This is the on-disk-state half of ``hermes skills opt-out`` /
    ``opt-in``; removal of already-present skills is a separate, explicit
    step (see ``remove_pristine_bundled_skills``).

    Returns:
        dict with keys: ok (bool), changed (bool), marker (str path),
                        message (str).
    TrM   zThis profile opted out of bundled-skill seeding (`hermes skills opt-out`).
Delete this file to re-enable sync on the next `hermes update`.
r0   r1   zpOpted out of bundled skills. Future install / update / sync runs will not seed bundled skills into this profile.u1   Already opted out — marker was already present.zgOpted back in. The next `hermes update` (or `hermes skills opt-in --sync`) will re-seed bundled skills.u&   Not opted out — no marker to remove.Fz#Could not update opt-out marker at r  )r   r   markerr   N)r  r  r5   r]   
write_textrj   r:   rb   )r\  r^  existedr   r   rq   s         r   set_bundled_skills_opt_outra    s     33FmmoG
dT:T !	   "kG G I	  G ? >	  7c&kgVV  
ES[<VHBqcJ
 	

s   AA? ?	B(B#B(#B(c                  8    t         t        z  j                         S )z=Return True if the active profile carries the opt-out marker.)r  r  r5   r   r   r   is_bundled_skills_opt_outrc  @  s    22::<<r   dry_runc                    t               }t               }t        t        |            }g }g }t	        |j                               D ]  \  }}|j                  |      }||j                  |dd       .t        ||      }	|	j                         s
| s||v r||= Tt        |	      }
|
|k7  r|j                  |dd       y| r|j                  |       	 t        |	       ||v r||= |j                  |        | s|rt        |       | rdnd}| dt        |       d	t        |       d
}d||| |dS # t        t        f$ r"}|j                  |d| d       Y d}~d}~ww xY w)a  Delete bundled skills that are present, manifest-tracked, AND unmodified.

    Safety is the whole point of this function. A skill on disk is removed
    ONLY when all of these hold:
      - it is recorded in the sync manifest (so it is genuinely a bundled
        skill, not a hub-installed or hand-written one), AND
      - it still exists in the bundled source (so we can hash-compare), AND
      - its on-disk copy is byte-identical to the manifest origin hash
        (so the user has not edited it).

    Anything user-modified, hub-installed, or locally authored is left
    untouched and reported under ``skipped``. The manifest entry for each
    removed skill is dropped so a later opt-in re-seed treats it as new.

    Args:
        dry_run: When True, compute what would be removed without deleting.

    Returns:
        dict with keys: ok (bool), removed (list[str]),
                        skipped (list[dict]) where each dict is
                        {name, reason}, dry_run (bool), message (str).
    Nz$no bundled source (removed upstream))r'   reasonzuser-modified (kept)zdelete failed: zWould removeRemoved z! pristine bundled skill(s); kept r   T)r   rO  r   rd  r   )r@   r   r   r   r_   r`   r   r   r   r5   r   r
  r:   r;   rr   r  )rd  r  r}   r2  rO  r   r'   r  r   r   on_diskrq   verbr   s                 r   remove_pristine_bundled_skillsrk  E  s   . H"$K3K@AOGG#HNN$45 k!!$';NND4Z[\%c;7{{}tx/TND/k!NND4JKLNN4 	T" 8t7: w!$>)DaG~%Fs7|nTUVGw7w  ! 	NNDoaS4IJK	s   D33E$EE$__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)J__doc__r   r   loggingrc   r   r   r   pathlibr   r   hermes_constantsr   r   r	   r"   r
   typingr   r   r   r   r   utilsr   	getLogger__name__rk   r  rF   r4   r  r   r   rb   r.   r@   r$   rJ   rr   r(   r   r   r   r   r   r   r   r   r8  r   r   r   r  r
  r6  r  r>  bytesrD  r[  ra  rc  rk  r   r<   r  r   rI   MAX_SHOWr^   shownr   r   r   r   r   <module>ry     s7  ,    	  ' ' ] ] 4 3 3  			8	$ 8#
00 0 K$ KU4 U
SX :S#X 6 4`T#s(^ `Bt s s *$ 4c4i8H3I &d  $  #  T c  c 	$T 	$c 	$tCsC~)>$>? 6$ T c  CH D# D4 DD DNZ Z$s) Zzmt m m`.+4 .+D .+b`Pc `PD `PT `PF	:s 	:s 	:t 	:"4: "J %#(F"G &`S `T `F/W /W /Wd=4 =
CD CT CL z	
=>u%Fvh 
!&vi !
"(+)
Z(E
 o'		%	*+u: s3u:0177EE
|#:5'BCiF9-.//EFGzz23F#CDEFFcde	HTYYu%&b)@(A
QR' r   