
    `gjaz                        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de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<   d;dZ)de	eef   fdZ*d<d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!<   d;d"Z.de
e   fd#Z/de
e   fd$Z0d%edefd&Z1defd'Z2defd(Z3de	eef   de	ee
f   fd)Z4de	eef   de
e	eef      fd*Z5de
e	eef      fd+Z6d,Z7d-e	eef   d.efd/Z8d0e
e	eef      de	eef   fd1Z9de	eef   defd2Z:d3ed4efd5Z; ejx                  d6      Z=d7edeee   ef   fd8Z>d9ee   defd:Z?y)=a  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                   }t        d |D              xs t        |       S # t        $ r$ ddlm}  |t	        |             j                   }Y Kw xY w)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   y wN)EXCLUDED_SKILL_DIRS).0parts     D/root/.hermes/venv/lib/python3.12/site-packages/agent/skill_utils.py	<genexpr>z)is_excluded_skill_path.<locals>.<genexpr>D   s     =tt**=s   )partsAttributeErrorpathlibr   stranyis_skill_support_path)pathr#   r   s      r!   is_excluded_skill_pathr*   5   s^    *

 =u== AVB   *$T#))*s   - *AAc                     t        | t              r| nt        t        |             }|j                  }t	        |dd       D ]3  \  }}|t
        vs|dk(  rt        |d|  }|dz  j                         s3 y y)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     "$-t4D	?HNNE uSbz* 	T))SAX5#;'
#++-     contentc                     t         .ddlt        dd      xs j                  dt        ffd}|a t        |       S )z7Parse YAML with lazy import and CSafeLoader preference.Nr   CSafeLoadervaluec                 *    j                  |       S )N)Loader)load)r9   loaderyamls    r!   _loadzyaml_load.<locals>._loadq   s    99U6922r5   )_yaml_load_fnr>   getattr
SafeLoaderr&   )r6   r?   r=   r>   s     @@r!   	yaml_loadrC   i   sC     }d3Ft	3 	3 !!r5   c                    i }| j                  d      r| dd } | }| j                  d      s||fS t        j                  d| dd       }|s||fS | d|j                         dz    }| |j	                         dz   d }	 t        |      }t        |t              r|}||fS # t        $ re |j                         j                  d      D ]=  }d|vr|j                  dd      \  }}|j                         ||j                         <   ? Y ||fS w xY w)	u  Parse 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.

    A single leading UTF-8 BOM (U+FEFF) is stripped before parsing. Windows
    GUI editors (Notepad, PowerShell ``>``) prepend one when saving a SKILL.md
    as UTF-8, and ``read_text(encoding="utf-8")`` preserves it (only
    ``utf-8-sig`` strips it). Left in place, the BOM defeats the ``---`` fence
    check below and the whole frontmatter is silently discarded — name,
    description, ``platforms`` gating, env-var setup, and conditional
    activation all vanish. See CONTRIBUTING.md "File encoding".

    Returns:
        (frontmatter_dict, remaining_body)
    u   ﻿   Nz---z
\n---\s*\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   {   s6   " #%K (#!"+De$D  		-5ID  1y0145L9==?Q&()D
5<(fd# K   5 &&(..t4 	5D$C+JC',{{}K		$		5 5s   <B A(D
D	platformsc                 6   | syt        | t              s| g} t        j                  }t	               }| D ]d  }t        |      j                         j                         }t        j                  ||      }|j                  |      r y|r|dk(  r y|s_|dv sd y y)z?Return True when *platforms* is compatible with the current OS.Tr   )termuxandroidF)r.   listsysplatformr   r&   lowerrP   PLATFORM_MAPgetrI   )rZ   currentrunning_in_termuxr`   
normalizedmappeds         r!   skill_matches_platform_listrh      s    i&K	llG! ]((*002
!!*j9f%
 7!2+@!@ r5   rR   c                 6    t        | j                  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.
    rZ   )rh   rc   )rR   s    r!   skill_matches_platformrj      s    ( '{{'CDDr5   >   s6dockerkanban_ENV_DETECT_CACHEenvc                    | t         v r	t         |    S d}| dk(  rEt        j                  d      st        j                  d      rd}nq	 ddlm} t         |             }nY| dk(  r	 dd	lm}  |       }nE| d
