
    `gjU                       U d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
mZ ddlmZmZmZ ddlmZmZmZ ddlmZ  ej,                  d      Zg dZg d	Zd
dgZg dZee   ed<    eh d      Z ee   ed<    eh d      Z!ee   ed<   dZ"dede#fdZ$defdZ% eh d      Z& eh d      Z' eh d      Z( eh d      Z)defdZ*defdZ+defdZ,defdZ-d edefd!Z.d eddfd"Z/d eddfd#Z0d edefd$Z1d ede#fd%Z2d edee   fd&Z3de#fd'Z4dkd ed(ee   dee   fd)Z5d ede#fd*Z6deddfd+Z7d,edee   fd-Z8d.Z9de:eef   fd/Z;e G d0 d1             Z<dede=fd2Z>dede=fd3Z?dede#fd4Z@i ZAe:ee=eBeBeCf   f   ed5<   d6ZDd7edeBfd8ZEdedeCfd9ZFdedefd:ZGdede:fd;ZHddd<ded=ee   d>ee#   ddfd?ZIdee<   fd@ZJdAe#deeeef      fdBZK	 	 	 	 	 	 dld edCee   dDe#dEe#dFe#dGe#d=ee   defdHZLdmdedIe#dee:   fdJZMdmdIe#dee   fdKZNdLededeeC   fdMZOdLededdfdNZPdeddfdOZQdmd edPe#defdQZRd,eddfdRZSd,eddfdSZTd ededdfdTZUdeddfdUZVdefdVZWd eddfdWZXdefdXZYdYefdZZZd ed[edefd\Z[d]edee   fd^Z\d_ed`eddfdaZ]d_ede^e   fdbZ_dkdced ee   defddZ`deedfedgeddfdhZadeedfedefdiZbd,edefdjZcy)nu_  
Profile management for multiple isolated Hermes instances.

Each profile is a fully independent HERMES_HOME directory with its own
config.yaml, .env, memory, sessions, skills, gateway, cron, and logs.
Profiles live under ``~/.hermes/profiles/<name>/`` by default.

The "default" profile is ``~/.hermes`` itself — backward compatible,
zero migration needed.

Usage::

    hermes profile create coder          # fresh profile + bundled skills
    hermes profile create coder --clone  # also copy config, .env, SOUL.md, skills
    hermes profile create coder --clone-all  # full copy of source profile
    coder chat                           # use via wrapper alias
    hermes -p coder chat                 # or via flag
    hermes profile use coder             # set as sticky default
    hermes profile delete coder          # remove profile + alias + service
    N)	dataclass)PathPurePosixPathPureWindowsPath)ListOptionalTuple)is_excluded_skill_pathz^[a-z0-9][a-z0-9_-]{0,63}$)	memoriessessionsskillsskinslogsplans	workspacecronhome)config.yaml.envSOUL.mdzmemories/MEMORY.mdzmemories/USER.md)gateway.pidgateway_state.jsonprocesses.json_CLONE_ALL_STRIP>   binprofilesnode_modules
.worktreeshermes-agent_CLONE_ALL_DEFAULT_EXCLUDE_ROOT>   state-snapshotsbackupsr   checkpointsstate.dbstate.db-shmstate.db-wal_CLONE_ALL_HISTORY_EXCLUDE_ROOTz.no-bundled-skillsprofile_dirreturnc                 P    	 | t         z  j                         S # t        $ r Y yw xY w)z>Return True if the profile opted out of bundled-skill seeding.F)NO_BUNDLED_SKILLS_MARKERexistsOSErrorr(   s    F/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/profiles.pyhas_bundled_skills_opt_outr0      s.    66>>@@ s    	%%
source_dirc                     | j                         t               j                         k(  dt        dt        t           dt        t           ffd}|S )uW  Exclude infrastructure artifacts when cloning a profile via --clone-all.

    Three categories:
      1. Root-level entries in ``_CLONE_ALL_HISTORY_EXCLUDE_ROOT`` — session
         history, backups, and snapshots that belong to the SOURCE profile
         and should never carry into a fresh clone.  Applies to any source.
      2. Root-level entries in ``_CLONE_ALL_DEFAULT_EXCLUDE_ROOT`` — known
         Hermes infrastructure directories that only the default profile
         (``~/.hermes``) ever contains.  Gated on ``source_dir`` actually
         being the default profile so a named-profile source never has its
         own data silently dropped.
      3. Universal exclusions at any depth — Python bytecode caches that
         are stale or regenerable (``__pycache__``, ``*.pyc``, ``*.pyo``)
         and runtime sockets / temp files (``*.sock``, ``*.tmp``).

    The export-side ignore (``_default_export_ignore``) uses the same
    two-tier pattern with the broader ``_DEFAULT_EXPORT_EXCLUDE_ROOT`` set
    because the export archive is a portable snapshot rather than a live
    clone.
    	directorynamesr)   c                 F   g }|D ]  }|dk(  s|j                  d      r|j                  |       +	 t        |       j                         k(  }|sK|t        v r|j                  |       esh|t        v sq|j                  |        |S # t        t
        f$ r d}Y Qw xY w)N__pycache__)z.pycz.pyo.sock.tmpF)endswithappendr   resolver-   
ValueErrorr'   r    )r3   r4   ignoredentryat_rootis_default_sourcesource_resolveds        r/   _ignorez+_clone_all_copytree_ignore.<locals>._ignore   s     	*E &>>"CDu% y/113F ;;NN5)$2Q)QNN5)-	*.  Z(    	 s   BB B )r;   _get_default_hermes_homestrr   )r1   rB   r@   rA   s     @@r/   _clone_all_copytree_ignorerE      sT    * !((*O'+C+E+M+M+OO3 tCy T#Y 6 N    >   	auth.lock
errors.log.update_check.hermes_historyhermes_state.dbresponse_store.dbresponse_store.db-shmresponse_store.db-walr   r   r   	sandboxesaudio_cacher#   image_cacher   active_profiledocument_cachebrowser_screenshotsr   r$   	auth.jsonr   r   r   r%   r&   r   r   >   USER.md	AGENTS.md	CLAUDE.md	MEMORY.md	todo.json.cursorrulessystem_prompt.mdr   r   pluginsscriptsr   r   	knowledgepreferencesr   r   >   tmprootsudotesthermesdefault>   acpmcpchatr   dumploginmodelsetuptoolsconfigdoctorhonchologoutr   statusupdategatewaypairingr]   profileversioninsightsr   whatsapp	uninstallc                      t               dz  S )a  Return the directory where named profiles are stored.

    Anchored to the hermes root, NOT to the current HERMES_HOME
    (which may itself be a profile).  This ensures ``coder profile list``
    can see all profiles.

    In Docker/custom deployments where HERMES_HOME points outside
    ``~/.hermes``, profiles live under ``HERMES_HOME/profiles/`` so
    they persist on the mounted volume.
    r   rC    rF   r/   _get_profiles_rootr     s     $%
22rF   c                      ddl m}   |        S )zReturn the default (pre-profile) HERMES_HOME path.

    In standard deployments this is ``~/.hermes``.
    In Docker/custom deployments where HERMES_HOME is outside ``~/.hermes``
    (e.g. ``/opt/data``), returns HERMES_HOME directly.
    r   get_default_hermes_root)hermes_constantsr   r   s    r/   rC   rC     s     9"$$rF   c                      t               dz  S )z2Return the path to the sticky active_profile file.rR   r}   r~   rF   r/   _get_active_profile_pathr   !  s    #%(888rF   c                  6    t        j                         dz  dz  S )z)Return the directory for wrapper scripts.z.localr   )r   r   r~   rF   r/   _get_wrapper_dirr   &  s    99;!E))rF   namec                     t        | t              st        |       } | j                         }|st        d      |j	                         dk(  ry|j                         S )u  Return the canonical profile id used on disk and in CLI ``-p`` argv.

    Named profiles are stored lowercase under ``profiles/<id>/``. The special
    alias ``default`` is matched case-insensitively (``Default`` → ``default``).
    Dashboards and tools may pass title-cased display labels; normalize before
    validation, assignment, and subprocess spawn (see issue #18498).
    zprofile name cannot be emptyrf   )
isinstancerD   stripr<   casefoldlower)r   strippeds     r/   normalize_profile_namer   /  sR     dC 4yzz|H788i'>>rF   c                     | dk(  ryt         j                  |       st        d| d      | t        v rt        d| d      y)u  Raise ``ValueError`` if *name* is not a valid profile identifier.

    Validates the input as-given — strict lowercase match. Callers that accept
    mixed-case or title-cased input from users (dashboard UI, CLI args) should
    call :func:`normalize_profile_name` first. This separation keeps validate
    honest about what the on-disk directory name must look like, while
    ingress-point normalization handles UX flexibility (see #18498).

    Also rejects names in :data:`_RESERVED_NAMES` (``hermes``, ``test``,
    ``tmp``, ``root``, ``sudo``) that would create confusing on-disk
    collisions (a ``hermes`` profile inside ``~/.hermes/``) or get refused
    at alias-creation time anyway. ``default`` is a special pass-through —
    it's a valid alias for the built-in root profile.
    rf   NzInvalid profile name %. Must match [a-z0-9][a-z0-9_-]{0,63}zProfile name uz    is reserved — it collides with either the Hermes installation itself or a common system binary.  Pick a different name.)_PROFILE_ID_REmatchr<   _RESERVED_NAMESr   s    r/   validate_profile_namer   A  sh     y%#D8 ,) *
 	
 D8 $% &
 	
 rF   c                 L    t         j                  |       st        d| d      y)u  Raise ``ValueError`` if *name* is not a safe wrapper-alias identifier.

    The alias is used verbatim as a filename under :func:`_get_wrapper_dir`
    (``~/.local/bin``), so it must be a single safe command name with no path
    separators or traversal segments — otherwise a value like ``../../.bashrc``
    would escape the wrapper directory and clobber arbitrary user files. We
    reuse the profile id regex, which already forbids ``/``, ``.``, and ``..``.
    zInvalid alias name r   N)r   r   r<   r   s    r/   validate_alias_namer   _  s5     %!$ *) *
 	
 &rF   c                 P    t        |       }|dk(  r
t               S t               |z  S )z4Resolve a profile name to its HERMES_HOME directory.rf   )r   rC   r   r   canons     r/   get_profile_dirr   o  s+    "4(E	'))%''rF   c                 V    t        |       }|dk(  ryt        |      j                         S )z)Check whether a profile directory exists.rf   T)r   r   is_dirr   s     r/   profile_existsr   w  s+    "4(E	5!((**rF   c                 l   t        |       }	 t        |       |t        v rd| dS |t
        v rd| dS t               }t        j                  dk(  }	 t        j                  |rdnd|gddd	
      }|j                  dk(  re|j                  j                         j                         d   }||r| dn|z  }|t        |      k(  r	 |j                         }d|v ry	 d| d| dS 	 y# t        $ r}t        |      cY d}~S d}~ww xY w# t         $ r Y 7w xY w# t"        t        j$                  f$ r Y yw xY w)zReturn a human-readable collision message, or None if the name is safe.

    Checks: alias-name validity, reserved names, hermes subcommands, existing
    binaries in PATH.
    N'z' is a reserved namez$' conflicts with a hermes subcommandwin32wherewhichT   )capture_outputtexttimeoutr   .bat	hermes -pz&' conflicts with an existing command ())r   r   r<   rD   r   _HERMES_SUBCOMMANDSr   sysplatform
subprocessrun
returncodestdoutr   
splitlines	read_text	ExceptionFileNotFoundErrorTimeoutExpired)	r   r   excwrapper_dir
is_windowsresultexisting_pathexpectedcontents	            r/   check_alias_collisionr     sq    #4(EE" 5'-..##5'=>> #$K(J"W7dA
 !"MM//1<<>qAM"
tnNHH-&002G"g-# . ugCM?RSTT " =  3x0 !  z889 sT   C' A3D D 	D '	D0
D :D D	DD DD D32D3c                      t        t                     } | t        j                  j	                  dd      j                  t        j                        v S )z!Check if ~/.local/bin is in PATH.PATH )rD   r   osenvirongetsplitpathsep)r   s    r/   _is_wrapper_dir_in_pathr     s9    &()K"**..4::2::FFFrF   targetc                 ,   t        |       }|rt        |      n|}t        |       t               }	 |j                  dd       t        j                  dk(  }|r || dz  }	 |j                  d| d	       |S ||z  }	 t        j                  d      xs d}|j                  dt        j                  |       d| d       |j                  |j                         j                  t        j                   z  t        j"                  z  t        j$                  z         |S # t        $ r}t        d| d|        Y d}~yd}~ww xY w# t        $ r}t        d
| d|        Y d}~yd}~ww xY w# t        $ r}t        d
| d|        Y d}~yd}~ww xY w)u  Create a shell wrapper script at ~/.local/bin/<name>.

    The wrapper file is named after ``name`` (the alias). The profile it
    activates is ``target`` if given, otherwise ``name`` — this lets a custom
    alias name point at a differently-named profile without a post-hoc rewrite.

    On Windows, creates a ``.bat`` file instead of a POSIX shell script.
    Returns the path to the created wrapper, or None if creation failed.
    Tparentsexist_oku   ⚠ Could not create : Nr   r   z@echo off
hermes -p z %*
u    ⚠ Could not create wrapper at re   z#!/bin/sh
exec z -p z "$@"
)r   r   r   mkdirr-   printr   r   
write_textshutilr   shlexquotechmodstatst_modeS_IEXECS_IXGRPS_IXOTH)	r   r   r   rw   r   er   wrapper_path
hermes_exes	            r/   create_wrapper_scriptr     s    #4(E06$V,EG "$K$6
 (J"wd^3	##&=gYg$NO
 #U*	h/;8J##&6u{{:7N6OtT[S\\c$de|002::T\\IDLLX[_[g[ggh'  %k]"QC89  	4\N"QCHI	  	4\N"QCHI	sH   D! "E ?B!E/ !	E*E  E	E,E''E,/	F8FFc                 f   t               }t        |       }	 t        |       t        j
                  dk(  }||z  g}|r|j                  d|| dz         |D ]:  }|j                         s	 |j                         }d|v r|j                           y< y# t        $ r Y yw xY w# t        $ r Y Xw xY w)zARemove the wrapper script for a profile. Returns True if removed.Fr   r   r   r   T)r   r   r   r<   r   r   insertr,   r   unlinkr   )r   r   r   r   
candidatesr   r   s          r/   remove_wrapper_scriptr     s    "$K"4(EE" (J %&J![eWD>9:" 	 &002') '') *	 '  "  s#   B ,$B$	B! B!$	B0/B0c                     | dz  }|j                         sy	 ddlm}m} ddlm}m}  |t        |             }	  |       \  }}||k  r
 |dd        ||       y#  ||       w xY w# t        $ r Y yw xY w)	u  Bring a copied profile config.yaml up to the current schema.

    Profile creation can clone a config file that predates schema tracking (no
    ``_config_version``) or that is simply older than the running Hermes. If we
    leave it untouched, the first desktop/doctor view of the new profile shows a
    scary ``v0 → latest`` warning even though we just created the profile. Scope
    the normal migration pipeline to the new profile and keep it non-interactive.
    r   Nr   )reset_hermes_home_overrideset_hermes_home_override)check_config_versionmigrate_configFT)interactivequiet)	r,   r   r   r   hermes_cli.configr   r   rD   r   )	r(   config_pathr   r   r   r   tokencurrent_ver
latest_vers	            r/   #_migrate_profile_config_if_outdatedr     s     -KYJ([)9:	.&:&<#KZ'5=&u-&u-  		s(   !A) A A) 
A&&A) )	A54A5profile_namec                 F    t               j                  t        |             S )u  Return the alias name of the wrapper that activates *profile_name*, or None.

    A wrapper created by :func:`create_wrapper_script` is a file named after the
    alias whose body invokes ``hermes -p <profile>``. When the alias name equals
    the profile name this is trivial, but a custom alias (``hermes profile alias
    <profile> --name <custom>``) produces a differently-named file — so the
    display side cannot assume ``wrapper == profile`` and must reverse-look-up.

    A custom alias (name != profile) is preferred over the profile-named wrapper
    so ``profile list``/``show`` surface the command the user actually typed.
    Results are sorted for deterministic output when several aliases match.

    For listing ALL profiles at once, prefer :func:`build_alias_map` — calling
    this per-profile re-reads every wrapper file N times (O(N*M)); on a wrapper
    dir like ``~/.local/bin`` that also holds large unrelated binaries (ffmpeg
    etc.) that meant multi-second ``list_profiles`` latency and desktop timeouts.
    )build_alias_mapr   r   )r   s    r/   find_alias_for_profiler     s    $   !7!EFFrF   i    c                     t               } i }| j                         s|S t        j                  dk(  }d}t	        | j                               D ]  }|j                         s|r|j                  dk7  r'|s|j                  r6	 t        |ddd      5 }|j                  t              }ddd       j                  |      }|d	k(  r{||t        |      z   d }|j                         r#|j!                  dd
      d   j                         nd}	|	st#        |	      }	|r|j$                  n|j&                  }
|
|	k(  r|j)                  |	|
       |
||	<   	 |S # 1 sw Y   xY w# t        t        f$ r Y *w xY w)a  Single-pass reverse map ``{canonical_profile -> alias_name}``.

    Scans the wrapper dir ONCE (vs. :func:`find_alias_for_profile` per profile)
    and reads only a small head slice of each candidate wrapper, skipping
    binaries. A custom alias (file name != profile) wins over the profile-named
    wrapper, matching ``find_alias_for_profile``'s preference; deterministic via
    sorted iteration.
    r   z
hermes -p r   rutf-8strict)encodingerrorsN   r   r   )r   r   r   r   sortediterdiris_filesuffixopenread_WRAPPER_READ_LIMITr-   UnicodeDecodeErrorfindlenr   r   r   stemr   
setdefault)r   r   r   prefixr>   fr   idxrestr   aliass              r/   r   r   9  sm    #$KF(JF++-. "}}%,,&0ell	eS78D 6&&!456
 ll6""9sS[()*26**,

4#A&,,.B&u-(

ejj E>eU+!F5M;"< M+6 6+, 		s*   E%E)E%E"	E%%E87E8c                       e Zd ZU dZeed<   eed<   eed<   eed<   dZe	e   ed<   dZ
e	e   ed<   d	Zeed
<   dZeed<   dZe	e   ed<   dZe	e   ed<   dZe	e   ed<   dZe	e   ed<   dZe	e   ed<   dZeed<   d	Zeed<   y)ProfileInfoz$Summary information about a profile.r   path
is_defaultgateway_runningNrl   providerFhas_envr   skill_count
alias_path
alias_namedistribution_namedistribution_versiondistribution_sourcer   descriptiondescription_auto)__name__
__module____qualname____doc__rD   __annotations__r   boolrl   r   r  r  r  intr  r  r  r  r  r  r  r~   rF   r/   r  r  n  s    .
I
JE8C="Hhsm"GTK!%J% !%J$'+x}+*.(3-.)-#- K
 #d"rF   r  c                 L   | dz  }|j                         sy	 ddl}t        |dd      5 }|j                  |      xs i }ddd       t	        t
              sy|j                  d      |j                  d	      |j                  d
      fS # 1 sw Y   LxY w# t        $ r Y yw xY w)u  Return ``(name, version, source)`` from the profile's ``distribution.yaml``
    if present; ``(None, None, None)`` otherwise.

    Failures (missing file, bad YAML) are swallowed — a bad manifest should
    never break ``hermes profile list`` for an unrelated profile.
    zdistribution.yaml)NNNr   Nr   r   r   r   rx   source)r   yamlr  	safe_loadr   dictr   r   )r(   mf_pathr'  r
  datas        r/   _read_distribution_metar,    s     //G?? '31 	+Q>>!$*D	+$%#HHVHHYHHX
 	
		+ 	+    s.   B B B 1B BB 	B#"B#c                    | dz  }|j                         sy	 ddl}t        |dd      5 }|j                  |      xs i }ddd       j	                  di       }t        |t              r|dfS t        |t              r5|j	                  d	      xs |j	                  d      |j	                  d
      fS y# 1 sw Y   uxY w# t        $ r Y yw xY w)zLRead model/provider from a profile's config.yaml. Returns (model, provider).r   )NNr   Nr   r   r%  rl   rf   r  )	r,   r'  r  r(  r   r   rD   r)  r   )r(   r   r'  r
  cfg	model_cfgs         r/   _read_config_modelr0    s    -K+sW5 	*..#)rC	*GGGR(	i%d?"i&==+Ey}}W/Ey}}U_G```	* 	*  s/   C  B4 -C  .AC  4B=9C   	CCc                     	 ddl m}  || dz  d      	 y	 	 ddl m}m}  || d	z        } ||| 
      duS # t        $ r Y +w xY w# t        $ r Y yw xY w)u  Check if a gateway is running for a given profile directory.

    Primary signal is the profile's ``gateway.pid`` (verified against the
    runtime lock).  That check fails closed whenever the lock isn't held by
    *this* reader — which is exactly the case for a dashboard process that is
    a separate s6 service from the gateway it's reporting on (Docker), or any
    launch-service-managed gateway that left a fresh ``gateway_state.json`` but
    no live PID file.  In those cases fall back to validating the PID recorded
    in the profile's own ``gateway_state.json`` against the live process table,
    mirroring the ``/api/status`` sidebar's liveness logic so the two surfaces
    agree.  Parameterized by ``profile_dir`` so it never mutates ``HERMES_HOME``.
    r   )get_running_pidr   F)cleanup_staleNT)get_runtime_status_running_pidread_runtime_statusr   )expected_home)gateway.statusr2  r   r4  r5  )r(   r2  r4  r5  runtimes        r/   _check_gateway_runningr9    s    2K-7uM 	
 &k4H&HI-g[QY]]]    s    8 A 	AA	AA_SKILL_COUNT_CACHEg      >@
