
    (Gjn                     Z   U d Z ddlZddlZddlZddlZddlmZ ddlmZm	Z	m
Z
mZmZmZ ddlmZmZmZ  ej        e          Zdddd	Z ed
          Z ed          ZdefdZdefdZdadefdZdedee	eef         ef         fdZde	eef         defdZ  eh d          Z!i Z"e	eef         e#d<   dedefdZ$de	eef         defdZ%i Z&e	eee'e'f         e	eef         f         e#d<   d7dZ(de	eef         fdZ)d8dedz  dee         fdZ*dee         fdZ+i Z,e	eee'f         e
e         f         e#d<   d7d Z-de
e         fd!Z.de
e         fd"Z/defd#Z0defd$Z1de	eef         de	ee
f         fd%Z2de	eef         de
e	eef                  fd&Z3de
e	eef                  fd'Z4d(Z5d)e	eef         d*efd+Z6d,e
e	eef                  de	eef         fd-Z7de	eef         defd.Z8d/ed0efd1Z9 ej:        d2          Z;d3edeee         ef         fd4Z<d5ee         defd6Z=dS )9a  Lightweight skill metadata utilities shared by prompt_builder and skills_tool.

This module intentionally avoids importing the tool registry, CLI config, or any
heavy dependency chain.  It is safe to import at module level without triggering
tool registration or provider resolution.
    N)Path)AnyDictListOptionalSetTuple)get_config_pathget_skills_dir	is_termuxdarwinlinuxwin32)macosr   windows)z.gitz.githubz.hubz.archivez.venvvenvnode_moduleszsite-packages__pycache__z.toxz.noxz.pytest_cachez.mypy_cachez.ruff_cache)
references	templatesassetsscriptsreturnc                     	 | j         }n3# t          $ r& ddlm}  |t	          |                     j         }Y nw xY wt          d |D                       pt          |           S )ax  True if *path* should be skipped by active skill scanners.

    Use this on every ``SKILL.md`` path produced by direct ``rglob`` scans to
    prune dependency, virtualenv, VCS, cache, and progressive-disclosure
    support-package paths. Centralising the check here keeps every
    skill-scanning site in sync with the shared exclusion set.

    Accepts a Path or string.
    r   )PurePathc              3   (   K   | ]}|t           v V  d S N)EXCLUDED_SKILL_DIRS).0parts     7/home/rurouni/.hermes/hermes-agent/agent/skill_utils.py	<genexpr>z)is_excluded_skill_path.<locals>.<genexpr>D   s(      ==tt**======    )partsAttributeErrorpathlibr   stranyis_skill_support_path)pathr$   r   s      r!   is_excluded_skill_pathr+   5   s    *
 * * *$$$$$$T##)* ==u===== AVB B s   
 -::c                 *   t          | t                    r| nt          t          |                     }|j        }t	          |dd                   D ]@\  }}|t
          vs|dk    rt          |d|          }|dz                                  r dS AdS )a  True if *path* is under a support dir of an actual skill root.

    ``references/``, ``templates/``, ``assets/``, and ``scripts/`` are
    progressive-disclosure support areas when they sit directly inside a skill
    directory containing ``SKILL.md``. They are not active discovery roots for
    standalone skills. A preserved package such as
    ``some-skill/references/old-skill-package/SKILL.md`` is documentation data
    unless the caller explicitly loads it via ``file_path``.

    Legitimate categories or skill names such as ``skills/scripts/foo`` remain
    discoverable because their ``scripts`` component is not directly under a
    directory that contains ``SKILL.md``.
    Nr   SKILL.mdTF)
