
    iE                       d Z ddlm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
 ddlmZmZmZmZmZmZmZ ddlmZ  ej        e          ZdZd	Zd
ZeeehZd>dZd>dZd>dZd?dZd@dZ dAdZ!dBdZ"dCdZ#dCdZ$dDd Z%dEd#Z&dFd&Z'dGd'Z(dHd)Z)dId,Z*dJd-Z+dKd.Z,dKd/Z-dKd0Z.dKd1Z/dLd3Z0dMd5Z1dKd6Z2dNd8Z3dNd9Z4dOd;Z5dPd=Z6dS )Qav  Skill usage telemetry + provenance tracking for the Curator feature.

Tracks per-skill usage metadata in a sidecar JSON file (~/.hermes/skills/.usage.json)
keyed by skill name. Counters are bumped by the existing skill tools (skill_view,
skill_manage); the curator orchestrator reads the derived activity timestamp to
decide lifecycle transitions.

Design notes:
  - Sidecar, not frontmatter. Keeps operational telemetry out of user-authored
    SKILL.md content and avoids conflict pressure for bundled/hub skills.
  - Atomic writes via tempfile + os.replace (same pattern as .bundled_manifest).
  - All counter bumps are best-effort: failures log at DEBUG and return silently.
    A broken sidecar never breaks the underlying tool call.
  - Provenance filter: "agent-created" == not in .bundled_manifest AND not in
    .hub/lock.json. The curator only ever mutates agent-created skills.

Lifecycle states:
    active    -> default
    stale     -> unused > stale_after_days (config)
    archived  -> unused > archive_after_days (config); moved to .archive/
    pinned    -> opt-out from auto transitions (boolean flag, orthogonal to state)
    )annotationsN)datetimetimezone)Path)AnyDictIterableListOptionalSetTupleget_hermes_homeactivestalearchivedreturnr   c                 $    t                      dz  S )Nskillsr        6/home/ubuntu/.hermes/hermes-agent/tools/skill_usage.py_skills_dirr   -   s    x''r   c                 $    t                      dz  S )Nz.usage.jsonr   r   r   r   _usage_filer   1   s    ===((r   c                 $    t                      dz  S )Nz.archiver   r   r   r   _archive_dirr   5   s    ==:%%r   strc                 b    t          j        t          j                                                  S )N)r   nowr   utc	isoformatr   r   r   _now_isor$   9   s     <%%//111r   valuer   Optional[datetime]c                    | sdS 	 t          j        t          |                     }n# t          t          f$ r Y dS w xY w|j         |                    t          j                  }|S )z<Parse an ISO timestamp defensively for activity comparisons.N)tzinfo)	r   fromisoformatr   	TypeError
ValueErrorr(   replacer   r"   )r%   parseds     r   _parse_iso_timestampr.   =   su     t'E

33z"   tt}x|44Ms   !( ==recordDict[str, Any]Optional[str]c                    d}d}dD ]B}|                      |          }t          |          }|)|||k    r|}t          |          }C|S )a(  Return the newest actual activity timestamp for a usage record.

    "Activity" means a skill was used, viewed, or patched. Creation time is
    intentionally excluded so callers can still distinguish never-active skills;
    lifecycle code can fall back to ``created_at`` as its own anchor.
    N)last_used_atlast_viewed_atlast_patched_at)getr.   r   )r/   	latest_dt
latest_rawkeyrawdts         r   latest_activity_atr<   J   sf     %)I $JD " "jjoo!#&&:YISJr   intc                    d}dD ]A}	 |t          |                     |          pd          z  }+# t          t          f$ r Y >w xY w|S )zFReturn the total observed activity count across use/view/patch events.r   )	use_count
view_countpatch_count)r=   r6   r*   r+   )r/   totalr9   s      r   activity_countrC   ^   sh    E9  	SC-A...EE:& 	 	 	H	Ls   '0AASet[str]c                    t                      dz  } |                                 st                      S t                      }	 |                     d                                          D ]^}|                                }|s|                    dd          d                                         }|r|                    |           _n2# t          $ r%}t          
                    d|           Y d}~nd}~ww xY w|S )	zReturn the set of skill names that were seeded from the bundled repo.

    Reads ~/.hermes/skills/.bundled_manifest (format: "name:hash" per line).
    Returns empty set if the file is missing or unreadable.
    z.bundled_manifestutf-8encoding:   r   z#Failed to read bundled manifest: %sN)r   existsset	read_text