skills_dirc                 l   	 | j                         j                  }	 t        j                  |       5 }|D ]8  }	 |j                  d      r#|j                  d      j                  }||kD  r|}: 	 ddd       |S # t        $ r Y yw xY w# t        $ r Y `w xY w# 1 sw Y   |S xY w# t        $ r Y |S w xY w)a\  Cheap change-signature for a skills tree.

    Max mtime of ``skills_dir`` and its immediate children (category dirs).
    Adding/removing a category bumps ``skills_dir``'s mtime; adding/removing a
    skill inside a category bumps that category dir's mtime. One ``scandir``
    (not a recursive walk) keeps this O(#categories), not O(#files).
    g        F)follow_symlinksN)r   st_mtimer-   r   scandirr   )r;  sigitr>   ms        r/   _skills_dir_signaturerC    s    oo((ZZ
# 	r ||E|:!JJuJ=FFs7"#C	 J    	 J  Jsb   A; B& B5B
.B1B& ;	BB
	BBBBB#B& #B& &	B32B3c                 V   | dz  }|j                         syt        |      }t        |      }t        j                         }t        j                  |      }||d   |k(  r||d   z
  t        k  r|d   S d}|j                  d      D ]  }t        |      r|dz  } |||ft        |<   |S )zECount installed skills in a profile (cached by skills-dir signature).r   r   r      zSKILL.md)	r   rD   rC  timer:  r   _SKILL_COUNT_TTL_SECONDSrglobr
   )r(   r;  key	signaturenowcachedcountmds           r/   _count_skillsrO    s    x'J
j/C%j1I
))+C##C(F1I"6!9_ 88ayEz* !"%
  )#u5sLrF   c                     | dz  S )Nzprofile.yamlr~   r.   s    r/   _profile_yaml_pathrQ  +  s    ''rF   c                    t        |       }|j                         sdddS 	 ddl}t        |dd      5 }|j	                  |      xs i }ddd       t        t              sdddS t        |j                  d	      xs d      j                         t        |j                  d