isinstancer   r'   r$   	enumerateSKILL_SUPPORT_DIRSexists)r*   path_objr$   idxr    
skill_roots         r!   r)   r)   I   s     "$--Btt4D		??HNE uSbSz**  	T)))SAXX5#;'
#++-- 	44	5r#   contentc                     t           ,ddlt          dd          pj        dt          ffd}|a t          |           S )z7Parse YAML with lazy import and CSafeLoader preference.Nr   CSafeLoadervaluec                 2                         |           S )N)Loader)load)r9   loaderyamls    r!   _loadzyaml_load.<locals>._loadq   s    99U69222r#   )_yaml_load_fnr>   getattr
SafeLoaderr'   )r6   r?   r=   r>   s     @@r!   	yaml_loadrC   i   sl     }d33Ft	3 	3 	3 	3 	3 	3 	3 	3 !!!r#   c                 X   i }| }|                      d          s||fS t          j        d| dd                   }|s||fS | d|                                dz            }| |                                dz   d         }	 t          |          }t          |t                    r|}n# t          $ rt |	                                
                    d          D ]I}d|vr|
                    dd          \  }}|	                                ||	                                <   JY nw xY w||fS )zParse YAML frontmatter from a markdown string.

    Uses yaml with CSafeLoader for full YAML support (nested metadata, lists)
    with a fallback to simple key:value splitting for robustness.

    Returns:
        (frontmatter_dict, remaining_body)
    z---z
\n---\s*\n   N
:   )
startswithresearchstartendrC   r/   dict	Exceptionstripsplit)	r6   frontmatterbody	end_matchyaml_contentparsedlinekeyr9   s	            r!   parse_frontmatterrY   {   s[    #%KDe$$ !D  	-55I !D  1y001445L9==??Q&(()D
5<((fd## 	! K 5 5 5 &&((..t44 	5 	5D$C++JC',{{}}K		$$		5 	55 s    &B' 'A;D%$D%rR   c                    |                      d          }|sdS t          |t                    s|g}t          j        }t                      }|D ]|}t          |                                                                          }t                               ||          }|
                    |          r dS |r	|dk    r dS |r|dv r dS }dS )av  Return True when the skill is compatible with the current OS.

    Skills declare platform requirements via a top-level ``platforms`` list
    in their YAML frontmatter::

        platforms: [macos]          # macOS only
        platforms: [macos, linux]   # macOS and Linux

    If the field is absent or empty the skill is compatible with **all**
    platforms (backward-compatible default).

    Termux note: on Termux/Android, ``sys.platform`` is ``"linux"`` on
    older Pythons but became ``"android"`` on Python 3.13+. Termux is a
    Linux userland riding on the Android kernel, so skills tagged
    ``linux`` are treated as compatible in Termux regardless of which
    ``sys.platform`` value Python reports. Individual Linux commands
    inside a skill may still misbehave (no systemd, BusyBox utils, no
    apt/dnf, etc.) but that is on the skill, not on platform gating.
    	platformsTr   )termuxandroidF)getr/   listsysplatformr   r'   lowerrP   PLATFORM_MAPrI   )rR   r[   currentrunning_in_termuxra   
normalizedmappeds          r!   skill_matches_platformrh      s    ( ,,I ti&&  K	lG!  ]]((**0022
!!*j99f%% 	44
  	7!2!244 	+@!@!@445r#   >   s6dockerkanban_ENV_DETECT_CACHEenvc                    | t           v rt           |          S d}| dk    r\t          j        d          st          j        d          rd}n	 ddlm} t           |                      }n# t          $ r d}Y nrw xY w| dk    r$	 dd	lm}  |            }nV# t          $ r d}Y nHw xY w| d
k    r>t          j	        
                    d          pt          j	        
                    d          }|t           | <   |S )zReturn True when the named runtime environment is currently active.

    Cached per process. Unknown env names return True (fail-open: never hide a
    skill because of a tag we don't understand).
    Trk   HERMES_KANBAN_TASKHERMES_KANBAN_BOARDr   )_profile_has_kanban_toolsetFrj   )is_containerri   z/run/s6z/package/admin/s6-overlay)rl   osgetenvtools.kanban_toolsrq   boolrO   hermes_constantsrr   r*   isdir)rm   resultrq   rr   s       r!   _detect_environmentrz      sF     %%F
h 9)** 	bi8M.N.N 	FFJJJJJJ99;;<<   		555555!\^^FF 	 	 	FFF		
 y)) 
