
    ژj5                    n   U d Z ddlm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 ddlZddlmZ  ej        e          Z ej        d          Z ej        d          Zi ad	ed
<   daded<   d6dZd7dZd8dZd9dZd:dZd;dZd;dZ d<dZ!d=d!Z"d>d#Z#	 	 d?d@d*Z$dAd+Z%	 	 	 dBdCd3Z&dAd4Z'dDd5Z(dS )Eu  Skill bundles — aliases that load multiple skills under one slash command.

A skill bundle is a small YAML file that names a set of skills to load
together. Invoking ``/<bundle-name>`` from the CLI or gateway loads every
referenced skill's full content into a single user message, the same way
``/<skill-name>`` does — but for N skills at once.

Storage
-------
Bundles live in ``~/.hermes/skill-bundles/*.yaml`` (and the equivalent
profile-aware directory under ``HERMES_HOME``). Each file looks like::

    name: backend-dev
    description: Backend feature work — code review, testing, PR workflow.
    skills:
      - github-code-review
      - test-driven-development
      - github-pr-workflow
    instruction: |
      Optional extra guidance to inject above the skill bodies.

The file's stem is treated as a fallback name when ``name:`` is absent, so
dropping a YAML into the directory is enough to register a new bundle.

Conflict resolution
-------------------
If a bundle and a skill share the same slash name, the bundle wins. The
slash command dispatch checks bundles first, then falls back to skills.
This is the intended behavior — a user who names a bundle ``research``
explicitly wants ``/research`` to mean their bundle, not whatever skill
happens to share the slug.

Public API
----------
- :func:`get_skill_bundles` — return ``{"/slug": bundle_info}``
- :func:`resolve_bundle_command_key` — map a user-typed command to its slug
- :func:`build_bundle_invocation_message` — produce the full user message
- :func:`reload_bundles` — re-scan disk and return a diff
- :func:`list_bundles` — return rich info for display (``hermes bundles``)
- :func:`save_bundle` / :func:`delete_bundle` — file-level operations
    )annotationsN)Path)AnyDictListOptionalTuple)get_hermes_homez
[^a-z0-9-]z-{2,}Dict[str, Dict[str, Any]]_bundles_cachezOptional[float]_bundles_cache_mtimereturnr   c                     t           j                            d          } | r!t          |                                           S t                      dz  S )zReturn the canonical bundles directory under HERMES_HOME.

    Honors ``HERMES_BUNDLES_DIR`` for tests; falls back to
    ``<HERMES_HOME>/skill-bundles``.
    HERMES_BUNDLES_DIRzskill-bundles)osenvirongetr   
expanduserr
   )overrides    8/home/ubuntu/.hermes/hermes-agent/agent/skill_bundles.py_bundles_dirr   B   sI     z~~233H +H~~((***..    namestrc                   |                                                      dd                              dd          }t                              d|          }t                              d|                              d          }|S )N -_ )lowerreplace_BUNDLE_INVALID_CHARSsub_BUNDLE_MULTI_HYPHENstrip)r   cmds     r   _slugifyr'   N   sl    
**,,

sC
(
(
0
0c
:
:C

#
#B
,
,C

"
"3
,
,
2
23
7
7CJr   
List[Path]c                     t                      } |                                 sg S g }dD ]7}|                    t          |                     |                               8|S )N)z*.yamlz*.yml)r   existsextendsortedglob)basefilesexts      r   _iter_bundle_filesr1   U   s`    >>D;;== 	E" - -VDIIcNN++,,,,Lr   r/   floatc                p   t                      }g }|                                r>	 |                    |                                j                   n# t
          $ r Y nw xY w| D ]?}	 |                    |                                j                   0# t
          $ r Y <w xY w|rt          |          ndS )zHighest mtime across the bundle files plus the dir itself.

    Watching the directory mtime catches deletions; watching individual
    files catches edits. Together they're a cheap freshness check.
    g        )r   r*   appendstatst_mtimeOSErrormax)r/   r.   mtimesfs       r   
_max_mtimer;   _   s     >>DF{{}} 	MM$))++.//// 	 	 	D	  	MM!&&((+,,,, 	 	 	H	 )3v;;;c)s#   ,A 
A A (,B
B"!B"pathOptional[Dict[str, Any]]c                   	 |                      d          }n4# t          $ r'}t                              d| |           Y d}~dS d}~ww xY w	 t	          j        |          }n9# t          j        $ r'}t                              d| |           Y d}~dS d}~ww xY wt          |t                    st                              d|            dS t          |
                    d          p| j                                                  }|st                              d|            dS |
                    d	          pg }t          |t                    r|st                              d
|            dS d |D             }|st                              d|            dS t          |
                    d          pd                                          }t          |
                    d          pd                                          }t          |          }|st                              d|            dS |||pdt          |           d||t          |           dS )u   Parse a single bundle YAML file. Returns ``None`` on any error.

    Errors are logged at WARNING level. We don't raise — a broken bundle
    shouldn't take down slash command discovery.
    utf-8encodingzCould not read bundle %s: %sNzInvalid YAML in bundle %s: %sz$Bundle %s is not a mapping; skippingr   zBundle %s has no name; skippingskillsz&Bundle %s has no skills list; skippingc                    g | ]D}t          |                                          #t          |                                          ES  r   r%   .0ss     r   
<listcomp>z%_load_bundle_file.<locals>.<listcomp>   s9    ???A?c!ffllnn???r   z)Bundle %s has empty skills list; skippingdescriptionr   instructionz&Bundle %s yielded empty slug; skippingzLoad z skills as a bundle)r   slugrJ   rB   rK   r<   )	read_textr7   loggerwarningyaml	safe_load	YAMLError
isinstancedictr   r   stemr%   listr'   len)	r<   rawexcdatar   rB   rJ   rK   rL   s	            r   _load_bundle_filer[   t   su   nngn..   5tSAAAttttt~c"">   6cBBBttttt dD!! =tDDDttxx,49--3355D 8$???tXXh%2Ffd## 6 ?FFFt??f???F BDIIItdhh}--344::<<Kdhh}--344::<<KD>>D ?FFFt "N&Nc&kk&N&N&N"D		  s,    
A
AA
A# #B2BBc                     t                      } i }| D ]R}t          |          }|sd|d          }||v r*t                              d||||         d                    M|||<   S|at          |           a|S )u   Scan the bundles directory and rebuild the cache.

    Returns the same mapping as :func:`get_skill_bundles` — ``"/slug"`` →
    bundle info dict. Later bundles with a duplicate slug are skipped with
    a warning (first wins, alphabetical order).
    /rL   z,Duplicate bundle slug %s from %s; keeping %sr<   )r1   r[   rN   rO   r   r;   r   )r/   outr:   infokeys        r   scan_bundlesra      s       E%'C   ## 	 $v,  #::NN>QC(   CN%e,,Jr   c                     t                      } t          |           }t          rt          |k    rt	                       t          S )zReturn the current bundle mapping, rescanning when disk changed.

    Cheap to call repeatedly: only rescans when the bundles directory or
    any bundle file's mtime is newer than the cached snapshot.
    )r1   r;   r   r   ra   )r/   current_mtimes     r   get_skill_bundlesrd      s>       Eu%%M 1]BBr   commandOptional[str]c                d    | sdS d|                      dd           }|t                      v r|ndS )zResolve a user-typed command to its canonical bundle slash key.

    Hyphens and underscores are treated interchangeably to mirror the
    skill-command behavior (Telegram converts hyphens to underscores in
    bot command names).
    Nr]   r   r   )r!   rd   )re   cmd_keys     r   resolve_bundle_command_keyri      sG      t-'//#s++--G!2!4!44477$>r   Dict[str, Any]c                    d	d}  | t                     t                      } | |          t          t                    t                    z
            }t          t                    t                    z
            }t          t                    t                    z            }fd|D             fd|D             |t	                    dS )
zRe-scan the bundles directory and return a diff.

    Mirrors :func:`agent.skill_commands.reload_skills` so callers can use
    the same display logic. Returns a dict with ``added``, ``removed``,
    ``unchanged``, and ``total`` keys.
    cmdsr   r   Dict[str, str]c                >    d |                                  D             S )Nc                l    i | ]1\  }}|                     d           |pi                     dd          2S )r]   rJ   r   )lstripr   )rG   kvs      r   
<dictcomp>z5reload_bundles.<locals>._snapshot.<locals>.<dictcomp>   s:    YYYDAqR}}]B??YYYr   )items)rl   s    r   	_snapshotz!reload_bundles.<locals>._snapshot   s    YYDJJLLYYYYr   c                &    g | ]}||         d S )r   rJ   rD   )rG   nafters     r   rI   z"reload_bundles.<locals>.<listcomp>   s%    LLL11U1X66LLLr   c                &    g | ]}||         d S rw   rD   )rG   rx   befores     r   rI   z"reload_bundles.<locals>.<listcomp>   s%    QQQaQvay99QQQr   )addedremoved	unchangedtotal)rl   r   r   rm   )r   ra   r,   setrW   )ru   newadded_namesremoved_namesr~   ry   r{   s        @@r   reload_bundlesr      s    Z Z Z Z Y~&&F
..CIcNNEUc&kk122K3v;;U344Ms5zzCKK/00I MLLLLLLQQQQ=QQQU	  r   List[Dict[str, Any]]c                 f    t                      } t          |                                 d           S )z6Return a sorted list of bundle info dicts for display.c                    | d         S )NrL   rD   )bs    r   <lambda>zlist_bundles.<locals>.<lambda>   s
    !F) r   )r`   )rd   r,   values)bundless    r   list_bundlesr      s.    !!G'..""(;(;<<<<r   r   rh   user_instructiontask_id
str | None*Optional[Tuple[str, List[str], List[str]]]c           	        t                      }|                    |           }|sdS ddlm}m} g }g }g }	t                      }
|d         }|d         }|                    d          pd}|D ]}|pd                                }|r||
v r|
                    |            |||          }|s|                    |           Y|\  }}}	 dd	l	m
}  ||           n# t          $ r Y nw xY wd
| d}|	                     |||||                     |                    |           |	sdS d| dt          |           ddd| dd                    |           g}|r+|                    dd                    |                      |r|                    dd| g           |r|                    dd| g           d                    |          }d                    |g|	          ||fS )u  Build the user message content for a bundle slash command invocation.

    Returns ``(message, loaded_skill_names, missing_skill_names)`` or
    ``None`` if the bundle wasn't found.

    A bundle that references skills the user doesn't have installed still
    loads — the agent gets a note about which ones were skipped. This is
    the same forgiving stance ``build_preloaded_skills_prompt`` uses for
    ``-s`` CLI preloading.
    Nr   )_load_skill_payload_build_skill_messager   rB   rK   r   )r   )bump_usez[Loaded as part of the "z" skill bundle.])
session_idz&[IMPORTANT: The user has invoked the "z" skill bundle, loading zL skills together. Treat every skill below as active guidance for this turn.]zBundle: zSkills loaded: z, zSkills missing (skipped): zBundle instruction: zUser instruction: 
z

)rd   r   agent.skill_commandsr   r   r   r%   addr4   tools.skill_usager   	ExceptionrW   joinr+   )rh   r   r   r   r_   r   r   loaded_namesmissingskill_blocksseenbundle_namerB   extra_instructionskill_id
identifierloadedloaded_skill	skill_dir
skill_namer   activation_noteheader_linesheaders                           r   build_bundle_invocation_messager      s     !!G;;wD t ONNNNNNN LG LUUDv,K(^F//52 ( (n"++--
 	Z4//$$ZAAA 	NN:&&&.4+i	222222HZ     	 	 	D	 E{DDD 	 	  "	  	
 	
 	
 	J'''' t
	- 	- 	-|$$	- 	- 	- 	 ;  3$))L1133L  OM79K9KMMNNN NR!K8I!K!KLMMM 
8&6889	
 	
 	
 YY|$$FKK/,/00,HHs   C
C,+C,c                r    t          |           }|st          d| d          t                      | dz  S )z7Return the canonical filesystem path for a bundle name.zBundle name z normalizes to an empty slugz.yaml)r'   
ValueErrorr   r   rL   s     r   bundle_path_forr   \  sE    D>>D NLLLLMMM>>tNNN**r   FrB   	List[str]rJ   rK   	overwriteboolc                   | pd                                 } | st          d          d |D             }|st          d          t          |           }|                                r|st	          d|           |j                            dd           | |d}|r||d	<   |r||d
<   |                    t          j	        |dd          d           t                       |S )zWrite a bundle to disk and invalidate the cache.

    Raises ``FileExistsError`` if the target exists and ``overwrite`` is
    False. Raises ``ValueError`` if the inputs are unusable.
    r   zBundle name is requiredc                    g | ]D}t          |                                          #t          |                                          ES rD   rE   rF   s     r   rI   zsave_bundle.<locals>.<listcomp>s  s9    GGGAGc!ffllnnGGGr   z(Bundle must reference at least one skillzBundle already exists at T)parentsexist_ok)r   rB   rJ   rK   F)	sort_keysallow_unicoder?   r@   )r%   r   r   r*   FileExistsErrorparentmkdir
write_textrP   	safe_dumpra   )r   rB   rJ   rK   r   cleaned_skillsr<   payloads           r   save_bundler   d  s&    JBD 42333GGfGGGN ECDDD4  D{{}} BY B@$@@AAAKdT222'+~FFG -!, -!,OOw%tDDD     NNNKr   c                    t          |           }|                                st          d|           |                                 t	                       |S )zvDelete a bundle by name. Returns the deleted path.

    Raises ``FileNotFoundError`` if the bundle doesn't exist.
    zNo bundle at )r   r*   FileNotFoundErrorunlinkra   )r   r<   s     r   delete_bundler     sS    
 4  D;;== 8 6 6 6777KKMMMNNNKr   c                h    t          |           }t                                          d|           S )z+Look up a bundle by name (slug-normalized).r]   )r'   rd   r   r   s     r   
get_bundler     s-    D>>D"":t::...r   )r   r   )r   r   r   r   )r   r(   )r/   r(   r   r2   )r<   r   r   r=   )r   r   )re   r   r   rf   )r   rj   )r   r   )r   N)rh   r   r   r   r   r   r   r   )r   r   r   r   )r   r   F)r   r   rB   r   rJ   r   rK   r   r   r   r   r   )r   r   r   r=   ))__doc__
__future__r   loggingr   repathlibr   typingr   r   r   r   r	   rP   hermes_constantsr
   	getLogger__name__rN   compiler"   r$   r   __annotations__r   r   r'   r1   r;   r[   ra   rd   ri   r   r   r   r   r   r   r   rD   r   r   <module>r      sV  ( ( (T # " " " " "  				 				       3 3 3 3 3 3 3 3 3 3 3 3 3 3  , , , , , ,		8	$	$ #
=11 !rz(++ ,. . . . .(,  , , , ,	/ 	/ 	/ 	/      * * * **1 1 1 1h   6
 
 
 

? 
? 
? 
?   4= = = = WI WI WI WI WI~+ + + + # # # # #L
 
 
 
/ / / / / /r   