d            dS # 1 sw Y   fxY w# t
        $ r dddcY S w xY w)u  Read ``<profile_dir>/profile.yaml`` and return a dict.

    Returns ``{"description": "", "description_auto": False}`` when the
    file is missing or unreadable. Never raises — a corrupt
    profile.yaml on an unrelated profile must not break
    ``hermes profile list``.
    r   Fr  r  r   Nr   r   r%  r  r  )rQ  r   r'  r  r(  r   r   r)  rD   r   r   r"  )r(   r  r'  r
  r+  s        r/   read_profile_metarT  /  s     k*D<<>!u==>$g. 	+!>>!$*D	+ dD!!u==488M28b9??A *<e!DE 	+ 	+ >!u==>s(   B; B/
B; /B84B; ;CCrS  r  r  c                   | j                         st        d|        ddl}t        |       }i }|j	                         r?	 t        |dd      5 }|j                  |      xs i }ddd       t        t              r|}||j                         |d<   |t        |      |d<   t        |d	d      5 }|j                  ||d
d
       ddd       y# 1 sw Y   mxY w# t        $ r i }Y kw xY w# 1 sw Y   yxY w)zUpdate ``<profile_dir>/profile.yaml`` in place.

    Only the explicitly passed fields are overwritten; unspecified
    fields preserve existing values. Creates the file if missing.
    Profile directory itself must exist.
    z"profile directory does not exist: r   Nr   r   r%  r  r  wF)	sort_keysdefault_flow_style)r   r   r'  rQ  r   r  r(  r   r)  r   r   r"  	safe_dump)r(   r  r  r'  r  existingr
  loadeds           r/   write_profile_metar\  H  s    "D[M RSSk*DH||~	dC'2 1a*0b1&$'! "-"3"3"5#'+,<'=#$	dC'	* OaxeNO O1 1  	H	O Os6   C C%C 2C.CC C+*C+.C7c                  >   g } t               }t               }|j                         rt        |      \  }}t	        |      \  }}}t        |      }| j                  t        d|dt        |      |||dz  j                         t        |      ||||j                  dd      |j                  dd                   t               }	|	j                         rIt               }
t        |	j                               D ]"  }|j                         s|j                   }|dk(  r't"        j%                  |      s=t        |      \  }}|
j                  t'        |            }|r t(        j*                  d	k(  }||r| d
n|z  }nd}t	        |      \  }}}t        |      }| j                  t        ||dt        |      |||dz  j                         t        |      |r|j                         r|nd|||||j                  dd      |j                  dd                   % | S )z4Return info for all profiles, including the default.rf   Tr   r  r   r  F)r   r  r  r  rl   r  r  r  r  r  r  r  r  r   r   N)r   r  r  r  rl   r  r  r  r  r  r  r  r  r  r  )r   rC   r   r0  r,  rT  r:   r  r9  r,   rO  r   r   r   r   r   r   r   r   r   r   r   )r   r   default_homerl   r  	dist_namedist_versiondist_sourcemetaprofiles_root	alias_mapr>   r   r  r   r  s                   r/   list_profilesre  m  s   H"$K ,-L,\:x/F|/T,	< .2<@!F*224%l3'!- +3!XX&8%@
 	" '(M $%	M1134 !	E<<>::Dy !''-07OE8"'=d'CDJ \\W4
