
    Pmj                        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mZmZ ddl	m
Z
mZ ej        ej        fD ]4Z eed          r&	 e                    dd           '# eef$ r Y 0w xY w5d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j!        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Z0de
deee*e
f                  fdZ1de
de
de
fdZ2de
de*fd Z3d!e
d"e
de*fd#Z4de
dee*         fd$Z5de
de*fd%Z6dee*ee*e*e
f         f         fd&Z7d!e
d'e
de*fd(Z8d)d*d+e*d,e9de:fd-Z;dee*ee
         f         fd.Z<	 d`d/e*d0eee*ee
         f                  dee
         fd1Z=dad2e9dee*         fd3Z>dee*         fd4Z?dee*ee
         f         fd5Z@d6e*d7e*d8e
d9ee*ee
         f         d:ee*         d2e9dee*         fd;ZAdad2e9de:fd<ZBd!e
ddfd=ZCdad+e*d,e9de:fd>ZDd7e*d?e*de9fd@ZEdee:         fdAZFd!e
deeeG         ee*         f         fdBZHd+e*de:fdCZIdDe9de:fdEZJde9fdFZKdadGe9de:fdHZLe"dIk    rW eMdJ            eBd)K          ZN eOeNdL                    dM eOeNdN                    dOeNdP          dQgZPeNdR         rneNdR         ZQdSZRdTS                    eQdeR                   ZT eOeQ          eRk    reTdU eOeQ          eRz
   dVz  ZTePU                     eOeQ           dWeT            eNdX         r'ePU                     eOeNdX                    dY           eNV                    dZ          r'ePU                     eOeNdZ                    d[            eMd\dTS                    eP           d]eNd^          d_           dS dS )bu=  
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 bundled still matches origin hash: no update → skip without reading
        the user copy.
      * If bundled changed and user copy matches origin hash: safe to update.
      * If bundled changed and user copy differs: 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reconfigureutf-8replaceencodingerrors)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 falls back to the relative path from this source file.
    r   )r   r   __file__parent     7/home/thesage/.hermes/hermes-agent/tools/skills_sync.py_get_bundled_dirr   F   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"   O   s"    "4>>#8#?BS#STTTr   c                     	 ddl m} m} n# t          $ r t	                      cY S w xY w |             t	                      } |             D ]r}|                    d          D ]Z}t          |          r|j        }|                    |j	                   t          |d          }|r|                    |           [s|S )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_indexr4   T   s   ZZZZZZZZZ   uu    "uuN++-- 
5 
5j11 		5 		5H%h//  Iy~.../"== 5""#3444		5 s    &&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.
    r   r   :r'   )MANIFEST_FILEexists	read_text
splitlinesstrip	partitionOSErrorIOError)resultliner-   _hash_vals        r   _read_manifestrD   q   s     !! 	!++W+==HHJJ 
	" 
	"D::<<D d{{$(NN3$7$7!a'/~~'7'7tzz||$$  "tW   			s   BB4 4C
	C
c                     	 ddl m}   |             S # t          $ r t          dz  }|                                st                      cY S t                      }	 |                    d                                          D ]B}|                                }|r*|	                    d          s|
                    |           Cn# 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_suppressedr   r6   #)tools.skill_usagerF   	Exception
SKILLS_DIRr9   r*   r:   r;   r<   
startswithr,   r>   )rF   pathnamesrA   s       r   _read_suppressed_namesrN      s
   ;;;;;;$$&&&   11{{}} 	55LLL	88CCEE $ $zz|| $ 4 4 $IIdOOO$  	 	 	D	s:    6C
CA+CC
CCC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 )r7   Nr   ).0r-   rC   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   r6   z&Failed to write skills manifest %s: %sexc_info)tempfiler8   r   mkdirjoinsorteditemsmkstempstrosfdopenwriteflushfsyncfilenor   BaseExceptionunlinkr>   rI   loggerdebug)rO   r`   datafdtmp_pathfes          r   _write_manifestrv      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r1   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   r	   r
   Ni  FrT   z---Tzname:r7      z"')r:   r>   splitr<   rK   )r1   rw   contentin_frontmatterrA   strippedvalues          r   r.   r.      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 ]Z}t          |                    |           |           r'|j        }t          ||j                  }|                    ||f           [|S )zz
    Find all SKILL.md files in the bundled directory.
    Returns list of (skill_name, skill_directory_path) tuples.
    r&   root)r9   r+   r   relative_tor   r.   r-   append)r   r   r1   r2   