splitlinesstripsplitaddOSErrorloggerdebug)manifestnameslinenamees        r   _read_bundled_manifest_namesrZ   m   s    }}22H?? uueeE	?&&&88CCEE 	  	 D::<<D ::c1%%a(..00D  		$	   ? ? ?:A>>>>>>>>?Ls   BC 
C:C55C:c                    t                      dz  dz  } |                                 st                      S 	 t          j        |                     d                    }t          |t                    rJ|                    d          pi }t          |t                    rd |	                                D             S n># t          t          j        f$ r%}t                              d|           Y d}~nd}~ww xY wt                      S )	zReturn the set of skill names installed via the Skills Hub.

    Reads ~/.hermes/skills/.hub/lock.json (see tools/skills_hub.py :: HubLockFile).
    z.hubz	lock.jsonrF   rG   	installedc                ,    h | ]}t          |          S r   )r   ).0ks     r   	<setcomp>z,_read_hub_installed_names.<locals>.<setcomp>   s    9991A999r   z Failed to read hub lock file: %sN)r   rK   rL   jsonloadsrM   
isinstancedictr6   keysrR   JSONDecodeErrorrS   rT   )	lock_pathdatar\   rY   s       r   _read_hub_installed_namesri      s   
 &4I uu<z)--w-??@@dD!! 	:--3I)T** :99	(8(89999T)* < < <7;;;;;;;;<55Ls   BC   C;C66C;	List[str]c                    t                      } |                                 sg S t                      }t                      }||z  }g }|                     d          D ]}	 |                    |           }n# t          $ r Y %w xY w|j        }|r(|d                             d          s|d         dk    rZt          ||j
        j                  }||v rz|                    |           t          t          |                    S )a  Enumerate skills that were authored by the agent (or user), NOT by a
    bundled or hub-installed source.

    The curator operates exclusively on this set. Bundled / hub skills are
    maintained by their upstream sources and must never be pruned here.
    SKILL.mdr   .node_modulesfallback)r   rK   rZ   ri   rglobrelative_tor+   parts
startswith_read_skill_nameparentrX   appendsortedrL   )	basebundledhub
off_limitsrV   skill_mdrelrs   rX   s	            r   list_agent_created_skill_namesr      s"    ==D;;== 	*,,G
#
%
%C3JEJJz**  	&&t,,CC 	 	 	H		 	eAh))#.. 	%(n2L2L8?3GHHH:T#e**s    A66
BBr}   rp   c                   	 |                      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 )z9Parse the `name:` field from a SKILL.md YAML frontmatter.rF   r,   )rH   errorsNi  F
z---Tzname:rI   rJ   z"')rM   rR   rP   rO   rt   )r}   rp   textin_frontmatterrW   strippedr%   s          r   ru   ru      s   !!79!EEeteL   N

4   
 
::<<u !N 	h11':: 	NN3**1-3355;;EBBE Os   " 11
skill_nameboolc                D    t                      t                      z  }| |vS )z:Whether *skill_name* is neither bundled nor hub-installed.)rZ   ri   )r   r|   s     r   is_agent_createdr      s$    -//2K2M2MMJZ''r   c                 >    ddd d dd t                      t          dd d
S )Nr   F)
r?   r@   r3   r4   rA   r5   
created_atstatepinnedarchived_at)r$   STATE_ACTIVEr   r   r   _empty_recordr      s3    jj  r   Dict[str, Dict[str, Any]]c                    t                      } |                                 si S 	 t          j        |                     d                    }nA# t
          t          j        f$ r(}t                              d| |           i cY d}~S d}~ww xY wt          |t                    si S i }|                                D ],\  }}t          |t                    r||t          |          <   -|S )zGRead the entire .usage.json map. Returns empty dict on missing/corrupt.rF   rG   zFailed to read %s: %sN)r   rK   ra   rb   rM   rR   rf   rS   rT   rc   rd   itemsr   )pathrh   rY   cleanr_   vs         r   