(:zl$,?S]^
!
3J53Q0I|[$U+DOOK  6u =!//1)%0*49J9J9L:SW%"+%1$/ HH]B7!%*<e!D #!	F OrF   	multiplexc                 d   t               xs d}| s|t        |      fgS dt               fg}t               }|j	                         rjt        |j                               D ]N  }|j	                         s|j                  }|dk(  r&t        j                  |      s<|j                  ||f       P |S )u  Return the ``(profile_name, hermes_home)`` pairs a gateway should serve.

    This is the single chokepoint for "which profiles does the inbound gateway
    handle" so later multiplexing phases never re-derive the set.

    - ``multiplex=False`` (default): returns exactly one entry for the *active*
      profile — byte-for-byte the single-profile behavior the gateway has
      always had. The name is ``"default"`` for the default profile or the
      active named profile's id.
    - ``multiplex=True``: returns the default profile plus every valid named
      profile under ``profiles/``, each paired with its own HERMES_HOME.

    Intentionally lightweight (a directory scan + name validation only): no
    per-profile config reads, gateway-running probes, or skill counts like
    :func:`list_profiles`. It runs on gateway startup and must stay cheap.

    The returned ``hermes_home`` is the path to pass to
    ``set_hermes_home_override`` when scoping a turn to that profile.
    rf   )get_active_profile_namer   rC   r   r   r   r   r   r   r   r:   )rf  activeserverc  r>   r   s         r/   profiles_to_serverk    s    ( %&3)F0122&/1I1K%L$ME&(MM1134 	(E<<>::Dy !''-LL$'	( LrF   
clone_from	clone_allclone_configno_alias	no_skillsc                    |r||s|rt        d      t        |       }t        |       |dk(  rt        d      t        |      }|j	                         rt        d| d|       d}	||s|rV|ddlm}
  |
       }	n!t        |      }t        |       t        |      }	|	j                         st        d	|xs d
 d|	       |rF|	rDt        j                  |	|dt        |	             t        D ]  }||z  j                  d        n"|j                  dd       t         D ]  }||z  j                  dd        |	t"        D ]Y  }|	|z  }|j	                         s||z  }t        j$                  ||       |dk(  s:	 t'        j(                  t+        |      d       [ |	dz  }|j                         rt        j                  ||dz  dd       t.        D ]P  }|	|z  }|j	                         s||z  }|j0                  j                  dd       t        j$                  ||       R |dz  }|j	                         s3	 |j3                  dd       t'        j(                  t+        |      d       |dz  }|j	                         s	 ddlm} |j3                  |d       |r	 |t:        z  j3                  dd       |st=        |       |r-|j?                         r	 tA        ||j?                         d       tC        |       |S # t,        $ r Y w xY w# t,        $ r Y w xY w# t8        $ r Y w xY w# t,        $ r Y w xY w# t8        $ r Y Uw xY w)a  Create a new profile directory.

    Parameters
    ----------
    name:
        Profile identifier (lowercase, alphanumeric, hyphens, underscores).
    clone_from:
        Source profile to clone from. If ``None`` and clone_config/clone_all
        is True, defaults to the currently active profile.
    clone_all:
        If True, do a full copytree of the source (all state).
    clone_config:
        If True, copy config files (config.yaml, .env, SOUL.md), installed
        skills, and selected profile identity files from the source profile.
    no_alias:
        If True, skip wrapper script creation.
    no_skills:
        If True, create an empty profile with no bundled skills, and write
        a marker file so ``hermes update`` skips re-seeding this profile's
        skills. Mutually exclusive with ``clone_config``/``clone_all`` (those
        explicitly copy skills from the source).

    Returns
    -------
    Path
        The newly created profile directory.
    Nz--no-skills is mutually exclusive with --clone / --clone-from / --clone-all (cloning explicitly copies skills from the source profile).rf   uS   Cannot create a profile named 'default' — it is the built-in profile (~/.hermes).	Profile '' already exists at r   get_hermes_homezSource profile 'ri  z' does not exist at Tsymlinksignore
missing_okr   r     r   )rw  dirs_exist_ok# Per-profile secrets for this Hermes profile.
# API keys and tokens set here override the shell environment.
# Behavioral settings belong in config.yaml, not here.
r   r%  r   )DEFAULT_SOUL_MDzThis profile opted out of bundled-skill seeding (`hermes profile create --no-skills`).
Delete this file to re-enable sync on the next `hermes update`.
FrS  )"r<   r   r   r   r,   FileExistsErrorr   ru  r   r   r   copytreerE   r   r   r   _PROFILE_DIRS_CLONE_CONFIG_FILEScopy2r   r   rD   r-   _CLONE_SUBDIR_FILESparentr   hermes_cli.default_soulr~  r   r+   r   r   r\  _maybe_register_gateway_service)r   rl  rm  rn  ro  rp  r  r   r(   r1   ru  stalesubdirfilenamesrcdstsource_skillsrelpathenv_path	soul_pathr~  s                        r/   create_profiler    s   H j,	J
 	
 #4(E% 	a
 	
 "%(K	%0D[MRSS Jl8(*J/