RW]]'.
 .
 $cMs$   A) )A87A8B B"!B"c                    |                      d          }|sdS t          |t                    s|g}|D ]V}t          |                                                                          }|s8|t          vr dS t          |          r dS WdS )uR  Return True when the skill is relevant to the current runtime environment.

    Skills may declare an ``environments`` list in their YAML frontmatter::

        environments: [kanban]        # only relevant when kanban is active
        environments: [s6]            # only relevant inside the s6 Docker image
        environments: [docker]        # only relevant inside any container

    If the field is absent or empty the skill is relevant in **all**
    environments (backward-compatible default).

    This is an OFFER-time filter: it controls whether a skill shows up in the
    skills index / autocomplete / slash-command list. It is intentionally NOT
    enforced by ``skill_view`` or ``--skills`` preloading — an explicit load is
    explicit consent, and load-bearing force-loads (e.g. a dispatcher pinning
    a task to a specialist skill via ``--skills``) must always succeed
    regardless of how the offer surfaces filter the skill.

    A skill matches when ANY of its declared environments is currently active
    (OR semantics, mirroring ``platforms``). Unknown env tags fail open.
    environmentsTF)r^   r/   r_   r'   rb   rP   _KNOWN_ENVIRONMENTSrz   )rR   r|   rm   rf   s       r!   skill_matches_environmentr~     s    , ??>22L tlD)) &$~  XX^^%%++--
 	00044z** 	44	5r#   _RAW_CONFIG_CACHEc                  8    t                                            dS )u/   Test hook — drop the shared raw config cache.N)r   clear r#   r!   _raw_config_cache_clearr   9  s    r#   c                  B   t                      } |                                 si S 	 |                                 }t          |           |j        |j        f}n# t          $ r d}Y nw xY w|t                              |          }||S 	 t          | 
                    d                    }n5# t          $ r(}t                              d| |           i cY d}~S d}~ww xY wt          |t                    si S |#t                                           |t          |<   |S )a  Read config.yaml with a shared mtime+size keyed cache.

    This module intentionally avoids importing ``hermes_cli.config`` on the
    skill prompt/build path. A tiny local cache gives the same repeated-read
    win without pulling the heavier CLI config stack into startup.
    Nutf-8encodingz"Could not read skill config %s: %s)r
   r2   statr'   st_mtime_nsst_sizeOSErrorr   r^   rC   	read_textrO   loggerdebugr/   rN   r   )config_pathr   	cache_keycachedrV   es         r!   _load_raw_configr   >  sN    "##K 	!!%%t'7F		   			 "&&y11M;00'0BBCC   9;JJJ						 fd## 	!!!'-)$Ms/   0A A&%A&
#B. .
C 8CC C ra   c                    t                      }|st                      S |                    d          }t          |t                    st                      S ddlm} | pt          j        d          p
 |d          }t          |                    d                    }|r>|                    d          pi                     |          }||t          |          z  S |S )a
  Read disabled skill names from config.yaml.

    Args:
        platform: Explicit platform name (e.g. ``"telegram"``).  When
            *None*, resolves from ``HERMES_PLATFORM`` or
            ``HERMES_SESSION_PLATFORM`` env vars.  Returns the global
            disabled list, unioned with the platform-specific list when a
            platform is resolved (a globally-disabled skill stays disabled
            on every platform).

    Reads the config file directly (no CLI config imports) to stay
    lightweight.
    skillsr   )get_session_envHERMES_PLATFORMHERMES_SESSION_PLATFORMdisabledplatform_disabled)