load_usager      s    ==D;;== 	z$..'.::;;T)*   ,dA666						 dD!! 	')E

  1a 	E#a&&MLs   (A B%BBBrh   Nonec                   t                      }	 |j                            dd           t          j        t          |j                  dd          \  }}	 t          j        |dd          5 }t          j	        | |d	dd
           |
                                 t          j        |                                           ddd           n# 1 swxY w Y   t          j        ||           dS # t          $ r( 	 t          j        |           n# t           $ r Y nw xY w w xY w# t"          $ r)}t$                              d||d           Y d}~dS d}~ww xY w)uN   Write the usage map atomically. Best-effort — errors are logged, not raised.Tparentsexist_okz.usage_z.tmp)dirprefixsuffixwrF   rG      F)indent	sort_keysensure_asciiNzFailed to write %s: %sexc_info)r   rv   mkdirtempfilemkstempr   osfdopenra   dumpflushfsyncfilenor,   BaseExceptionunlinkrR   	ExceptionrS   rT   )rh   r   fdtmp_pathfrY   s         r   
save_usager      s   ==DG$666'DK  6
 
 
H	2sW555 %	$!t%PPPP			$$$% % % % % % % % % % % % % % % Jx&&&&& 	 	 		(####   	  G G G-tQFFFFFFFFFGss   AD' C2 0ACC2 CC2 CC2 2
D$=DD$
DD$DD$$D' '
E1EEc                   t                      }|                    |           }t          |t                    st	                      S t	                      }|                                D ]\  }}|                    ||           |S )zDReturn the record for *skill_name*, creating a fresh one if missing.)r   r6   rc   rd   r   r   
setdefault)r   rh   recry   r_   r   s         r   
get_recordr     sx    <<D
((:

Cc4   ??D

  1q!Jr   c                h   | sdS 	 t          |           sdS t                      }|                    |           }t          |t                    st                      } ||           ||| <   t          |           dS # t          $ r)}t          	                    d| |d           Y d}~dS d}~ww xY w)a>  Load, apply *mutator(record)* in place, save. Best-effort.

    Bundled and hub-installed skills are NEVER recorded in the sidecar.
    This keeps .usage.json focused on agent-created skills (the only ones
    the curator considers) and prevents stale counters from hanging around
    for upstream-managed skills.
    Nz"skill_usage._mutate(%s) failed: %sTr   )
r   r   r6   rc   rd   r   r   r   rS   rT   )r   mutatorrh   r   rY   s        r   _mutater     s      Y
++ 	F||hhz""#t$$ 	"//CZ4 Y Y Y9:qSWXXXXXXXXXYs   A> A%A> >
B1B,,B1c                .    dd}t          | |           dS )z=Bump view_count and last_viewed_at. Called from skill_view().r   r0   r   r   c                |    t          |                     d          pd          dz   | d<   t                      | d<   d S )Nr@   r   rJ   r4   r=   r6   r$   r   s    r   _applyzbump_view.<locals>._apply<  s?     5 5 :;;a?L (

r   Nr   r0   r   r   r   r   r   s     r   	bump_viewr   :  s.    + + + + Jr   c                .    dd}t          | |           dS )zBump use_count and last_used_at. Called when a skill is actively used
    (e.g. loaded into the prompt path or referenced from an assistant turn).r   r0   r   r   c                |    t          |                     d          pd          dz   | d<   t                      | d<   d S )Nr?   r   rJ   r3   r   r   s    r   r   zbump_use.<locals>._applyE  s>    sww{338q99A=K&jjNr   Nr   r   r   s     r   bump_user   B  s.    ) ) ) ) Jr   c                .    dd}t          | |           dS )zLBump patch_count and last_patched_at. Called from skill_manage (patch/edit).r   r0   r   r   c                |    t          |                     d          pd          dz   | d<   t                      | d<   d S )NrA   r   rJ   r5   r   r   s    r   r   zbump_patch.<locals>._applyM  s?     !7!7!<1==AM!)r   Nr   r   r   s     r   