;J!*-(4J  "#":#9"::Nzl[  Z-j9		
 & 	:E5 ((D(9	: 	$6# 	FF6!(((E	F !/ ! 8+::<%0CLLc*
  6)!HHSXu5!& '1M##%{X/EPTdhi / + 7*::<%/CJJ$$TD$ALLc*+ V#H??		K !	    HHS]E* i'I	?  7 C 	33??T !	 @  +K8
 {((*	'--/!& $E*u  ' ! !F  		  		  		*  		sZ   L2L L* (L9 "M 	LL	L'&L'*	L65L69	MM	MMr   c                 $   t        |       rg g g ddS t        t              j                  j                  j	                         }	 t        j                  t        j                  ddgi t        j                  dt        |       it        |      ddd      }|j                  dk(  rG|j                  j                         r-t        j                   |j                  j                               S |s[t#        d	|j                          |j$                  j                         r)t#        d
|j$                  j                         dd         y# t
        j&                  $ r |st#        d       Y yt(        $ r}|st#        d|        Y d}~yd}~ww xY w)u  Seed bundled skills into a profile via subprocess.

    Uses subprocess because sync_skills() caches HERMES_HOME at module level.
    Returns the sync result dict, or None on failure.

    Profiles that opted out of bundled skills (via ``hermes profile create
    --no-skills`` — which writes ``.no-bundled-skills`` to the profile root)
    are skipped and get an empty-result dict so callers can report
    "opted out" instead of "failed".
    T)copiedupdateduser_modifiedskipped_opt_outz-cziimport json; from tools.skills_sync import sync_skills; r = sync_skills(quiet=True); print(json.dumps(r))HERMES_HOME<   )envcwdr   r   r   r   u%   ⚠ Skill seeding returned exit code z  N   u!   ⚠ Skill seeding timed out (60s)u   ⚠ Skill seeding failed: )r0   r   __file__r  r;   r   r   r   
executabler   r   rD   r   r   r   jsonloadsr   stderrr   r   )r(   r   project_rootr   r   s        r/   seed_profile_skillsr    sc    "+.#	
 	
 >((//779L^^TAB @2::?}c+.>?L!dB
 !fmm&9&9&;::fmm113449&:K:K9LMN}}""$6==..0#6789$$ 56 .qc23s&   B'E -AE  F-F5F

Fc                    g }t               }|j                         s|S t               dz  }t        |j	                               D ]  }|j                         rt
        j                  |j                        s3|j                  dk(  rC|dz  }|j                         rY	 |j                         rt        j                  ||       n|j                  dd       t        j                  t        |      d       |j!                  |j                          |S # t"        $ r'}| st%        d|j                   d|        Y d	}~d	}~ww xY w)
u  Give every named profile that predates per-profile ``.env`` files one.

    Profiles created before the dashboard/CLI started seeding a ``.env``
    (PR #44792) have none, so once the Channels/Keys endpoints became
    profile-scoped those profiles stopped inheriting the root install's
    credentials and showed everything as unconfigured. To avoid breaking
    anyone on update, copy the DEFAULT install's ``.env`` into each named
    profile that lacks one — that preserves the effective credentials those
    profiles were already running with (they previously read the root
    ``.env`` via the process environment). Users can then diverge per
    profile from there.

    Falls back to the placeholder header when the default install has no
    ``.env`` itself. Never overwrites an existing profile ``.env``.

    Returns the list of profile names that received a backfilled ``.env``.
    r   rf   r}  r   r%  r{  u%   ⚠ Could not seed .env for profile 'z': N)r   r   rC   r   r   r   r   r   r,   r   r   r  r   r   r   rD   r:   r-   r   )r   
backfilledrc  default_envr>   r  r   s          r/   backfill_profile_envsr    s/   $ J&(M!*,v5K--/0 R||~^%9%9%**%E::"6>??	R""$[(3##O %	 $  HHS]E*ejj)'R0 	  	R=ejj\QCPQ	Rs   A4D	E EEr   c                 f   	 ddl }	 |j                         }t	        j
                         h}	 |j                  t	        j
                               j                         }|.|j                  |j                         |j                         }|.	 |j                  t	        j
                               j                         }h d}d}g }	|j                  g d      D ]  }
	 |
j                  }|j                  d      }|||v r)||j                  d      |k7  r@|j                  d      xs g }|sXd	j                  |      t        j                  j!                  |d         j#                         }t%        fd
|D              xs |dk(  xs |j'                  d      }|s|D ch c]  }|j#                          }}||z  sd}t)        |      D ]j  \  }}|dv r*|dz   t+        |      k  rt-        ||dz            | k(  s0d} n9|j'                  d      sFt-        |j/                  dd      d         | k(  shd} n |sE	 |
j1                         xs i j                  dd      }|rt3        |      j                         |k(  rd}|r|	j5                  |        |	S # t        $ r g cY S w xY w# t        $ r |}Y w xY w# t        $ r Y Bw xY w# t        $ r d}Y "w xY wc c}w # t        $ r Y mw xY w# |j6                  |j8                  |j:                  f$ r Y Ht        $ r Y Sw xY w)u  PIDs of running Hermes *backends* bound to this profile.

    The ``gateway.pid`` file only tracks the messaging gateway.  A Desktop app
    spawns a headless ``serve`` (or legacy ``dashboard --no-open``) backend per
    profile that holds the profile's SQLite connection open and keeps writing
    sessions/WAL/sandbox files — the writer that makes ``rmtree`` hit
    ``ENOTEMPTY`` (and, pre-fix, resurrected the tree).  ``gateway.pid`` never
    names it, so find it by inspection: a Hermes backend subcommand
    (``serve``/``dashboard``/``gateway``) that is bound to *this* profile either
    by a ``--profile <canon>`` / ``-p <canon>`` selector or by a ``HERMES_HOME``
    that resolves to ``profile_dir``.

    Best-effort and tightly scoped: current-user processes only, backend
    subcommands only (never an interactive ``chat``/``tui``), and never this
    process or its ancestors.  Returns an empty list if ``psutil`` can't
    inspect anything.
    r   N>   rj  ru   	dashboard)zhermes_cli.mainzhermes-gatewaytui_gateway)pidr   usernamecmdliner  r  r   c              3   &   K   | ]  }|v  
 y wNr~   ).0markerjoineds     r/   	<genexpr>z._profile_bound_backend_pids.<locals>.<genexpr>B  s     BFf$Bs   re   F>   -p	--profiler   Tz
--profile==r  r   )psutilr   r;   r-   r   getpidProcessr  addr  r  process_iterinfor   joinr  basenamer   any
startswith	enumerater  r   r   r   r   r:   NoSuchProcessAccessDeniedZombieProcess)r   r(   r  resolved_dirskipr  current_userbackend_tokenshermes_markerspidsprocr  r  argvexe_name	is_hermestoktokensboundienv_homer  s                        @r/   _profile_bound_backend_pidsr     s:   $#"**, iik]D		,335 HHVZZ ]]_F  ~~biik2;;= 7NIND##$JK ;:	99D((5/C{cTk'DHHZ,@L,P88I&,"D XXd^Fww''Q0668HB>BB 1x'1&&x0 
  .22cciik2F2^+ E#D/ 3--!a%#d)2C-d1q5k:eC $^^L1-ciiQ.?.BCuL $  $ 4"99-LHDN$:$:$<$L $ C o;z Ks  	
  #"#  
  D 3, !   $$f&9&96;O;OP 	 		s   J" J3 A K 1K !#K;K;K;4A1K;&K;*K'K;	<K;K;!K;>K;AK,
K;"J0/J03KK	KKK$#K$'K;,	K85K;7K88K;;&L0$L0/L0c                 "   t        | |      }|sy	 ddlmm} |D ]  }	  ||        t        j                         dz   }t        j                         |k  rBt        fd|D              sn-t        j                  d       t        j                         |k  rB|D ]  } |      s	  ||d        t        d	t        |       d
       y# t        $ r Y yw xY w# t
        t        t        f$ r Y w xY w# t
        t        t        f$ r Y qw xY w)a  Terminate any Desktop-spawned / stray backends bound to this profile.

    Complements ``_stop_gateway_process`` (which only knows ``gateway.pid``):
    without this, a live ``serve``/``dashboard`` backend keeps creating files
    under the profile dir while ``rmtree`` walks it, so the final ``rmdir``
    fails with ``ENOTEMPTY`` and the delete doesn't converge.  Best-effort:
    any failure is reported and swallowed so it never makes delete worse.
    Nr   )_pid_existsterminate_pidg      $@c              3   .   K   | ]  } |        y wr  r~   )r  r  r  s     r/   r  z)_stop_profile_backends.<locals>.<genexpr>  s     4;s#4s         ?Tforceu   ✓ Stopped z profile backend process(es))r  r7  r  r  r   ProcessLookupErrorPermissionErrorr-   rF  r  sleepr   r  )r   r(   r  _terminate_pidr  deadliner  s         @r/   _stop_profile_backendsr  p  s    'uk:DO  	3 yy{T!H
))+
 4t44

3 ))+
 
  ss$/ 