r   setr^   r/   rN   gateway.session_contextr   rs   rt   _normalize_string_set)ra   rV   
skills_cfgr   resolved_platformglobal_disabledr   s          r!   get_disabled_skill_namesr   a  s     F uuH%%Jj$'' uu777777 	69&''	6?455 
 ,JNN:,F,FGGO N'^^,?@@FBKK
 
 ("%:;L%M%MMMr#   c                 j    | t                      S t          | t                    r| g} d | D             S )Nc                     h | ]D}t          |                                          #t          |                                          ES r   )r'   rP   )r   vs     r!   	<setcomp>z(_normalize_string_set.<locals>.<setcomp>  s9    ===qc!ffllnn=CFFLLNN===r#   )r   r/   r'   )valuess    r!   r   r     s=    ~uu&# ==F====r#   _EXTERNAL_DIRS_CACHEc                  T    t                                            t                       dS )u(   Test hook — drop the in-process cache.N)r   r   r   r   r#   r!   _external_dirs_cache_clearr     s&       r#   c                  P   t                      } |                                 sg S 	 |                                 }t          |           |j        f}n# t
          $ r d}Y nw xY w|+t                              |          }|t          |          S t                      }|sg S |                    d          }t          |t                    sg S |                    d          }|sg }|t          |          t          |<   |S t          |t                    r|g}t          |t                    sg S ddlm}  |            }	t                                                      }
t!                      }g }|D ]}t          |                                          }|s't$          j                            t$          j                            |                    }t-          |          }|                                s|	|z                                  }n|                                }||
k    r||v r|                                r+|                    |           |                    |           t6                              d|           |t          |          t          |<   |S )u3  Read ``skills.external_dirs`` from config.yaml and return validated paths.

    Each entry is expanded (``~`` and ``${VAR}``) and resolved to an absolute
    path.  Only directories that actually exist are returned.  Duplicates and
    paths that resolve to the local ``~/.hermes/skills/`` are silently skipped.

    Cached in-process, keyed on ``config.yaml`` mtime — the function is
    called once per skill during banner / tool-registry scans, and YAML
    parsing a non-trivial config dominates ``hermes`` cold-start time
    when the cache is absent.
    Nr   external_dirsr   )get_hermes_homez0External skills dir does not exist, skipping: %s)r
   r2   r   r'   r   r   r   r^   r_   r   r/   rN   rw   r   r   resolver   rP   rs   r*   
expanduser
expandvarsr   is_absoluteis_diraddappendr   r   )r   r   r   r   rV   r   raw_dirsry   r   hermes_homelocal_skillsseenentryexpandedps                  r!   get_external_skills_dirsr     s    "##K 	!!&)+&6&68H%I		   			 %)))44<<F 	H%%Jj$'' 	~~o..H  .26ll +(C   :h%% 	000000!/##K!##++--LeeDF P PE

  "" 	7%%bg&8&8&?&?@@NN}} 	q))++AA		A9988:: 	PHHQKKKMM!LLKQOOOO*.v,,Y'Ms   *A A A c                  f    t                      g} |                     t                                 | S )u   Return all skill directories: local ``~/.hermes/skills/`` first, then external.

    The local dir is always first (and always included even if it doesn't exist
    yet — callers handle that).  External dirs follow in config order.
    )r   extendr   )dirss    r!   get_all_skills_dirsr     s0     DKK(**+++Kr#   c                 0   t          | t                    r| nt          t          |                     }	 |                                                                S # t
          t          f$ r) |                                                                cY S w xY wr   )r/   r   r'   r   r   r   RuntimeErrorabsolute)r*   r3   s     r!   _resolve_for_skill_ownershipr     s    !$--Btt4D		??H0""$$,,...\" 0 0 0""$$--/////0s   %A 7BBc                     t          |           }t                      D ]9}t          |          }	 |                    |            dS # t          $ r Y 6w xY wdS )a  Return True when ``path`` lives under a configured external skills dir.

    ``skills.external_dirs`` are externally owned: Hermes can discover and view
    their skills, and foreground user-directed tool calls may still edit them,
    but autonomous lifecycle maintenance must treat them as read-only. This
    helper centralizes the ownership boundary so curator/reporting/tool paths do
    not each need to re-interpret the config.
    TF)r   r   relative_to