bump_patchr   K  s.    , , , , Jr   r   c                    t           vrt                              d|            dS dfd}t          | |           dS )	z1Set lifecycle state. No-op if *state* is invalid.z"set_state: invalid state %r for %sNr   r0   r   r   c                r    | d<   t           k    rt                      | d<   d S t          k    rd | d<   d S d S )Nr   r   )STATE_ARCHIVEDr$   r   )r   r   s    r   r   zset_state.<locals>._applyX  sN    GN""!)Cl""!%C #"r   r   )_VALID_STATESrS   rT   r   )r   r   r   s    ` r   	set_stater   S  s`    M!!95*MMM& & & & & & Jr   r   c                4    dfd}t          | |           d S )Nr   r0   r   r   c                ,    t                    | d<   d S )Nr   )r   )r   r   s    r   r   zset_pinned.<locals>._applyb  s    VHr   r   r   )r   r   r   s    ` r   
set_pinnedr   a  s7    % % % % % %Jr   c                    | sdS 	 t                      }| |v r|| = t          |           dS dS # t          $ r)}t                              d| |d           Y d}~dS d}~ww xY w)zFDrop a skill's usage entry entirely. Called when the skill is deleted.Nz!skill_usage.forget(%s) failed: %sTr   )r   r   r   rS   rT   )r   rh   rY   s      r   forgetr   g  s     X||Z t   X X X8*aRVWWWWWWWWWXs   $. 
A!AA!Tuple[bool, str]c                   t          |           sdd|  dfS t          |           }|dd|  dfS t                      }	 |                    dd           n# t          $ r}dd| fcY d}~S d}~ww xY w||j        z  }|                                r>||j         d	t          j        t          j
                                      d
           z  }	 |                    |           np# t          $ rc}ddl}	 |                    t          |          t          |                     n## t           $ r}dd| fcY d}~cY d}~S d}~ww xY wY d}~nd}~ww xY wt#          | t$                     dd| fS )u   Move an agent-created skill directory to ~/.hermes/skills/.archive/.

    Returns (ok, message). Never archives bundled or hub skills — callers are
    responsible for checking provenance, but we double-check here as a safety net.
    Fskill 'z,' is bundled or hub-installed; never archiveNz' not foundTr   zfailed to create archive dir: -z%Y%m%d%H%M%Sr   zfailed to archive: zarchived to )r   _find_skill_dirr   r   rR   rX   rK   r   r!   r   r"   strftimerenameshutilmover   r   r   r   )r   	skill_dirarchive_rootrY   destr   e2s          r   archive_skillr   x  s
    J'' YX
XXXXX
++I7
77777>>L;4$7777 ; ; ;:q:::::::::;
 ).(D{{}} hgg(,x|2L2L2U2UVd2e2eggg5 5 5 5	5KKID		2222 	5 	5 	5444444444444444	5 32222	5 j.)))&&&&&sl    A 
A3"A.(A3.A3C) )
E3E80D)(E)
E	3E9E	:E>EE		EEc                    t                     sdd  dfS t                      }|                                sdS  fd|                    d          D             }|s0t	           fd|                    d          D             d	          }|sdd  d
fS |d         }t                       z  }|                                rdd| fS 	 |                    |           nf# t          $ rY ddl}	 |	                    t          |          t          |                     n # t          $ r}dd| fcY d}~cY S d}~ww xY wY nw xY wt           t                     dd| fS )u  Move an archived skill back to ~/.hermes/skills/. Restores to the flat
    top-level layout; original category nesting is NOT reconstructed.

    Refuses to restore under a name that now collides with a bundled or
    hub-installed skill — that would shadow the upstream version.
    Fr   zL' is now bundled or hub-installed; restore would shadow the upstream version)Fzno archive directoryc                R    g | ]#}|                                 |j        k    !|$S r   )is_dirrX   r^   pr   s     r   
<listcomp>z!restore_skill.<locals>.<listcomp>  s3    \\\

\qvQ[G[G[!G[G[G[r   *c                v    g | ]5}|                                 |j                             d           3|6S )r   )r   rX   rt   r   s     r   r   z!restore_skill.<locals>.<listcomp>  s^     D D D1

D v00J1A1A1ABBDQ D D Dr   T)reversez' not found in archiver   zdestination already exists: Nzfailed to restore: zrestored to )r   r   rK   rq   rx   r   r   rR   r   r   r   r   r   r   )r   r   
candidatessrcr   r   rY   s   `      r   restore_skillr     s    J'' 
8j 8 8 8
 	
  >>L   -,,
 ]\\\\//44\\\J 