LT#?
@A/   #OW= 		 'A s4   C C)
C7	CCC43C47DDc                 X   d}d}t        |      D ]  }	 	 t        j                  | |        y ||y# t        $ r t        j                  | |       Y  yw xY w# t        $ rE}|}| j                         sY d}~ y||dz
  k  rt        j                  d|dz   z         Y d}~d}~ww xY w)a  ``shutil.rmtree`` with a short retry loop for transient races.

    Even after stopping the gateway and profile backends, a just-terminated
    process can leave in-flight writes (SQLite ``-wal``/``-shm`` checkpoints,
    sandbox temp files) that land after ``rmtree`` has walked past a directory,
    surfacing as ``ENOTEMPTY`` (POSIX) or a transient ``PermissionError``
    (Windows file lock still releasing).  A few spaced retries let those settle
    instead of failing the whole delete on a race the next attempt would win.
       N)onexc)onerrorr   g333333?)ranger   rmtree	TypeErrorr-   r,   rF  r  )r(   onexc_handlerattemptslast_excattemptr   s         r/   _rmtree_with_retryr    s     H#H? 0	0Bk? 0    Bk=AB  	0H%%'A%

3'A+./	0s3   4 AAAA	B)$B$<#B$$B)yesc                 D   t        |       }t        |       |dk(  rt        d      t        |      }|j	                         st        d| d      t        |      \  }}t        |      }t        |      }t        |      \  }}	}
t        d|        t        d|        |rt        d| |rd| d	nd
z          |rt        d|        |r%t        d| d|	xs d        |
rt        d|
        dg}t               |z  }|j                         }|r|j                  d| d	       t        d       |D ]  }t        d|         |rt        d       |s:t                	 t        d| d      j                         }||k7  rt        d       |S t%        ||       t'        |       |rt)        |       t+        ||       |rt-        |      rt        d|        d}	 d }t/        ||       t        d|        	 t3               }||k(  rt5        d       t        d       |t7        d| d|       |t        d | d!       |S # t         t"        f$ r t        d       |cY S w xY w# t0        $ r}t        d| d|        |}Y d}~d}~ww xY w# t0        $ r Y {w xY w)"zDelete a profile, its wrapper script, and its gateway service.

    Stops the gateway if running. Disables systemd/launchd service first
    to prevent auto-restart.

    Returns the path that was removed.
    rf   zZCannot delete the default profile (~/.hermes).
To remove everything, use: hermes uninstallrr  ' does not exist.z

Profile: z	Path:    z	Model:   z (r   r   z	Skills:  zDistribution: @?zInstalled from: z;All config, API keys, memories, sessions, skills, cron jobszCommand alias (z
This will permanently delete:u     • u0     ⚠ Gateway is running — it will be stopped.zType 'z' to confirm: z
Cancelled.z
Cancelled.u   ✓ Removed Nc                    ddl }t        |t              r|d   }t        |t              r	 t	        j
                  |t	        j                   |      j                  |j                  z         t        j                  j                  |      }|rA	 t	        j
                  |t	        j                   |      j                  |j                  z          | |       y # t        $ r Y ww xY w# t        $ r Y $w xY w)a  onexc/onerror handler: add +w on PermissionError so rmtree can proceed.

            Handles two cases on NixOS (and other systems with read-only
            copies from immutable stores):
            1. The path itself isn't writable (e.g. a file with mode 0444)
            2. The *parent* directory isn't writable (e.g. mode 0555)

            Compatible with both the ``onexc`` API (3.12+, receives an
            exception instance) and the ``onerror`` API (3.11-, receives
            ``sys.exc_info()`` tuple).
            r   Nr   )r   r   tupler  r   r   r   S_IWUSRr-   r  dirname)funcr  r   _statr  s        r/   _make_writablez&delete_profile.<locals>._make_writable  s     !
 #u%!f#/HHT2774=#8#85==#HI .)@)@5==)PQ T
   # s%   A C A C& 	C#"C#&	C21C2u   ⚠ Could not remove r   u#   ✓ Active profile reset to defaultz#Could not remove profile directory z