skill_names        r   _discover_bundled_skillsr      s    
 F %%j11 / /
 "  --K
 
 
 	 O	%h	??
z9-....Mr   r2   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
    )r   rJ   )r2   r   rels      r   _compute_relative_destr      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md5rc   r+   is_filer   updaterf   encode
read_bytesr>   r?   	hexdigest)r   hasherfpathr   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rL   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   rV   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   rW   z)_safe_rel_install_path.<locals>.<genexpr>  s&      -M-Mtddl-M-M-M-M-M-Mr   zUnsafe optional skill path: /)r   as_posixr   partsis_absoluteany
ValueErrorrb   )rL   r   r   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.r   )rc   r+   r   r   r   r   )r2   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   rI   r   )r   r   s     r   _content_hashr      sZ    $333333|I&&& $ $ $ #####$s    //c                     t                      } i }|                                 s|S t          |                     d                    D ]v}t	          |                    |           |           r'|j        }	 t          ||           }n# t          $ r Y Lw xY w|j	        }t          ||          }|||f}|||<   |||<   w|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.
    r&   r   )r"   r9   rc   r+   r   r   r   r   r   r-   r.   )optional_dirindexr1   srcinstall_pathfolder_namer3   r~   s           r   _optional_skill_indexr   ,  s     %&&L.0E   <--j99:: ( (!  ..\
 
 
 	 o	1#|DDLL 	 	 	H	h+HkBBlC0"k"'Ls   8B		
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.TrQ   ry   -)r   rJ   r   ra   r9   	with_namer-   shutilmoverf   r   )rL   r   r   targetr\   s        r   _move_to_restore_backupr   I  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>   r   allc                     | d         S )Nry   r   )items    r   <lambda>z1restore_official_optional_skill.<locals>.<lambda>b  s
    47 r   keyNz#Official optional skill not found: z%Y%m%d-%H%M%Sz.restore-backupszofficial-optional-r   r&   TrQ   quietz(Official optional skill repair complete.r'   )r   r   r   r   r   
backup_dir)r   rc   r*   valuesgetr   nowr   utcstrftimerJ   r   rz   r   r9   r.   r+   r   r   r   r   r-   r   r   ra   r   copytree_backfill_optional_provenancerf   )r-   r   r   targetsr   r   r   	timestampr   r   r   r   destsrc_hashcanonical_oksrc_frontmattermatchesr1   	candidatecandidate_namematchr   s                         r   restore_official_optional_skillr   W  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c                     i } t                                           s| S t                               d          D ]}t          |          r|j        }	 |                                                    t                                                      n# t          t          f$ r Y lw xY w| 	                    |j
        g                               |           | S )zCIndex installed skills by directory name with one active-tree scan.r&   )rJ   r9   r+   r   r   resolver   r>   r   
setdefaultr-   r   )r   r1   r   s      r   #_index_installed_skill_dirs_by_namer     s    #%E $$Z00 	? 	?!(++ 	O		++J,>,>,@,@AAAA$ 	 	 	H	,,33I>>>>Ls   >BB%$B%skill_dir_nameinstalled_indexc                     | rt                                           sdS |t                      }|                    | g           }t	          |          dk    rdS |d         S )u  Locate an installed skill directory by its directory name.

    Used only as a fallback when the repo-derived install path doesn't exist in
    the active tree (upstream recategorized the skill after it was installed).
    Returns None when there is no match, or when the name is AMBIGUOUS — two
    skills sharing a directory name give us no basis to pick one, and guessing
    would write provenance onto the wrong skill. The caller still verifies a
    byte-identical content hash before recording anything.
    Nry   r   )rJ   r9   r   r   len)r   r   r   s      r   !_find_installed_skill_dir_by_namer     sj      !2!2!4!4 t=??!!."55G
7||qt1:r   r   c                 6   t                      }|                                sg S t          dz  dz  }	 |                                r(t          j        |                    d                    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	}d
}t          |                    d                    D ]}	t          |	          r|	j        }
	 t          |
|          }n3# t          $ r&}t                               d|
|           Y d
}~Wd
}~ww xY w|
j        }||v s||v rot          t'          |                    d           z  }|                                r|                                ss|t-                      }t/          |
j        |          }|	 t          |t                    }n4# t          $ r'}t                               d||           Y d
}~&d
}~ww xY w||v r4t1          |          t1          |
          k    rVt3          j        t6          j                                                  }dd| ddt=          |          |t?          |          ddi||d