k(  r@t        j                  j                  d      xs t        j                  j                  d      }|t         | <   |S # t        $ r d}Y w xY w# t        $ r d}Y )w xY w)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).
    Trm   HERMES_KANBAN_TASKHERMES_KANBAN_BOARDr   )_profile_has_kanban_toolsetFrl   )is_containerrk   z/run/s6z/package/admin/s6-overlay)rn   osgetenvtools.kanban_toolsrs   boolrO   hermes_constantsrt   r)   isdir)ro   resultrs   rt   s       r!   _detect_environmentr|      s      %%F
h 99)*bii8M.NFJ9;< 
	5!^F 

 y) 
RWW]]'.
 $cM'    	F	s$   C $C CCC! C!c                     | j                  d      }|syt        |t              s|g}|D ]C  }t        |      j	                         j                         }|s-|t        vr yt        |      sC y y)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)rc   r.   r^   r&   ra   rP   _KNOWN_ENVIRONMENTSr|   )rR   r~   ro   rf   s       r!   skill_matches_environmentr     ss    , ??>2LlD)$~ X^^%++-
00z* r5   _RAW_CONFIG_CACHEc                  ,    t         j                          y)u/   Test hook — drop the shared raw config cache.N)r   clear r5   r!   _raw_config_cache_clearr   I  s    r5   c                     t               } | j                         si S 	 | j                         }t        |       |j                  |j
                  f}|t        j                  |      }||S 	 t        | j                  d            }t        |t              si S |t        j!                          |t        |<   |S # t        $ r d}Y ww xY w# t        $ r#}t        j                  d| |       i cY d}~S d}~ww xY w)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
   r1   statr&   st_mtime_nsst_sizeOSErrorr   rc   rC   	read_textrO   loggerdebugr.   rN   r   )config_pathr   	cache_keycachedrV   es         r!   _load_raw_configr   N  s     "#K	!%t'7'7F	 "&&y1M;00'0BC fd#	!'-)$M'  	  9;J	s/   2B: ,C :CC	C7C2,C72C7r`   c                 z   t               }|s