Profile 'z
' deleted.)r   r   r<   r   r   r   r0  r9  rO  r,  r   r   r,   r:   inputr   KeyboardInterruptEOFError_cleanup_gateway_service!_maybe_unregister_gateway_service_stop_gateway_processr  r   r  r   get_active_profileset_active_profileRuntimeError)r   r  r   r(   rl   r  
gw_runningr  r_  r`  ra  itemsr   has_wrapperitemconfirmremove_errorr  r   ri  s                       r/   delete_profiler	    s    #4(E% 	:
 	

 "%(K)E72C DEE )5OE8'4J,K+B;+O(I|[	Kw
 	Ik]
#$	%!r(1%5rJK	+'(yk<+>3*?@A$[M23 	FE
 $%-L%%'K|nA67	
+, tfo@A 	fUG>:;AACG e, UK0 &e, k* 5+.  'L/0 &*L*#	J 	;7[M*+#%U?y)78 @RP\~^_eqq	Kwj
)*s "8, 	.!	P  %k]"QC89  s<   #I  I* >%J I'&I'*	J3JJ	JJc                    	 ddl m}  |       dk7  ryddl m}  |       }|j                         sy	 |j                  | d       y# t        $ r Y yt        $ r Y yw xY w# t        $ r Y yt        $ r}t        d|        Y d}~yd}~ww xY w)	u  Register a profile's gateway with s6 inside the container.

    No-op on host (systemd/launchd/windows) — those backends raise
    ``NotImplementedError`` on ``register_profile_gateway`` and the
    existing per-profile unit-generation paths handle lifecycle.

    Best-effort: any error (no backend detected, s6 not yet ready,
    etc.) is logged and swallowed so profile creation doesn't fail
    because the s6 supervision tree is in a weird state. The user
    can re-register manually later via the gateway start command,
    which goes through the same dispatch path.

    Port selection: each supervised profile gateway loads its own
    ``HERMES_HOME`` and binds the port resolved by ``gateway/config.py``
    from that profile's environment — ``API_SERVER_PORT`` (or
    ``platforms.api_server.extra.port`` in the profile's
    ``config.yaml``), defaulting to 8642. There is no ``[gateway] port``
    key and no Python-side allocator (PR #30136 review item I5 retired
    the SHA-256-derived range [9200, 9800) as dead code), so two
    profiles that both leave the port at its default will both try to
    bind 8642 — give each profile a distinct ``API_SERVER_PORT`` in its
    ``.env``.

    Host short-circuit: check ``detect_service_manager()`` first and
    return immediately if it isn't ``"s6"``. This keeps host
    (systemd/launchd/windows) profile creation completely silent —
    no ``get_service_manager()`` call, no exception path, no chance
    of the ``⚠ Could not register s6 gateway service`` warning ever
    rendering on a non-container machine. The earlier
    ``supports_runtime_registration()`` check still catches the case
    where detection somehow returns ``"s6"`` but the backend isn't
    actually the S6 one.
    r   detect_service_managers6Nget_service_managerF)	start_nowu+   ⚠ Could not register s6 gateway service: )	hermes_cli.service_managerr  r  r  r   supports_runtime_registrationregister_profile_gatewayr<   r   r   r  r  mgrr   s        r/   r  r  N  s    DE!#t+B!# ,,.C$$\U$C    		   	 C;C5ABBCs9   A A A 	AAA	B
*B
2BB
c                    	 ddl m}  |       dk7  ryddl m}  |       }|j                         sy	 |j                  |        y# t        $ r Y yt        $ r Y yw xY w# t        $ r}t        d|        Y d}~yd}~ww xY w)u  Tear down a profile's s6 gateway service inside the container.

    No-op on host. Idempotent: absent services are silently skipped
    by ``unregister_profile_gateway``.

    Same host short-circuit as :func:`_maybe_register_gateway_service`
    — see that docstring.
    r   r  r  Nr  u-   ⚠ Could not unregister s6 gateway service: )r  r  r  r  r   r  unregister_profile_gatewayr   r  s        r/   r  r    s    	E!#t+B!#
 ,,.E&&|4     E=cUCDDEs3   A A A 	AAA	A>&A99A>c                 .   ddl }t        j                  j                  d      }	 t	        |      t        j                  d<   ddlm}m} |j                         dk(  r |       }t        j                         dz  dz  dz  | d	z  }|j                         rt        j                  d
dd|gddd       t        j                  d
dd|gddd       |j                  d       t        j                  g dddd       t        d| d       nl|j                         dk(  rY |       }|j                         rBt        j                  ddt	        |      gddd       |j                  d       t        d       ||t        j                  d<   ydt        j                  v rt        j                  d= yy# t         $ r}	t        d|	        Y d}	~	Wd}	~	ww xY w# ||t        j                  d<   w dt        j                  v rt        j                  d= w w xY w)z9Disable and remove systemd/launchd service for a profile.r   Nr  )get_service_nameget_launchd_plist_pathLinuxz.configsystemduserz.service	systemctl--userdisableTF
   )r   checkr   stopry  )r  r  zdaemon-reloadu   ✓ Service z removedDarwin	launchctlunloadu   ✓ Launchd service removedu   ⚠ Service cleanup: )r   r   r   r   rD   hermes_cli.gatewayr  r  systemr   r   r,   r   r   r   r   r   )
r   r(   	_platformold_homer  r  svc_namesvc_file
plist_pathr   s
             r/   r  r    s     zz~~m,H&*$'$4

=!O(')Hyy{Y.:VC
RZF[[H  (Ix@#'ub  (FH=#'ub 40<#'ub XJh788+/1J  " (C
O<#'ub !!T!234 (0BJJ}%bjj(

=) )  +%aS)**+ (0BJJ}%bjj(

=) )s*   EF4 4	G=GG GG <Hc                 p   ddl }| dz  }|j                         sy	 |j                         j                         }|j	                  d      rt        j                  |      ndt        |      i}t        |d         }ddlm	} ddlm
}  ||       t        d      D ],  }|j                  d	        ||      rt        d
| d        y 	  ||d       t        d| d       y# t        t        f$ r Y !w xY w# t        t         f$ r t        d       Y yt"        $ r}	t        d|	        Y d}	~	yd}	~	ww xY w)z0Stop a running gateway process via its PID file.r   Nr   {r  )r  )r     r  u   ✓ Gateway stopped (PID r   Tr  u   ✓ Gateway force-stopped (PID u   ✓ Gateway already stoppedu   ⚠ Could not stop gateway: )rF  r,   r   r   r  r  r  r#  r7  r  r  r  r  r   r  r-   r  r   )
r(   _timepid_filerawr+  r  r  r  _r   s
             r/   r  r    s.   ]*H??2  "((*"%.."5tzz#E3s8;L$u+ 	C.s r 	AKKs#1#a89			3d+ 	/uA67 #G, 		 0 -+, 2,QC0112sN   BC9 7C9 C9 

C$ C9 $C63C9 5C66C9 9D5D5D00D5c                      t               } 	 | j                         j                         }|sy|S # t        t        t
        f$ r Y yw xY w)ztRead the sticky active profile name.

    Returns ``"default"`` if no active_profile file exists or it's empty.
    rf   )r   r   r   r   r  r-   )r  r   s     r/   r   r      sK    
 $%D~~%%'17; s    / / AAc                 \   t        |       }t        |       |dk7  rt        |      st        d| d|       t	               }|j
                  j                  dd       |dk(  r|j                  d       y	|j                  d      }|j                  |dz          |j                  |       y	)
zlSet the sticky active profile.

    Writes to ``~/.hermes/active_profile``. Use ``"default"`` to clear.
    rf   rr  8' does not exist. Create it with: hermes profile create Tr   ry  r8   
N)r   r   r   r   r   r  r   r   with_suffixr   replace)r   r   r  ra   s       r/   r  r    s    
 #4(E% 	."7w 55:G=
 	

 $%DKKdT2	t$ v&ut|$DrF   c                  Z   ddl m}   |        }|j                         }t               j                         }||k(  ryt	               j                         }	 |j                  |      }|j                  }t        |      dk(  rt        j                  |d         r|d   S y# t        $ r Y yw xY w)a%  Infer the current profile name from HERMES_HOME.

    Returns ``"default"`` if HERMES_HOME is not set or points to ``~/.hermes``.
    Returns the profile name if HERMES_HOME points into ``~/.hermes/profiles/<name>``.
    Returns ``"custom"`` if HERMES_HOME is set to an unrecognized path.
    r   rt  rf   r   custom)r   ru  r;   rC   r   relative_topartsr  r   r   r<   )ru  hermes_homeresolveddefault_resolvedrc  relr>  s          r/   rh  rh  (  s     1!#K""$H/199;##&(002M""=1		u:?~33E!H=8O   s   AB 	B*)B*root_dirc                 8     dt         dt        dt        f fd}|S )u  Return an *ignore* callable for :func:`shutil.copytree`.

    Two-tier filtering:

    * **Root-level allow-list** — only entries whose name appears in
      ``_DEFAULT_EXPORT_INCLUDE_ROOT`` survive. Everything else (such as
      an unrelated ``x11-dev/`` directory in a Docker deployment where
      HERMES_HOME equals the cwd) is excluded. Blacklisting was tried
      first and proved unable to anticipate every non-Hermes file the
      user may have lying alongside HERMES_HOME (#58394).
    * **Universal exclusions at any depth** — ``__pycache__``, sockets,
      temp files; plus npm lockfiles, which may appear at the root.

    All other profile artifacts are copied through untouched.
    r3   contentsr)   c                     t               }|D ]@  }|dk(  s|j                  d      r|j                  |       +|dv s0|j                  |       B t        |       k(  r|j	                  d |D               |S )Nr6   )r7   r8   >   package.jsonpackage-lock.jsonc              3   2   K   | ]  }|t         vs|  y wr  )_DEFAULT_EXPORT_INCLUDE_ROOT)r  r>   s     r/   r  z:_default_export_ignore.<locals>._ignore.<locals>.<genexpr>d  s      u<X/Xs   )setr9   r  r   rt   )r3   rE  r=   r>   rC  s       r/   rB   z'_default_export_ignore.<locals>._ignoreX  s|    u 	#E%8I)JE"??E"	# 	?h&NN #+  rF   )rD   listrK  )rC  rB   s   ` r/   _default_export_ignorerM  G  s$    "3 $ 3 " NrF   output_pathc           	        
 ddl }t        |       }t        |       t        |      }|j	                         st        d| d      t        |      }t        |      j                  d      j                  d      }|dk(  rm|j                         5 }t        |      dz  }t        j                  ||dt        |      	       t        j                  |d
|d      }	t        |	      cddd       S |j                         5 }t        |      |z  }ddh
t        j                  ||d
fd	       t        j                  |d
||      }	t        |	      cddd       S # 1 sw Y   txY w# 1 sw Y   yxY w)zMExport a profile to a tar.gz archive.

    Returns the output file path.
    r   Nrr  r  z.tar.gzz.tgzrf   Trv  gztarrU   r   c                      t        |      z  S r  )rK  )drE  _CREDENTIAL_FILESs     r/   <lambda>z export_profile.<locals>.<lambda>  s    '83x='H rF   )tempfiler   r   r   r   r   r   rD   removesuffixTemporaryDirectoryr   r  rM  make_archive)r   rN  rU  r   r(   outputbasetmpdirstagedr   rS  s             @r/   export_profiler]  l  sd   
 "4(E% !%(K)E72C DEE+Fv;##I.;;FCD	 ((* 		 f&\I-FOO-k:	 ((w	JF<		  		  
	$	$	& 
&f%(&1H		
 $$T7FEBF|
 
		  		 
 
s   AE<AE#E #E,member_namec                 X   | j                  dd      }t        |      }t        |       }|r,|j                         s|j                         s|j                  rt        d|        |j                  D cg c]	  }|dvs| }}|rt        d |D              rt        d|        |S c c}w )z4Return safe path parts for a profile archive member.\/zUnsafe archive member path: >   r   .c              3   &   K   | ]	  }|d k(    yw)z..Nr~   )r  parts     r/   r  z3_normalize_profile_archive_parts.<locals>.<genexpr>  s     77s   )r:  r   r   is_absolutedriver<   r>  r  )r^  normalized_name
posix_pathwindows_pathrd  r>  s         r/    _normalize_profile_archive_partsrj    s    !))$4O/J";/L !!###%7}EFF(..Hd$i2GTHEHC7777}EFFL Is   3	B'=B'archivedestinationc           	         ddl }|j                  | d      5 }|j                         D ]  }t        |j                        } |j
                  | }|j                         r|j                  dd       L|j                         st        d|j                         |j                  j                  dd       |j                  |      }|t        d|j                         |5  t        |d      5 }t        j                  ||       ddd       ddd       	 t        j                  ||j                   d	z          	 ddd       y# 1 sw Y   BxY w# 1 sw Y   FxY w# t"        $ r Y Gw xY w# 1 sw Y   yxY w)