||<   |                     |           |!                    |           d}| stE          d| d           |r,|j        #                    dd           dd
l$}t          j%        |dd	          dz   }|&                    tO          |j                  dd           \  }}	 tQ          j)        |d!d          5 }|*                    |           |+                                 tQ          j,        |-                                           d
d
d
           n# 1 swxY w Y   t]          ||           n5# t^          $ r( 	 tQ          j0        |           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.
    .hub	lock.jsonr   r6   ry   )version	installedr   c                 b    h | ],}t          |t                    |                    d           -S )r   )
isinstancedictr   )rV   entrys     r   	<setcomp>z0_backfill_optional_provenance.<locals>.<setcomp>  sD       eT""		.!!  r   FNr&   z/Skipping optional skill with unsafe path %s: %sr   z(Skipping relocated optional skill %s: %sofficialz	official/builtinr   backfilled_fromr!   )
source
identifiertrust_levelscan_verdictr   r   r   metadatainstalled_at
updated_atTz  = z* (official optional provenance backfilled)rQ   r      )indentensure_asciirT   z.lock_rX   rY   r]   )1r"   r9   rJ   jsonloadsr:   JSONDecodeErrorr>   r   r   rc   r+   r   r   r   r   ro   rp   r-   r   rz   is_dirr   r   r   r   r   r   r   	isoformatr   r   r,   r   printra   r`   dumpsre   rf   rg   rh   ri   rj   rk   rl   r   rm   rn   )r   r   	lock_pathrq   r   existing_pathsr   changedinstalled_dir_indexr1   r   r   ru   	lock_namer   r   r`   payloadrr   rs   rt   s                        r   r   r     s    %&&L   	V#k1I/DMDTDTDVDV{tz)--w-??@@@hixz\{\{ '* / / /2../R00I %%''  N JG;?<--j99:: 6P 6P!(++ 	o	1#|DDLL 	 	 	LLJCQRSSSHHHH	 H		!!\^%C%CD,"4"4S"9"9::{{}} 	DKKMM 	 #*&I&K&K#4SX?RSSD|5dJGG   GqQQQ >))T??inn,,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   AA5 5BBD
E(E		EG,,
H6HHO$ ,AO<O$ OO$ OO$ $
P/PP
PPPPc                  B   t           dz  dz  } |                                 st                      S 	 t          j        |                     d                    }n*# t          j        t          f$ r t                      cY S w xY wt                      }|                    d          pi 	                                D ]c}t          |t                    rL|                    d          }|r5|                    t          |                              d                     d|S )aq  Return install paths recorded in the skills-hub lock, as POSIX strings.

    Hub-installed skills are owned by the hub (``hermes skills uninstall``),
    never by bundled sync. Rename recovery must not move them even when their
    content happens to match a bundled origin hash, or the lock's
    ``install_path`` would point at a directory that no longer exists.
    r   r   r   r6   r   r   r   )rJ   r9   r*   r   r  r:   r  r>   r   r   r   r   r,   rf   r<   )r  rq   pathsr   r   s        r   _read_hub_install_pathsr  7  s    V#k1I uuz)--w-??@@ '*   uueeE((;''-25577 8 8eT"" 	8 99^44L 8		#l++11#66777Ls   (A $B Bc                  $   i } t                                           s| S t                               d          D ]W}t          |          r|j        }t          ||j                  }|                     |g                               |           X| S )zIndex every skill in the user's tree by frontmatter name.

    Returns ``{skill_name: [skill_dir, ...]}``. Used by rename recovery to
    locate a bundled skill that upstream moved to a new category/directory.
    r&   )	rJ   r9   r+   r   r   r.   r-   r   r   )r   r1   r2   r-   s       r   _index_active_skillsr  O  s     $&E $$Z00 5 5!(++ 	O	).99r"")))4444Lr   r   origin_hashr   active_index	hub_pathsc                    |sdS |                     | g           D ]}||k    s|                                s	 |                    t                                                    }n# t
          $ r Y Xw xY w||v rat          |          |k    rI|sFt          d|  d|                    t                                                     d| d|  d	           	 |j        	                    dd           t          j        t          |          t          |                     n7# t          t          f$ r# t                              d	||d
           Y  dS w xY wt                              d||           |sCt          d|  d| d|                    t                                                     d           |c S dS )uI  Move a bundled skill's stale copy to its new canonical path.

    When upstream RENAMES or RECATEGORIZES a bundled skill, the manifest key
    (frontmatter name) still matches but ``dest`` is a brand-new path that does
    not exist yet. Without recovery, ``sync_skills()`` falls through to its
    "in manifest but not on disk" branch and misreads the skill as
    *user-deleted*: the old directory is stranded forever and never receives
    another update.

    A stale copy is only moved when it is byte-identical to ``origin_hash`` —
    the hash recorded the last time sync wrote that skill — which proves the
    directory is the copy *we* placed there rather than the user's own work.
    Anything else (user-edited, hub-installed) is left untouched.

    Returns the relative source path when a move happened, else ``None``.
    N     ⚠ z: upstream moved this skill to z, but your modified copy at uD    was kept — it will not receive updates. Run `hermes skills reset z( --restore` to move to the new location.TrQ   z)Could not relocate renamed skill %s -> %sr^   z)Relocated renamed bundled skill: %s -> %su     → z (moved u    → ))r   r  r   rJ   r   r   r   r  r   ra   r   r   rf   r>   r?   ro   warninginfo)r   r  r   r  r  r   r   r   s           r   _recover_renamed_skillr  a  s8   0  t!%%j"55 # #	I$4$4$6$6	''