t               S |j                  d      }t        |t              s
t               S ddlm} | xs t        j                  d      xs  |d      }t        |j                  d            }|r4|j                  d      xs i j                  |      }||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   setrc   r.   rN   gateway.session_contextr   ru   rv   _normalize_string_set)r`   rV   
skills_cfgr   resolved_platformglobal_disabledr   s          r!   get_disabled_skill_namesr   q  s     FuH%Jj$'u7 	699&'	645 
 ,JNN:,FGO'^^,?@FBKK
 ("%:;L%MMMr5   c                     | 
t               S t        | t              r| g} | D ch c]5  }t        |      j                         st        |      j                         7 c}S c c}w r   )r   r.   r&   rP   )valuesvs     r!   r   r     sI    ~u&#$*=qc!fllnCFLLN===s   A!A!_EXTERNAL_DIRS_CACHEc                  @    t         j                          t                y)u(   Test hook — drop the in-process cache.N)r   r   r   r   r5   r!   _external_dirs_cache_clearr     s     r5   c                  j   t               } | j                         sg S 	 | j                         }t        |       |j                  f}|"t        j                  |      }|t        |      S t               }|sg S |j                  d      }t        |t              sg S |j                  d      }|sg }|t        |      t        |<   |S t        |t              r|g}t        |t              sg S ddlm}  |       }	t               j                         }
t!               }g }|D ]  }t        |      j#                         }|st$        j&                  j)                  t$        j&                  j+                  |            }t-        |      }|j/                         s|	|z  j                         }n|j                         }||
k(  r||v r|j1                         r#|j3                  |       |j5                  |       t6        j9                  d|        |t        |      t        |<   |S # t
        $ r d}Y w xY w)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
   r1   r   r&   r   r   r   rc   r^   r   r.   rN   ry   r   r   resolver   rP   ru   r)   
expanduser
expandvarsr   is_absoluteis_diraddappendr   r   )r   r   r   r   rV   r   raw_dirsr{   r   hermes_homelocal_skillsseenentryexpandedps                  r!   get_external_skills_dirsr     s    "#K	!&)+&68H8H%I	 %)))4<F	H%Jj$'	~~o.H .26l +(C :h%	0!#K!#++-LeDF PE
  "77%%bgg&8&8&?@N}}q))+A		A988:HHQKMM!LLKQO)P, *.v,Y'Mw  	s   'H# #H21H2c                  N    t               g} | j                  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     s$     DKK(*+Kr5   
identifierc                    | xs dj                         }|s|S t        |      j                         }|j                         s|j	                  d      S 	 ddlm} t        |j                        }|g}	 |j                  t                      |D ]  }	 t        |j                  |            c S  	 t        |j                         j                  |j                                     S # t        $ r t               }Y w xY w# t        $ r Y ~w xY w# t        $ r Y w xY w# t        $ r t         j#                  d|       |cY S w xY w)a  Normalize a skill identifier to a ``skill_view()``-safe relative path.

    Slash commands and cron jobs may store absolute paths to skills that live
    under ``~/.hermes/skills/`` (including via symlinks) or configured
    ``skills.external_dirs``. ``skill_view()`` rejects absolute names for
    security, so callers must translate trusted absolute paths to their
    relative form first.
     /r   )skills_toolu~   Skill identifier %r is an absolute path outside trusted skills roots — passing through unchanged (skill_view will reject it))rP   r   r   r   lstriptoolsr   
SKILLS_DIRrO   r   r   r   r&   relative_to
ValueErrorr   r   r   )r   raw_identifieridentifier_path_skills_toolprimary_roottrusted_rootsroots          r!   normalize_skill_lookup_namer     sM    !&B--/N>*557O&&($$S))(5L334 "NM578  	224899?**,889M9M9OPQQ+  (%'(    		
  N	

 sH   C& 3C? D05D &C<;C<?	D
D	DD!E Ec                     t        | t              r| nt        t        |             }	 |j                         j	                         S # t
        t        f$ r! |j                         j                         cY S w xY wr   )r.   r   r&   r   r   r   RuntimeErrorabsolute)r)   r2   s     r!   _resolve_for_skill_ownershipr   G  sc    !$-t4D	?H0""$,,..\" 0""$--//0s   A -A65A6c                     t        |       }t               D ]   }t        |      }	 |j                  |        y y# t        $ r Y /w xY w)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   r   r   )r)   	candidater   resolved_roots       r!   is_external_skill_pathr   O  sY     -T2I(* 4T:	!!-0	   		s   :	AAc                 $   | j                  d      }t        |t              si }|j                  d      xs i }t        |t              si }|j                  dg       |j                  dg       |j                  dg       |j                  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   )rc   r.   rN   )rR   r   r   s      r!   extract_skill_conditionsr   f  s    z*Hh%\\(#)rFfd#!',CR!H#ZZ(;R@$jj)=rB **%5r:	 r5   c                    | j                  d      }t        |t              sg S |j                  d      }t        |t              sg S |j                  d      }|sg S t        |t              r|g}t        |t              sg S g }t	               }|D ]  }t        |t              st        |j                  dd            j                         }|r||v rDt        |j                  dd            j                         }|sp||d}	|j                  d      }
|
|
|	d<   |j                  d	      }t        |t
              r$|j                         r|j                         |	d	<   n||	d	<   |j                  |       |j                  |	        |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   r   description)rX   r   defaultprompt)	rc   r.   rN   r^   r   r&   rP   r   r   )rR   r   r   rawr{   r   itemrX   descr   r   prompt_texts               r!   extract_skill_config_varsr   z  ss     z*Hh%	\\(#Ffd#	
**X
C	#tec4 	#%FD $%$((5"%&,,.cTk488M2./557!
 ((9%&E)hhx(k3'K,=,=,?)//1E(O"E(Oe/0 Mr5   c                     g } t               }t               }t               D ]  }|j                         st	        |d      D ]  }	 |j                  d      }t        |      \  }}|j                  d      xs |j                  j                  }t        |      |v r[t        |      sgt        |      }	|	D ]=  }
|
d   |vst        |      |
d<   | j                  |
       |j                  |
d          ?   | S # t        $ r Y w xY w)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   rc   parentr   r&   rj   r   r   r   )all_vars	seen_keysr   
skills_dir
skill_filer   rR   _
skill_nameconfig_varsvars              r!   discover_all_skill_config_varsr     s    &(HUI')H)+ .
  "0ZH 	.J **G*<!23!7Q %0JJ4E4E4J4JJ:(*)+63K@K" .u:Y.#&z?CLOOC(MM#e*-	.	... O!  s    C;;	DDzskills.configr   
dotted_keyc                 p    |j                  d      }| }|D ]  }t        |t              r
||v r||   } y |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     sI    S!EG gt$dmG	
 Nr5   r   c                 ~   t               }i }| D ]  }|d   }t         d| }t        ||      }| t        |t              r"|j                         s|j                  dd      }t        |t              rDd|v sd|v r<t        j                  j                  t        j                  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   r   r   ~z${)r   SKILL_CONFIG_PREFIXr   r.   r&   rP   rc   ru   r)   r   r   )r   r   resolvedr   logical_keystorage_keyr9   s          r!   resolve_skill_config_valuesr    s     F!H &%j,-Q{m< 5=Zs3EKKMGGIr*E eS!se|tu}GG&&rww'9'9%'@AE %& Or5   c                     | j                  dd      }|syt        |      j                         j                  d      }t        |      dkD  r|dd dz   S |S )z8Extract a truncated description from parsed frontmatter.r   r   z'"<   N9   z...)rc   r&   rP   len)rR   raw_descr   s      r!   extract_skill_descriptionr	    sV    }b1Hx= &&u-D
4y2~CRy5  Kr5   r   filenamec              #   d  K   t        |       }g }t        j                  |d      D ]c  \  }}}d|v }|D cg c]  }|t        vr|r|t        v s| c}|dd ||v s5|j                  t        j                  j                  ||             e t        |      D ]  }	t        |	        yc c}w w)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-   N)
r&   ru   walkr   r0   r   r)   joinsortedr   )
r   r
  skills_dir_strmatchesr   r   fileshas_skill_mddr)   s
             r!   r   r     s      _NGWW^F 	9dE!U* 
++!a+=&= 
Q uNN277<<h78	9 w 4j
s   4B0B+
B0AB0z^[a-zA-Z0-9_-]+$r   c                 H    d| vrd| fS t        | j                  dd            S )z~Split ``'namespace:skill-name'`` into ``(namespace, bare_name)``.

    Returns ``(None, name)`` when there is no ``':'``.
    rH   NrE   )tuplerQ   )r   s    r!   parse_qualified_namer  ;  s+    
 $TzC#$$r5   r   c                 D    | syt        t        j                  |             S )zDCheck whether *candidate* is a valid namespace (``[a-zA-Z0-9_-]+``).F)rx   _NAMESPACE_REmatch)r   s    r!   is_valid_namespacer  E  s    ##I.//r5   )r   Nr   )@__doc__loggingru   rJ   r_   r%   r   typingr   r   r   r   r   r	   ry   r
   r   r   	getLogger__name__r   rb   	frozensetr   r0   rx   r*   r(   r@   r&   rC   rY   rh   rj   r   rn   __annotations__r|   r   r   intr   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r	  r   compiler  r  r  r   r5   r!   <module>r%     sU    	 	 
  8 8 G G			8	$
    . OP D (4 : "s "$.s .uT#s(^S-@'A .h3 4 2ES#X E4 EB   :; %' 4T	? '+S +T +\$4S> $d $T AC 4c3m,d38n<= B
 $sCx.  F#sTz #SX #L>SX >  ;= d5c?DJ67 <P$t* PfT$Z 6C 6C 6r0$ 0D .$sCx. T#t)_ (84S> 8d4S>>R 8v$T#s(^(< $T & 	T#s(^ 	 	d38n%	#s(^B4S> c t s 6 

./%s %uXc]C-?'@ %0(3- 0D 0r5   