ValueError)r*   	candidaterootresolved_roots       r!   is_external_skill_pathr     sx     -T22I(**  4T::	!!-00044 	 	 	H	5s   A
AAc                 d   |                      d          }t          |t                    si }|                     d          pi }t          |t                    si }|                     dg           |                     dg           |                     dg           |                     dg           dS )z>Extract conditional activation fields from parsed frontmatter.metadatahermesfallback_for_toolsetsrequires_toolsetsfallback_for_toolsrequires_tools)r   r   r   r   )r^   r/   rN   )rR   r   r   s      r!   extract_skill_conditionsr     s    z**Hh%% \\(##)rFfd## !',CR!H!H#ZZ(;R@@$jj)=rBB **%5r::	  r#   c                    |                      d          }t          |t                    sg S |                     d          }t          |t                    sg S |                     d          }|sg S t          |t                    r|g}t          |t                    sg S g }t	                      }|D ]3}t          |t                    st          |                     dd                                                    }|r||v rUt          |                     dd                                                    }|s||d}	|                     d          }
|
|
|	d<   |                     d
          }t          |t
                    r,|                                r|                                |	d
<   n||	d
<   |                    |           |                    |	           5|S )a   Extract config variable declarations from parsed frontmatter.

    Skills declare config.yaml settings they need via::

        metadata:
          hermes:
            config:
              - key: wiki.path
                description: Path to the LLM Wiki knowledge base directory
                default: "~/wiki"
                prompt: Wiki directory path

    Returns a list of dicts with keys: ``key``, ``description``, ``default``,
    ``prompt``.  Invalid or incomplete entries are silently skipped.
    r   r   configrX    description)rX   r   defaultNprompt)	r^   r/   rN   r_   r   r'   rP   r   r   )rR   r   r   rawry   r   itemrX   descr   r   prompt_texts               r!   extract_skill_config_varsr   1  s     z**Hh%% 	\\(##Ffd## 	
**X

C 	#t ec4   	#%FD  $%% 	$((5"%%&&,,.. 	cTkk488M2..//5577 	!
 !
 ((9%%&E)hhx((k3'' 	#K,=,=,?,? 	#)//11E(OO"E(OeMr#   c                  r   g } t                      }t                      }t                      D ]}|                                st	          |d          D ]}	 |                    d          }t          |          \  }}n# t          $ r Y 8w xY w|                    d          p|j	        j
        }t          |          |v rot          |          st          |          }	|	D ]N}
|
d         |vrBt          |          |
d<   |                     |
           |                    |
d                    O
| S )aY  Scan all enabled skills and collect their config variable declarations.

    Walks every skills directory, parses each SKILL.md frontmatter, and returns
    a deduplicated list of config var dicts.  Each dict also includes a
    ``skill`` key with the skill name for attribution.

    Disabled and platform-incompatible skills are excluded.
    r.   r   r   namerX   skill)r   r   r   r   iter_skill_index_filesr   rY   rO   r^   parentr   r'   rh   r   r   r   )all_vars	seen_keysr   