33<<>>CC 	 	 	H	 )Y;..  4Z 4 4''
33<<>>4 4(+4 4 :D4 4 4   	KdT:::KID		2222! 	 	 	NN;Y     444	 	?DQQQ 	ec:ccscc9I9I*9U9U9^9^9`9`cccddd


4s$   ,A''
A43A4AD%%/EEc                    t           t          z                                  r| st          d           g g dg g dg ddS t	                      }|                                sg g dg g g dg dS t
                              dd           t                      }t          |          }d |D             }t                      }t                      }g }d}d}	g }
g }g }g }g }d}|D ]\  }}||v r|                    |            t          ||          }t          |          }|                    d	          }|                                r|                                s	 |j                            dd           t!          j        t%          |          t%          |                     t&                              d
|           n4# t*          t,          f$ r  t&                              d|d           Y nw xY w|                                so||v rk|t1                      }t3                      }	t5          ||                    |d          |||	pt9                      |           }|r|                    |           ||v r|                    |           |dz  }| st          d| d           |                                rLt          |          |k    r9t;          |           | st          d|            |                    |d           0||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          }|r||k    r|dz  }Qt          |          }|s|||<   ||k    r|dz  }n|dz  }ztA          ||          r,|                    |           | st          d| d           ||k    r	 |                    d	          }|                                rt;          |           t!          j        t%          |          t%          |                     	 t!          j        ||           |||<   |                    |           | st          d| d           	 t;          |           n4# t*          t,          f$ r  t&          !                    d|d           Y nw xY wn# t*          t,          f$ r |                                r|                                rE	 t;          |           n4# t*          t,          f$ r  t&                              d|d           Y nw xY w|                                s/t!          j        t%          |          t%          |                      w xY wx# t*          t,          f$ r"}| st          d| d|            Y d}~d}~ww xY w|dz  }|dz  }tE          t9          |#                                          |z
            }|D ]}||= |$                    d          D ]}|%                    |          }t
          |z  }|                                sm	 |j                            dd           t!          j&        ||           h# t*          t,          f$ r&}t&          !                    d ||           Y d}~d}~ww xY wtO          |           tQ          | !          }|