D D D D**3// D D D
 
 


  CB
BBBBB
Q-C==:%D{{}} <;T;;;;4

4 4 4 4	4KKC#d)),,,, 	4 	4 	4333333333333	4 -,4 j,'''&&&&&sB   C% %E40D%$E%
E/D=5E6E=EEEOptional[Path]c                l   t                      }|                                sdS |                    d          D ]y}	 |                    |          }n# t          $ r Y %w xY w|j        r!|j        d                             d          rQt          ||j        j	                  | k    r	|j        c S zdS )zLocate the directory for a skill by its frontmatter `name:` field.

    Handles both flat (~/.hermes/skills/<skill>/SKILL.md) and category-nested
    (~/.hermes/skills/<category>/<skill>/SKILL.md) layouts.
    Nrl   r   rm   ro   )
r   rK   rq   rr   r+   rs   rt   ru   rv   rX   )r   ry   r}   r~   s       r   r   r     s     ==D;;== tJJz** # #	&&t,,CC 	 	 	H	9 	10055 	Hx/CDDD
RR?""" S4s   A
A A List[Dict[str, Any]]c                    t                      } g }t                      D ]}|                     |          }t          |t                    st                      }t                      }|                                D ]\  }}|                    ||           d|i|}t          |          |d<   t          |          |d<   |
                    |           |S )zReturn a list of {name, state, pinned, last_activity_at, ...}
    records for every agent-created skill. Missing usage records are backfilled
    with defaults so callers can always index fields.rX   last_activity_atrC   )r   r   r6   rc   rd   r   r   r   r<   rC   rw   )rh   rowsrX   r   ry   r_   r   rows           r   agent_created_reportr     s     <<D!#D.00 
 
hhtnn#t$$ 	"//CJJLL 	! 	!DAqNN1a    t#s#"4S"9"9 .s 3 3CKr   )r   r   )r   r   )r%   r   r   r&   )r/   r0   r   r1   )r/   r0   r   r=   )r   rD   )r   rj   )r}   r   rp   r   r   r   )r   r   r   r   )r   r0   )r   r   )rh   r   r   r   )r   r   r   r0   )r   r   r   r   )r   r   r   r   r   r   )r   r   r   r   r   r   )r   r   r   r   )r   r   r   r   )r   r   )7__doc__
__future__r   ra   loggingr   r   r   r   pathlibr   typingr   r   r	   r
   r   r   r   hermes_constantsr   	getLogger__name__rS   r   STATE_STALEr   r   r   r   r   r$   r.   r<   rC   rZ   ri   r   ru   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   <module>r     s   . # " " " " "   				  ' ' ' ' ' ' ' '       B B B B B B B B B B B B B B B B B B , , , , , ,		8	$	$ {N;( ( ( () ) ) )& & & &2 2 2 2
 
 
 
   (      .   &   @   *( ( ( (      (G G G G0
 
 
 
Y Y Y Y8                                   
X 
X 
X 
X"$' $' $' $'N.' .' .' .'b   2     r   