zAExtract a profile archive without allowing path escapes or links.r   Nr:gzTr   z!Unsupported archive member type: zCannot read archive member: wbi  )tarfiler  
getmembersrj  r   joinpathisdirr   isfiler<   r  extractfiler   copyfileobjr   r   moder-   )	rk  rl  rp  tfmemberr>  r   	extractedr  s	            r/   _safe_extract_profile_archiver{    s_   	gv	& "mmo 	F4V[[AE)[))51F||~TD9==? 7}E  MMt<v.I  #?}!MNN 3D. 3#""9c23 3u!45-	 &3 3 3 3
  1 s`   CE3&E3E	
EE3#E#>E3EEE E3#	E0,E3/E00E33E<c                    ddl }|j                  | d      5 }|j                         D ch c]:  }t        |j                        }t        |      dkD  s|j                         r|d   < }}}|sC|j                         D ch c]*  }|j                         rt        |j                        d   , }}ddd       |S c c}}w c c}w # 1 sw Y   S xY w)a  Return the archive's top-level directory names.

    Profile imports expect exactly one root directory. Inspecting the archive
    before extraction lets us stage the import safely instead of mutating a
    live profile tree first and reconciling names later.
    r   Nrn  r   )rp  r  rq  rj  r   r  rs  )rk  rp  rx  ry  r>  top_dirss         r/   _inspect_profile_archive_rootsr~    s     	gv	& " --/
:6;;G5zA~ !H
 
  !mmo<<> 1=a@H  O
 Os(   C?B<*C/C0C<CCarchive_pathc                 "   ddl }t        |       }|j                         st        d|       t	        |      }t        |      dk(  r|j                         nd}|xs |}|st        d      |t        d      t        |      }t        |       |dk(  rt        d      t        |      }|j                         rt        d	| d
|       t               }	|	j                  dd       |j                  d      5 }
t        |
      }t        ||       ||z  }|j!                         st        d|       |}||k7  r||z  }|j#                  |       t%        j&                  t)        |      t)        |             ddd       |S # 1 sw Y   |S xY w)zImport a profile from a tar.gz archive.

    If *name* is not given, infers it from the archive's top-level directory.
    Returns the imported profile directory.
    r   NzArchive not found: r   zpCannot determine profile name from archive. Specify it explicitly: hermes profile import <archive> --name <name>z=Profile archive must contain exactly one top-level directory.rf   u   Cannot import as 'default' — that is the built-in root profile (~/.hermes). Specify a different name: hermes profile import <archive> --name <name>rr  rs  Tr   hermes_profile_import_)r	  z,Profile archive root is missing or invalid: )rU  r   r,   r   r~  r  popr<   r   r   r   r  r   r   rW  r{  r   renamer   moverD   )r  r   rU  rk  r}  archive_rootinferred_namer   r(   rc  r[  staging_rootrz  final_sources                 r/   import_profiler    s    < G>>"5gY ?@@-g6H%(]a%78<<>TL(LMS
 	
 K
 	
 #=1E% 	V
 	

 "%(K	%0D[MRSS&(Mt4		$	$,D	$	E 9F|%g|< </	!>|nM  !5 '%/L\*C%s;'789" #9" s   :B FFold_namenew_namenew_dirc                 n   d|  }d|  }d| }|dz  t               dz  t        j                         dz  dz  g}t               }|D ]  }	 |j	                         }	|	|v s|j                         s*|j                  |	       	 t        j                  |j                  d            }
|
j                  d      }t        |t              s||v r|n|}||vr||v rt        d	| d
|        ||   }t        |t              rKd|vrG|j!                  d      r|j#                  dd      d   }nd|v r|j#                  dd      d   n|}||d<   |j%                  |      ||<   |j'                  |j(                  dz         }	 |j+                  t        j,                  |
dd      dz   d       |j/                  |       t        d| d|         y# t
        $ r |}	Y w xY w# t
        t        j                  f$ r Y w xY w# t
        $ r' 	 |j1                  d       n# t
        $ r Y nw xY wY w xY w)zGRename Honcho host blocks for a renamed profile without changing peers.hermes_zhermes.zhoncho.jsonz.honchozconfig.jsonr   r%  hostsu$   ⚠ Honcho host block not migrated: z already exists in aiPeerr4  r   rb  r8   rE  F)indentensure_asciir8  Try  u   ✓ Honcho host updated:     → N)rC   r   r   rK  r;   r-   r   r  r  r  r   JSONDecodeErrorr   r   r)  r   r  r   r  r9  r   r   dumpsr:  r   )r  r  r  old_hostlegacy_old_hostnew_hostr   seenr  r@  r3  r  source_hostblockbarera   s                   r/   _migrate_honcho_profile_hostr  *  se   
#Hz*O
#H 	- "]2		i-/J eD ,H	||~H t4<<>	**T^^W^=>C  %&"*e"3he#u8
BUVZU[\]k"eT"xu'<%%i0"((a037:k7I{((a03{"E(O))K0ht{{V34	NN4::c!%H4OZaNbKK 	)+eH:FGY,H  	H	 --. 		4  	

d
+ 	sZ   G%G$ =HG! G!$H H	H4H! H4!	H-*H4,H--H43H4c                    t        |       }t        |      }t        |       t        |       |dk(  rt        d      |dk(  rt        d      t        |      }t        |      }|j	                         st        d| d      |j                         rt        d| d      t        |      rt        ||       t        |       |j                  |       t        d|j                   d|j                          t        |||       t        |       t!        |      }|st#        |       t        d	|        nt        d
| d|        	 t%               |k(  rt'        |       t        d|        |S # t(        $ r Y |S w xY w)zrRename a profile: directory, wrapper script, service, active_profile.

    Returns the new profile directory.
    rf   z"Cannot rename the default profile.u.   Cannot rename to 'default' — it is reserved.rr  r  z' already exists.u   ✓ Renamed r  u   ✓ Alias updated: u   ⚠ Cannot create alias 'u   ' — u   ✓ Active profile updated: )r   r   r<   r   r   r   r,   r  r9  r  r  r  r   r   r  r   r   r   r   r  r   )r  r  	old_canon	new_canonold_dirr  	collisions          r/   rename_profiler  f  s   
 'x0I&x0I)$)$I=>>IIJJi(Gi(G>>)I;6G HII~~	)4EFGG g& G4g& NN7	LeGLL>
:; !Iw? )$%i0Ii(#I;/0))F9+FG9,y)0<= N  Ns   
&E2 2	E?>E?c                     t        |       }t        |       t        |      }|dk7  r!|j                         st	        d| d|       t        |      S )zResolve a profile name to a HERMES_HOME path string.

    Called early in the CLI entry point, before any hermes modules
    are imported, to set the HERMES_HOME environment variable.
    rf   rr  r7  )r   r   r   r   r   rD   )r   r   r(   s      r/   resolve_profile_envr    sc     #<0E% !%(K	+"4"4"6w 55:G=
 	

 {rF   r  )NFFFFN)F)dr   r  r   rer   r   r   r   r   rF  dataclassesr   pathlibr   r   r   typingr   r   r	   agent.skill_utilsr
   compiler   r  r  r  r   rL  rD   r!  	frozensetr    r'   r+   r"  r0   rE   _DEFAULT_EXPORT_EXCLUDE_ROOTrJ  r   r   r   rC   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r)  r   r  r  r,  r0  r9  r:  floatr#  rG  rC  rO  rQ  rT  r\  re  rk  r  r  r  r  r  r  r	  r  r  r  r  r   r  rh  rM  r]  rj  r{  rK  r~  r  r  r  r  r~   rF   r/   <module>r     s+  *  	 	     
  ! 8 8 ( ( 49:"    $s) , 3< = 3 3 , 3< = 3 3   0 D T 34 3t  ) *   B  ) *      
   !  3D 3%$ %9$ 9
*$ *  $
 
 
<
c 
d 
 (# ($ (+ + +' ' 'TG G( (Xc] (htn (V  <T d >G G# G6  .c3h .j # # #@   %  2D U &  R ;= DeE5#$5667 < d u 6t  P(D (T (4 D 8 "&'+	OO #O tn	O
 
OJEtK( EP& &$uS$Y/?*@ &V !%!%~
~~ ~ 	~
 ~ ~ #~ 
~B*T *$ *8D> *Z1 1$s) 1hms m m$s) m`&B# &BD &BT &BRD D <T T4 TD Tn9C# 9C$ 9CxEC ED E6-*3 -*T -*d -*`$2t $2 $2VC S T 2 >"T "J+ +3 +4 +\# $s) (4 d t @D SX 2< <HSM <T <F9H3 9H# 9H 9HQU 9Hx4S 4C 4D 4vc c rF   