||||||tS          |          ||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)
    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"  rQ   c                     h | ]\  }}|S r   r   )rV   r-   rB   s      r   r   zsync_skills.<locals>.<setcomp>  s    888gdAT888r   Nz.bakz#Recovered orphaned skill backup: %sz*Could not recover orphaned skill backup %sr^   r'   ry   u     ⇢ z7 (deferred to external_dirs, not written to local tree)u     ✓ removed stale shadow of r  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 : 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$  	relocatedr!  r"  shadowed_by_external)*HERMES_HOMENO_BUNDLED_SKILLS_MARKERr9   r  r   rJ   ra   rD   r   rN   r4   r   r   r   with_suffixr   r   r   rf   ro   r  r>   r?   r  r  r  r  r   r*   _rmtree_writablepopr   _is_tracked_user_modificationrp   rc   keysr+   r   copy2rv   r   r   ) r   r   manifestbundled_skillsbundled_namesr$  external_indexr(  r  r  r  r  r  suppressed_skippedr'  r  r   	skill_srcr   bundled_hash_orphan_moved_fromru   r  	user_hashbackupr   r-   desc_mdr   	dest_descr"  s                                    r   sync_skillsr>    s	    	..6688 
 	`^___RAB.0T
 
 	
 #$$K 
RABbST.0
 
 	
 TD111H-k::N88888M'))J022N&( 59L$(IFGM$&IG!/  
I ##%%j111%i== ++ ""6**>> 		DKKMM 		!!$!>>>CLL#d))444A7KKKKW%   @'!       {{}} 	-x!7!7#355355	0Z,,"SUU K  -  ,,,''
 !''
333qLG 1Z 1 1 1   {{}} /4L!@!@ &&& IG:GGHHHZ...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
 [[]] P	",,z266K  |{::1!$I 	 (1$,,qLGG qLG,[)DD $$Z000 IGGGGHHH {**'I!--f55F
 }} 1(000KD		3v;;777	4888/;,z222$ C!"A:"A"A"ABBB^,V4444 '1 ^ ^ ^"LL)EvX\L]]]]]^#W-    "==?? 
D#{{}} &!&$4T$:$:$:$:(/'9 !& !& !&$*NN(X(,t %3 %& %& %& %& %&!&
 $(;;== D &CKKT C C C   ) 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 4  s   A&G.G54G5B*OO4O//O4A'YAVUV.V	VVVY9Y
WY.XY
XAYYZ)ZZ 1]^	#^^	c                     t          |                                           }t                                          }||j        vrt	          d|d|d          ddlfd}t          j        | |           dS )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                            |          |fD ]-}	 t          j        |j                   # t
          $ r Y *w xY w | |           d S N)rg   rL   dirnamechmodS_IRWXUr>   )funcr   r_   r   stats       r   	_on_errorz#_rmtree_writable.<locals>._on_error  st     wu--u5 	 	F....   Us   A
AA)onerror)r   r   rJ   rR   r   rF  r   rmtree)rL   r   skills_rootrG  rF  s       @r   r,  r,    s    ( $ZZ!!F$$&&K &.((,& , , , , ,
 
 	
 KKK     M$	******r   c                 .   t                      }t                      }t          |          }t          |          }| |v }| |v }|s|sddd|  dddS d}|rv|sddd|  dddS t	          ||          |          }	|	                                r?	 t          |	           d	}n,# t          t          f$ r}
dd
d|	 d|
 dddcY d}
~
S d}
~
ww xY w|r|| = t          |           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_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.)rD   r   r   r   r   r9   r,  r>   r?   rv   r>  )r-   r   r1  r   r2  bundled_by_namein_manifest
is_bundleddeleted_user_copyr   ru   rO  rN  r   s                 r   reset_bundled_skillrW    sG   0 H"$$K-k::N>**O("K(J 	
z 	
'ID I I I 
 
 	
   		+U U U U    &od&;[II;;== 	 &&&$(!!W% 	 	 	)M M M M M M #       	  "TN!!! t$$$F 
$ 
;t;;;	 	
TtTTT#W4 W W W 	
 &WOOOs   B   C	1C>C	C	r:  c                 ,    t          |           o|| k    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.  K  s     9k!99r   c                     t                      } | sg S t                      }g }t          |          D ]y\  }}|                     |d          }|st	          ||          }|                                sCt          |t          |                    r|                    |||d           z|	                    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   )ru   s    r   r   z3list_user_modified_bundled_skills.<locals>.<lambda>x  s
    &	 r   r   )
rD   r   r   r   r   r9   r.  r   r   sort)r1  r   modifiedr   r2   r  r   s          r   !list_user_modified_bundled_skillsr_  W  s     H 	"$$KH!9+!F!F  
Ill:r22  	%i=={{}} 	(iooFF 	OO#T)LL   MM))M***Or   c                     	 |                                  }n# t          $ r Y dS w xY wd|v r|dfS 	 ||                    d          fS # 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    Nr   )r   r>   decodeUnicodeDecodeError)rL   rq   s     r   _read_for_diffre  |  s         zz$TzT[[))))   Tzs    
%%A AAc                 H   ddl }t                      }t          t          |                    }|                    |           }|d| ddg d|  ddS t          ||          }|                                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|| ||k    r|                    |	ddd           e||k    rld                    |                    |                    d          |                    d          d|	 d|	                     }|                    |	d|d           |
r|                    |	dd|	 d           |                    |	dd|	 d           t          |          }d| d|||sd|  dn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   NFrM  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   ra  binaryz<binary file differs>)rL   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   r9   r*   r   rc   re  r   rb   unified_diffr;   rY  r   )r-   rq  r   rS  r[  r   
user_filesstock_filesrh  r   in_userin_stock
user_bytes	user_textstock_bytes
stock_texttextr^  s                     r   diff_bundled_skillr|    s4    NNN"$$K3K@@AAO!%%d++KTD T T T

 

 
	
 "+{;;D;;== 
DDDDTDDD
 
 	
 %d++,,J&{3344KEj;.// % %#+%*1CN4#:&&&| 	
I 2:KN;,---| 	 Z  	x 	 J$6 ,,LL!$BYZZ   J&&77$$))4)88(($(77+c^^)C>>	 %   D LL#TJJKKKK 	LL9VQT9V9VWW    LL	;Ts;T;TUU    E{{H U22222TTTTs5zzTTT  r   enabledc                    t           t          z  }|                                }	 | r=t                               dd           |                    dd           | }|rdnd}n|r|                                 |}|rdnd	}n1# t          $ r$}d
d
t          |          d| d| dcY d}~S d}~ww xY wd|t          |          |dS )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).
    TrQ   zThis profile opted out of bundled-skill seeding (`hermes skills opt-out`).
Delete this file to re-enable sync on the next `hermes update`.
r   r6   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*  r9   ra   
write_textrn   r>   rf   )r}  r  existedr	  r   ru   s         r   set_bundled_skills_opt_outr    sU    33FmmooG
 	dT:::T !	     "kG I G G I	 G   G > ? ? >	   
 
 
ES[[JVJJqJJ
 
 	
 	
 	
 	
 	
 	


 7c&kkgVVVs   AB 
B1B,&B1,B1c                  D    t           t          z                                  S )z=Return True if the active profile carries the opt-out marker.)r)  r*  r9   r   r   r   is_bundled_skills_opt_outr  $  s    22::<<<r   dry_runc                 l   t                      }t                      }t          t          |                    }g }g }t	          |                                          D ]\  }}|                    |          }||                    |dd           6t          ||          }	|		                                s
| s||v r||= dt          |	          }
|
|k    r|                    |dd           | r|                    |           	 t          |	           n9# t          t          f$ r%}|                    |d| d           Y d}~d}~ww xY w||v r||= |                    |           | s|rt          |           | rdnd}| dt          |           d	t          |           d
}d||| |dS )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   rp  r  r  r   )rD   r   r   r   rc   rd   r   r   r   r9   r   r,  r>   r?   rv   r   )r  r1  r   rS  rp  r  r-   r  r   r   on_diskru   verbr   s                 r   remove_pristine_bundled_skillsr  )  s#   . H"$$K3K@@AAOGG#HNN$4$455  k!!$'';NND4Z[[\\\%c;77{{}} 	 #tx//TND//k!!NND4JKKLLL 	NN4   	T""""! 	 	 	NND4Ia4I4IJJKKKHHHH	 8t "w "!!!$3>>)DVVGVVs7||VVVGw7w  s   	D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.rA  )F)W__doc__r   r   loggingrg   r   sysr   r   pathlibr   r   stdoutstderr_streamhasattrr   r   	TypeErrorhermes_constantsr   r   r   r(   r   typingr   r   r   r   r   utilsr   	getLogger__name__ro   r)  rJ   r8   r*  r   r"   rf   r4   rD   r*   rN   rv   r.   r   r   r   r   r   r   r   r   rY  r   r   r   r   r   r  r  r  r>  r,  rW  r.  r_  bytesre  r|  r  r  r  r  r@   r   r   rM   MAX_SHOWrb   shownr   r   r   r   r   <module>r     s   .    				  



 ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' 
CJ'  Gww&& 	CCCCI& 	 	 	D	
 ^ ] ] ] ] ] ] ] ] ] 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3            		8	$	$ o8#
00 0 K$ K K K KU4 U U U U
SX    :S#X    6    4`T#s(^ ` ` ` `Bt s s    *$ 4c4i8H3I    2d  $     #     T c     c    	$T 	$c 	$ 	$ 	$ 	$tCsC~)>$>?    :$ T c     CH D D D# D4 DD D D D DNT#tDz/-B    ( 8< d3T
?34 d^   .m m m$s) m m m m`S    0d3T
?3    $??? ? sDJ'	?
 3x? ? c]? ? ? ?DQ Qt Q Q Q Q Qh.+4 .+D .+ .+ .+ .+b`P `Pc `PD `PT `P `P `P `PF	:s 	:s 	:t 	: 	: 	: 	:"4: " " " "J %#(F"G    &`S `T ` ` ` `F/W /W /W /W /W /Wd=4 = = = =
C CD CT C C C CL 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' s   A  A*)A*