skills_dir
skill_filer   rR   _
skill_nameconfig_varsvars              r!   discover_all_skill_config_varsr   l  si    &(HUUI'))H)++ . .
  "" 	0ZHH 	. 	.J **G*<<!23!7!7QQ    %00JJ4E4JJ:(**)+66 3K@@K" . .u:Y..#&z??CLOOC(((MM#e*---	.	.( Os   (B
BBzskills.configr   
dotted_keyc                     |                     d          }| }|D ]&}t          |t                    r||v r	||         }$ dS |S )zPWalk a nested dict following a dotted key.  Returns None if any part is missing..N)rQ   r/   rN   )r   r   r$   rd   r    s        r!   _resolve_dotpathr     sZ    S!!EG  gt$$ 	dmGG44Nr#   r   c                    t                      }i }| D ]}|d         }t           d| }t          ||          }|)t          |t                    r*|                                s|                    dd          }t          |t                    rDd|v sd|v r<t          j        	                    t          j        
                    |                    }|||<   |S )aV  Resolve current values for skill config vars from config.yaml.

    Skill config is stored under ``skills.config.<key>`` in config.yaml.
    Returns a dict mapping **logical** keys (as declared by skills) to their
    current values (or the declared default if the key isn't set).
    Path values are expanded via ``os.path.expanduser``.
    rX   r   Nr   r   ~z${)r   SKILL_CONFIG_PREFIXr   r/   r'   rP   r^   rs   r*   r   r   )r   r   resolvedr   logical_keystorage_keyr9   s          r!   resolve_skill_config_valuesr     s     F!H & &%j,<<{<< 55=Zs33=EKKMM=GGIr**E eS!! 	Bse||tu}}G&&rw'9'9%'@'@AAE %Or#   c                     |                      dd          }|sdS t          |                                                              d          }t          |          dk    r|dd         dz   S |S )z8Extract a truncated description from parsed frontmatter.r   r   z'"<   N9   z...)r^   r'   rP   len)rR   raw_descr   s      r!   extract_skill_descriptionr     sn    }b11H rx==  &&u--D
4yy2~~CRCy5  Kr#   r   filenamec              #      K   g }t          j         d          D ]F\  }}}d|v fd|D             |dd<   ||v r%|                    t          |          |z             Gt	          | fd          D ]}|V  dS )a  Walk skills_dir yielding sorted paths matching *filename*.

    Excludes Hermes metadata, VCS, virtualenv/dependency, cache, and skill
    support directories. Support directories (references/templates/assets/
    scripts) can contain arbitrary markdown and even archived package
    ``SKILL.md`` files, but they are progressive-disclosure data loaded through
    ``skill_view(..., file_path=...)`` rather than active skill roots.
    T)followlinksr.   c                 <    g | ]}|t           vrr	|t          v |S r   )r   r1   )r   dhas_skill_mds     r!   
<listcomp>z*iter_skill_index_files.<locals>.<listcomp>  sE     
 
 
+++! ,&'+=&=&=  '>&=&=r#   Nc                 H    t          |                                         S r   )r'   r   )r   r   s    r!   <lambda>z(iter_skill_index_files.<locals>.<lambda>  s    c!--
2K2K.L.L r#   )rX   )rs   walkr   r   sorted)r   r   matchesr   r   filesr*   r  s   `      @r!   r   r     s       GWZTBBB 	2 	2dE!U*
 
 
 

 
 
QQQ uNN4::0111w$L$L$L$LMMM  



 r#   z^[a-zA-Z0-9_-]+$r   c                 X    d| vrd| fS t          |                     dd                    S )z~Split ``'namespace:skill-name'`` into ``(namespace, bare_name)``.

    Returns ``(None, name)`` when there is no ``':'``.
    rG   NrH   )tuplerQ   )r   s    r!   parse_qualified_namer    s3    
 $TzC##$$$r#   r   c                 X    | sdS t          t                              |                     S )zDCheck whether *candidate* is a valid namespace (``[a-zA-Z0-9_-]+``).F)rv   _NAMESPACE_REmatch)r   s    r!   is_valid_namespacer    s+     u##I..///r#   )r   Nr   )>__doc__loggingrs   rJ   r`   r&   r   typingr   r   r   r   r   r	   rw   r
   r   r   	getLogger__name__r   rc   	frozensetr   r1   rv   r+   r)   r@   r'   rC   rY   rh   r}   rl   __annotations__rz   r~   r   intr   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   compiler  r  r  r   r#   r!   <module>r     sV      				 				 



       8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 G G G G G G G G G G		8	$	$
    i  . YOPP D    (4    : "s " " " "$"s "uT#s(^S-@'A " " " "P)S#X )4 ) ) ) )l  i : : :;; %' 4T	? ' ' '+S +T + + + +\$4S> $d $ $ $ $T AC 4c3m,d38n<= B B B   
 $sCx.        F# #sTz #SX # # # #L>SX > > > >  ;= d5c?DJ67 < < <   P$t* P P P PfT$Z    0$ 0 0 0 0D    .$sCx. T#t)_    (84S> 8d4S>>R 8 8 8 8v$T#s(^(< $ $ $ $T & 	T#s(^ 	 	 	 	 	d38n%	#s(^   B4S> c    t s    4 
.//%s %uXc]C-?'@ % % % %0(3- 0D 0 0 0 0 0 0r#   