@@ -119,6 +119,70 @@ MIGRATION_OPTION_METADATA: Dict[str, Dict[str, str]] = {
" label " : " Archive unmapped docs " ,
" description " : " Archive compatible-but-unmapped docs for later manual review. " ,
} ,
" mcp-servers " : {
" label " : " MCP servers " ,
" description " : " Import MCP server definitions from OpenClaw into Hermes config.yaml. " ,
} ,
" plugins-config " : {
" label " : " Plugins configuration " ,
" description " : " Archive OpenClaw plugin configuration and installed extensions for manual review. " ,
} ,
" cron-jobs " : {
" label " : " Cron / scheduled tasks " ,
" description " : " Import cron job definitions. Archive for manual recreation via ' hermes cron ' . " ,
} ,
" hooks-config " : {
" label " : " Hooks and webhooks " ,
" description " : " Archive OpenClaw hook configuration (internal hooks, webhooks, Gmail integration). " ,
} ,
" agent-config " : {
" label " : " Agent defaults and multi-agent setup " ,
" description " : " Import agent defaults (compaction, context, thinking) into Hermes config. Archive multi-agent list. " ,
} ,
" gateway-config " : {
" label " : " Gateway configuration " ,
" description " : " Import gateway port and auth settings. Archive full gateway config for manual setup. " ,
} ,
" session-config " : {
" label " : " Session configuration " ,
" description " : " Import session reset policies (daily/idle) into Hermes session_reset config. " ,
} ,
" full-providers " : {
" label " : " Full model provider definitions " ,
" description " : " Import custom model providers (baseUrl, apiType, headers) into Hermes custom_providers. " ,
} ,
" deep-channels " : {
" label " : " Deep channel configuration " ,
" description " : " Import extended channel settings (Matrix, Mattermost, IRC, group configs). Archive complex settings. " ,
} ,
" browser-config " : {
" label " : " Browser configuration " ,
" description " : " Import browser automation settings into Hermes config.yaml. " ,
} ,
" tools-config " : {
" label " : " Tools configuration " ,
" description " : " Import tool settings (exec timeout, sandbox, web search) into Hermes config.yaml. " ,
} ,
" approvals-config " : {
" label " : " Approval rules " ,
" description " : " Import approval mode and rules into Hermes config.yaml approvals section. " ,
} ,
" memory-backend " : {
" label " : " Memory backend configuration " ,
" description " : " Archive OpenClaw memory backend settings (QMD, vector search, citations) for manual review. " ,
} ,
" skills-config " : {
" label " : " Skills registry configuration " ,
" description " : " Archive per-skill enabled/config/env settings from OpenClaw skills.entries. " ,
} ,
" ui-identity " : {
" label " : " UI and identity settings " ,
" description " : " Archive OpenClaw UI theme, assistant identity, and display preferences. " ,
} ,
" logging-config " : {
" label " : " Logging and diagnostics " ,
" description " : " Archive OpenClaw logging and diagnostics configuration. " ,
} ,
}
MIGRATION_PRESETS : Dict [ str , set [ str ] ] = {
" user-data " : {
@@ -139,6 +203,22 @@ MIGRATION_PRESETS: Dict[str, set[str]] = {
" shared-skills " ,
" daily-memory " ,
" archive " ,
" mcp-servers " ,
" agent-config " ,
" session-config " ,
" browser-config " ,
" tools-config " ,
" approvals-config " ,
" deep-channels " ,
" full-providers " ,
" plugins-config " ,
" cron-jobs " ,
" hooks-config " ,
" memory-backend " ,
" skills-config " ,
" ui-identity " ,
" logging-config " ,
" gateway-config " ,
} ,
" full " : set ( MIGRATION_OPTION_METADATA ) ,
}
@@ -578,6 +658,28 @@ class Migrator:
) ,
)
self . run_if_selected ( " archive " , self . archive_docs )
# ── v2 migration modules ──────────────────────────────
self . run_if_selected ( " mcp-servers " , lambda : self . migrate_mcp_servers ( config ) )
self . run_if_selected ( " plugins-config " , lambda : self . migrate_plugins_config ( config ) )
self . run_if_selected ( " cron-jobs " , lambda : self . migrate_cron_jobs ( config ) )
self . run_if_selected ( " hooks-config " , lambda : self . migrate_hooks_config ( config ) )
self . run_if_selected ( " agent-config " , lambda : self . migrate_agent_config ( config ) )
self . run_if_selected ( " gateway-config " , lambda : self . migrate_gateway_config ( config ) )
self . run_if_selected ( " session-config " , lambda : self . migrate_session_config ( config ) )
self . run_if_selected ( " full-providers " , lambda : self . migrate_full_providers ( config ) )
self . run_if_selected ( " deep-channels " , lambda : self . migrate_deep_channels ( config ) )
self . run_if_selected ( " browser-config " , lambda : self . migrate_browser_config ( config ) )
self . run_if_selected ( " tools-config " , lambda : self . migrate_tools_config ( config ) )
self . run_if_selected ( " approvals-config " , lambda : self . migrate_approvals_config ( config ) )
self . run_if_selected ( " memory-backend " , lambda : self . migrate_memory_backend ( config ) )
self . run_if_selected ( " skills-config " , lambda : self . migrate_skills_config ( config ) )
self . run_if_selected ( " ui-identity " , lambda : self . migrate_ui_identity ( config ) )
self . run_if_selected ( " logging-config " , lambda : self . migrate_logging_config ( config ) )
# Generate migration notes
self . generate_migration_notes ( )
return self . build_report ( )
def run_if_selected ( self , option_id : str , func ) - > None :
@@ -1459,6 +1561,776 @@ class Migrator:
else :
self . record ( " archive " , source , destination , " archived " , reason )
# ── MCP servers ─────────────────────────────────────────────
def migrate_mcp_servers ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
mcp_raw = ( config . get ( " mcp " ) or { } ) . get ( " servers " ) or { }
if not mcp_raw :
self . record ( " mcp-servers " , None , None , " skipped " , " No MCP servers found in OpenClaw config " )
return
hermes_cfg_path = self . target_root / " config.yaml "
hermes_cfg = load_yaml_file ( hermes_cfg_path )
existing_mcp = hermes_cfg . get ( " mcp_servers " ) or { }
added = 0
for name , srv in mcp_raw . items ( ) :
if not isinstance ( srv , dict ) :
continue
if name in existing_mcp and not self . overwrite :
self . record ( " mcp-servers " , f " mcp.servers. { name } " , f " mcp_servers. { name } " , " conflict " ,
" MCP server already exists in Hermes config " )
continue
hermes_srv : Dict [ str , Any ] = { }
# STDIO transport
if srv . get ( " command " ) :
hermes_srv [ " command " ] = srv [ " command " ]
if srv . get ( " args " ) :
hermes_srv [ " args " ] = srv [ " args " ]
if srv . get ( " env " ) :
hermes_srv [ " env " ] = srv [ " env " ]
if srv . get ( " cwd " ) :
hermes_srv [ " cwd " ] = srv [ " cwd " ]
# HTTP/SSE transport
if srv . get ( " url " ) :
hermes_srv [ " url " ] = srv [ " url " ]
if srv . get ( " headers " ) :
hermes_srv [ " headers " ] = srv [ " headers " ]
if srv . get ( " auth " ) :
hermes_srv [ " auth " ] = srv [ " auth " ]
# Common fields
if srv . get ( " enabled " ) is False :
hermes_srv [ " enabled " ] = False
if srv . get ( " timeout " ) :
hermes_srv [ " timeout " ] = srv [ " timeout " ]
if srv . get ( " connectTimeout " ) :
hermes_srv [ " connect_timeout " ] = srv [ " connectTimeout " ]
# Tool filtering
tools_cfg = srv . get ( " tools " ) or { }
if tools_cfg . get ( " include " ) or tools_cfg . get ( " exclude " ) :
hermes_srv [ " tools " ] = { }
if tools_cfg . get ( " include " ) :
hermes_srv [ " tools " ] [ " include " ] = tools_cfg [ " include " ]
if tools_cfg . get ( " exclude " ) :
hermes_srv [ " tools " ] [ " exclude " ] = tools_cfg [ " exclude " ]
# Sampling
sampling = srv . get ( " sampling " )
if sampling and isinstance ( sampling , dict ) :
hermes_srv [ " sampling " ] = {
k : v for k , v in {
" enabled " : sampling . get ( " enabled " ) ,
" model " : sampling . get ( " model " ) ,
" max_tokens_cap " : sampling . get ( " maxTokensCap " ) or sampling . get ( " max_tokens_cap " ) ,
" timeout " : sampling . get ( " timeout " ) ,
" max_rpm " : sampling . get ( " maxRpm " ) or sampling . get ( " max_rpm " ) ,
} . items ( ) if v is not None
}
existing_mcp [ name ] = hermes_srv
added + = 1
self . record ( " mcp-servers " , f " mcp.servers. { name } " , f " config.yaml mcp_servers. { name } " ,
" migrated " , servers_added = added )
if added > 0 and self . execute :
self . maybe_backup ( hermes_cfg_path )
hermes_cfg [ " mcp_servers " ] = existing_mcp
dump_yaml_file ( hermes_cfg_path , hermes_cfg )
# ── Plugins ───────────────────────────────────────────────
def migrate_plugins_config ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
plugins = config . get ( " plugins " ) or { }
if not plugins :
self . record ( " plugins-config " , None , None , " skipped " , " No plugins configuration found " )
return
# Archive the full plugins config
if self . archive_dir and self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " plugins-config.json "
dest . write_text ( json . dumps ( plugins , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " plugins-config " , " openclaw.json plugins.* " , str ( dest ) , " archived " ,
" Plugins config archived for manual review " )
else :
self . record ( " plugins-config " , " openclaw.json plugins.* " , " archive/plugins-config.json " ,
" archived " if not self . execute else " migrated " , " Would archive plugins config " )
# Copy extensions directory if it exists
ext_dir = self . source_root / " extensions "
if ext_dir . is_dir ( ) and self . archive_dir :
dest_ext = self . archive_dir / " extensions "
if self . execute :
shutil . copytree ( ext_dir , dest_ext , dirs_exist_ok = True )
self . record ( " plugins-config " , str ( ext_dir ) , str ( dest_ext ) , " archived " ,
" Extensions directory archived " )
# Extract any plugin env vars
entries = plugins . get ( " entries " ) or { }
for plugin_name , plugin_cfg in entries . items ( ) :
if isinstance ( plugin_cfg , dict ) :
env_vars = plugin_cfg . get ( " env " ) or { }
api_key = plugin_cfg . get ( " apiKey " )
if api_key and self . migrate_secrets :
env_key = f " PLUGIN_ { plugin_name . upper ( ) . replace ( ' - ' , ' _ ' ) } _API_KEY "
self . _set_env_var ( env_key , api_key , f " plugins.entries. { plugin_name } .apiKey " )
# ── Cron jobs ─────────────────────────────────────────────
def migrate_cron_jobs ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
cron = config . get ( " cron " ) or { }
if not cron :
self . record ( " cron-jobs " , None , None , " skipped " , " No cron configuration found " )
return
# Archive the full cron config
if self . archive_dir and self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " cron-config.json "
dest . write_text ( json . dumps ( cron , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " cron-jobs " , " openclaw.json cron.* " , str ( dest ) , " archived " ,
" Cron config archived. Use ' hermes cron ' to recreate jobs manually. " )
else :
self . record ( " cron-jobs " , " openclaw.json cron.* " , " archive/cron-config.json " ,
" archived " , " Would archive cron config " )
# Also check for cron store files
cron_store = self . source_root / " cron "
if cron_store . is_dir ( ) and self . archive_dir :
dest_cron = self . archive_dir / " cron-store "
if self . execute :
shutil . copytree ( cron_store , dest_cron , dirs_exist_ok = True )
self . record ( " cron-jobs " , str ( cron_store ) , str ( dest_cron ) , " archived " ,
" Cron job store archived " )
# ── Hooks ─────────────────────────────────────────────────
def migrate_hooks_config ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
hooks = config . get ( " hooks " ) or { }
if not hooks :
self . record ( " hooks-config " , None , None , " skipped " , " No hooks configuration found " )
return
# Archive the full hooks config
if self . archive_dir and self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " hooks-config.json "
dest . write_text ( json . dumps ( hooks , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " hooks-config " , " openclaw.json hooks.* " , str ( dest ) , " archived " ,
" Hooks config archived for manual review " )
else :
self . record ( " hooks-config " , " openclaw.json hooks.* " , " archive/hooks-config.json " ,
" archived " , " Would archive hooks config " )
# Copy workspace hooks directory
for ws_name in ( " workspace " , " workspace.default " ) :
hooks_dir = self . source_root / ws_name / " hooks "
if hooks_dir . is_dir ( ) and self . archive_dir :
dest_hooks = self . archive_dir / " workspace-hooks "
if self . execute :
shutil . copytree ( hooks_dir , dest_hooks , dirs_exist_ok = True )
self . record ( " hooks-config " , str ( hooks_dir ) , str ( dest_hooks ) , " archived " ,
" Workspace hooks directory archived " )
break
# ── Agent config ──────────────────────────────────────────
def migrate_agent_config ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
agents = config . get ( " agents " ) or { }
defaults = agents . get ( " defaults " ) or { }
agent_list = agents . get ( " list " ) or [ ]
if not defaults and not agent_list :
self . record ( " agent-config " , None , None , " skipped " , " No agent configuration found " )
return
hermes_cfg_path = self . target_root / " config.yaml "
hermes_cfg = load_yaml_file ( hermes_cfg_path )
changes = False
# Map agent defaults
agent_cfg = hermes_cfg . get ( " agent " ) or { }
if defaults . get ( " contextTokens " ) :
# No direct mapping but useful context
pass
if defaults . get ( " timeoutSeconds " ) :
agent_cfg [ " max_turns " ] = min ( defaults [ " timeoutSeconds " ] / / 10 , 200 )
changes = True
if defaults . get ( " verboseDefault " ) :
agent_cfg [ " verbose " ] = defaults [ " verboseDefault " ]
changes = True
if defaults . get ( " thinkingDefault " ) :
# Map OpenClaw thinking -> Hermes reasoning_effort
thinking = defaults [ " thinkingDefault " ]
if thinking in ( " always " , " high " ) :
agent_cfg [ " reasoning_effort " ] = " high "
elif thinking in ( " auto " , " medium " ) :
agent_cfg [ " reasoning_effort " ] = " medium "
elif thinking in ( " off " , " low " , " none " ) :
agent_cfg [ " reasoning_effort " ] = " low "
changes = True
# Map compaction -> compression
compaction = defaults . get ( " compaction " ) or { }
if compaction :
compression = hermes_cfg . get ( " compression " ) or { }
if compaction . get ( " mode " ) == " off " :
compression [ " enabled " ] = False
else :
compression [ " enabled " ] = True
if compaction . get ( " timeout " ) :
pass # No direct mapping
if compaction . get ( " model " ) :
compression [ " summary_model " ] = compaction [ " model " ]
hermes_cfg [ " compression " ] = compression
changes = True
# Map humanDelay
human_delay = defaults . get ( " humanDelay " ) or { }
if human_delay :
hd = hermes_cfg . get ( " human_delay " ) or { }
if human_delay . get ( " enabled " ) :
hd [ " mode " ] = " natural "
if human_delay . get ( " minMs " ) :
hd [ " min_ms " ] = human_delay [ " minMs " ]
if human_delay . get ( " maxMs " ) :
hd [ " max_ms " ] = human_delay [ " maxMs " ]
hermes_cfg [ " human_delay " ] = hd
changes = True
# Map userTimezone
if defaults . get ( " userTimezone " ) :
hermes_cfg [ " timezone " ] = defaults [ " userTimezone " ]
changes = True
# Map terminal/exec settings
exec_cfg = defaults . get ( " exec " ) or ( config . get ( " tools " ) or { } ) . get ( " exec " ) or { }
if exec_cfg :
terminal_cfg = hermes_cfg . get ( " terminal " ) or { }
if exec_cfg . get ( " timeout " ) :
terminal_cfg [ " timeout " ] = exec_cfg [ " timeout " ]
changes = True
hermes_cfg [ " terminal " ] = terminal_cfg
# Map sandbox -> terminal docker settings
sandbox = defaults . get ( " sandbox " ) or { }
if sandbox and sandbox . get ( " backend " ) == " docker " :
terminal_cfg = hermes_cfg . get ( " terminal " ) or { }
terminal_cfg [ " backend " ] = " docker "
if sandbox . get ( " docker " , { } ) . get ( " image " ) :
terminal_cfg [ " docker_image " ] = sandbox [ " docker " ] [ " image " ]
hermes_cfg [ " terminal " ] = terminal_cfg
changes = True
if changes :
hermes_cfg [ " agent " ] = agent_cfg
if self . execute :
self . maybe_backup ( hermes_cfg_path )
dump_yaml_file ( hermes_cfg_path , hermes_cfg )
self . record ( " agent-config " , " openclaw.json agents.defaults " , " config.yaml agent/compression/terminal " ,
" migrated " , " Agent defaults mapped to Hermes config " )
# Archive multi-agent list
if agent_list :
if self . archive_dir and self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " agents-list.json "
dest . write_text ( json . dumps ( agent_list , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " agent-config " , " openclaw.json agents.list " , " archive/agents-list.json " ,
" archived " , f " Multi-agent setup ( { len ( agent_list ) } agents) archived for manual recreation " )
# Archive bindings
bindings = config . get ( " bindings " ) or [ ]
if bindings :
if self . archive_dir and self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " bindings.json "
dest . write_text ( json . dumps ( bindings , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " agent-config " , " openclaw.json bindings " , " archive/bindings.json " ,
" archived " , f " Agent routing bindings ( { len ( bindings ) } rules) archived " )
# ── Gateway config ────────────────────────────────────────
def migrate_gateway_config ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
gateway = config . get ( " gateway " ) or { }
if not gateway :
self . record ( " gateway-config " , None , None , " skipped " , " No gateway configuration found " )
return
# Archive the full gateway config (complex, many settings)
if self . archive_dir and self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " gateway-config.json "
dest . write_text ( json . dumps ( gateway , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " gateway-config " , " openclaw.json gateway.* " , " archive/gateway-config.json " ,
" archived " , " Gateway config archived. Use ' hermes gateway ' to configure. " )
# Extract gateway auth token to .env if present
auth = gateway . get ( " auth " ) or { }
if auth . get ( " token " ) and self . migrate_secrets :
self . _set_env_var ( " HERMES_GATEWAY_TOKEN " , auth [ " token " ] , " gateway.auth.token " )
# ── Session config ────────────────────────────────────────
def migrate_session_config ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
session = config . get ( " session " ) or { }
if not session :
self . record ( " session-config " , None , None , " skipped " , " No session configuration found " )
return
hermes_cfg_path = self . target_root / " config.yaml "
hermes_cfg = load_yaml_file ( hermes_cfg_path )
sr = hermes_cfg . get ( " session_reset " ) or { }
changes = False
reset_triggers = session . get ( " resetTriggers " ) or session . get ( " reset_triggers " ) or { }
if reset_triggers :
daily = reset_triggers . get ( " daily " ) or { }
idle = reset_triggers . get ( " idle " ) or { }
if daily . get ( " enabled " ) and idle . get ( " enabled " ) :
sr [ " mode " ] = " both "
elif daily . get ( " enabled " ) :
sr [ " mode " ] = " daily "
elif idle . get ( " enabled " ) :
sr [ " mode " ] = " idle "
else :
sr [ " mode " ] = " none "
if daily . get ( " hour " ) is not None :
sr [ " at_hour " ] = daily [ " hour " ]
if idle . get ( " minutes " ) or idle . get ( " timeoutMinutes " ) :
sr [ " idle_minutes " ] = idle . get ( " minutes " ) or idle . get ( " timeoutMinutes " )
changes = True
if changes :
hermes_cfg [ " session_reset " ] = sr
if self . execute :
self . maybe_backup ( hermes_cfg_path )
dump_yaml_file ( hermes_cfg_path , hermes_cfg )
self . record ( " session-config " , " openclaw.json session.resetTriggers " ,
" config.yaml session_reset " , " migrated " )
# Archive full session config (identity links, thread bindings, etc.)
complex_keys = { " identityLinks " , " threadBindings " , " maintenance " , " scope " , " sendPolicy " }
complex_session = { k : v for k , v in session . items ( ) if k in complex_keys and v }
if complex_session and self . archive_dir :
if self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " session-config.json "
dest . write_text ( json . dumps ( complex_session , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " session-config " , " openclaw.json session (advanced) " ,
" archive/session-config.json " , " archived " ,
" Advanced session settings archived (identity links, thread bindings, etc.) " )
# ── Full model providers ──────────────────────────────────
def migrate_full_providers ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
models = config . get ( " models " ) or { }
providers = models . get ( " providers " ) or { }
if not providers :
self . record ( " full-providers " , None , None , " skipped " , " No model providers found " )
return
hermes_cfg_path = self . target_root / " config.yaml "
hermes_cfg = load_yaml_file ( hermes_cfg_path )
custom_providers = hermes_cfg . get ( " custom_providers " ) or [ ]
added = 0
# Well-known providers: just extract API keys
WELL_KNOWN = { " openrouter " , " openai " , " anthropic " , " deepseek " , " google " , " groq " }
for prov_name , prov_cfg in providers . items ( ) :
if not isinstance ( prov_cfg , dict ) :
continue
# Extract API key to .env
api_key = prov_cfg . get ( " apiKey " ) or prov_cfg . get ( " api_key " )
if api_key and self . migrate_secrets :
env_key = f " { prov_name . upper ( ) . replace ( ' - ' , ' _ ' ) } _API_KEY "
self . _set_env_var ( env_key , api_key , f " models.providers. { prov_name } .apiKey " )
# For non-well-known providers, create custom_providers entry
if prov_name . lower ( ) not in WELL_KNOWN and prov_cfg . get ( " baseUrl " ) :
# Check if already exists
existing_names = { p . get ( " name " , " " ) . lower ( ) for p in custom_providers }
if prov_name . lower ( ) in existing_names and not self . overwrite :
self . record ( " full-providers " , f " models.providers. { prov_name } " ,
" config.yaml custom_providers " , " conflict " ,
f " Provider ' { prov_name } ' already exists " )
continue
api_type = prov_cfg . get ( " apiType " ) or prov_cfg . get ( " type " ) or " openai "
api_mode_map = {
" openai " : " chat_completions " ,
" anthropic " : " anthropic_messages " ,
" cohere " : " chat_completions " ,
}
entry = {
" name " : prov_name ,
" base_url " : prov_cfg [ " baseUrl " ] ,
" api_key " : " " , # referenced from .env
" api_mode " : api_mode_map . get ( api_type , " chat_completions " ) ,
}
custom_providers . append ( entry )
added + = 1
self . record ( " full-providers " , f " models.providers. { prov_name } " ,
f " config.yaml custom_providers[ { prov_name } ] " , " migrated " )
if added > 0 and self . execute :
self . maybe_backup ( hermes_cfg_path )
hermes_cfg [ " custom_providers " ] = custom_providers
dump_yaml_file ( hermes_cfg_path , hermes_cfg )
# Archive model aliases/catalog
agent_defaults = ( config . get ( " agents " ) or { } ) . get ( " defaults " ) or { }
model_aliases = agent_defaults . get ( " models " ) or { }
if model_aliases :
if self . archive_dir and self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " model-aliases.json "
dest . write_text ( json . dumps ( model_aliases , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " full-providers " , " agents.defaults.models " , " archive/model-aliases.json " ,
" archived " , f " Model aliases/catalog ( { len ( model_aliases ) } entries) archived " )
# ── Deep channel config ───────────────────────────────────
def migrate_deep_channels ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
channels = config . get ( " channels " ) or { }
if not channels :
self . record ( " deep-channels " , None , None , " skipped " , " No channel configuration found " )
return
# Extended channel token/allowlist mapping
CHANNEL_ENV_MAP = {
" matrix " : { " token " : " MATRIX_ACCESS_TOKEN " , " allowFrom " : " MATRIX_ALLOWED_USERS " ,
" extras " : { " homeserverUrl " : " MATRIX_HOMESERVER_URL " , " userId " : " MATRIX_USER_ID " } } ,
" mattermost " : { " token " : " MATTERMOST_BOT_TOKEN " , " allowFrom " : " MATTERMOST_ALLOWED_USERS " ,
" extras " : { " url " : " MATTERMOST_URL " , " teamId " : " MATTERMOST_TEAM_ID " } } ,
" irc " : { " extras " : { " server " : " IRC_SERVER " , " nick " : " IRC_NICK " , " channels " : " IRC_CHANNELS " } } ,
" googlechat " : { " extras " : { " serviceAccountKeyPath " : " GOOGLE_CHAT_SA_KEY_PATH " } } ,
" imessage " : { } ,
" bluebubbles " : { " extras " : { " server " : " BLUEBUBBLES_SERVER " , " password " : " BLUEBUBBLES_PASSWORD " } } ,
" msteams " : { " token " : " MSTEAMS_BOT_TOKEN " , " allowFrom " : " MSTEAMS_ALLOWED_USERS " } ,
" nostr " : { " extras " : { " nsec " : " NOSTR_NSEC " , " relays " : " NOSTR_RELAYS " } } ,
" twitch " : { " token " : " TWITCH_BOT_TOKEN " , " extras " : { " channels " : " TWITCH_CHANNELS " } } ,
}
for ch_name , ch_mapping in CHANNEL_ENV_MAP . items ( ) :
ch_cfg = channels . get ( ch_name ) or { }
if not ch_cfg :
continue
# Extract tokens
if ch_mapping . get ( " token " ) and ch_cfg . get ( " botToken " ) and self . migrate_secrets :
self . _set_env_var ( ch_mapping [ " token " ] , ch_cfg [ " botToken " ] ,
f " channels. { ch_name } .botToken " )
if ch_mapping . get ( " allowFrom " ) and ch_cfg . get ( " allowFrom " ) :
allow_val = ch_cfg [ " allowFrom " ]
if isinstance ( allow_val , list ) :
allow_val = " , " . join ( str ( x ) for x in allow_val )
self . _set_env_var ( ch_mapping [ " allowFrom " ] , str ( allow_val ) ,
f " channels. { ch_name } .allowFrom " )
# Extra fields
for oc_key , env_key in ( ch_mapping . get ( " extras " ) or { } ) . items ( ) :
val = ch_cfg . get ( oc_key )
if val :
if isinstance ( val , list ) :
val = " , " . join ( str ( x ) for x in val )
is_secret = " password " in oc_key . lower ( ) or " token " in oc_key . lower ( ) or " nsec " in oc_key . lower ( )
if is_secret and not self . migrate_secrets :
continue
self . _set_env_var ( env_key , str ( val ) , f " channels. { ch_name } . { oc_key } " )
# Map Discord-specific settings to Hermes config
discord_cfg = channels . get ( " discord " ) or { }
if discord_cfg :
hermes_cfg_path = self . target_root / " config.yaml "
hermes_cfg = load_yaml_file ( hermes_cfg_path )
discord_hermes = hermes_cfg . get ( " discord " ) or { }
changed = False
if " requireMention " in discord_cfg :
discord_hermes [ " require_mention " ] = discord_cfg [ " requireMention " ]
changed = True
if discord_cfg . get ( " autoThread " ) is not None :
discord_hermes [ " auto_thread " ] = discord_cfg [ " autoThread " ]
changed = True
if changed and self . execute :
hermes_cfg [ " discord " ] = discord_hermes
dump_yaml_file ( hermes_cfg_path , hermes_cfg )
# Archive complex channel configs (group settings, thread bindings, etc.)
complex_archive = { }
for ch_name , ch_cfg in channels . items ( ) :
if not isinstance ( ch_cfg , dict ) :
continue
complex_keys = { k : v for k , v in ch_cfg . items ( )
if k not in ( " botToken " , " appToken " , " allowFrom " , " enabled " )
and v and k not in ( " requireMention " , " autoThread " ) }
if complex_keys :
complex_archive [ ch_name ] = complex_keys
if complex_archive and self . archive_dir :
if self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " channels-deep-config.json "
dest . write_text ( json . dumps ( complex_archive , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " deep-channels " , " openclaw.json channels (advanced settings) " ,
" archive/channels-deep-config.json " , " archived " ,
f " Deep channel config for { len ( complex_archive ) } channels archived " )
# ── Browser config ────────────────────────────────────────
def migrate_browser_config ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
browser = config . get ( " browser " ) or { }
if not browser :
self . record ( " browser-config " , None , None , " skipped " , " No browser configuration found " )
return
hermes_cfg_path = self . target_root / " config.yaml "
hermes_cfg = load_yaml_file ( hermes_cfg_path )
browser_hermes = hermes_cfg . get ( " browser " ) or { }
changed = False
if browser . get ( " inactivityTimeoutMs " ) :
browser_hermes [ " inactivity_timeout " ] = browser [ " inactivityTimeoutMs " ] / / 1000
changed = True
if browser . get ( " commandTimeoutMs " ) :
browser_hermes [ " command_timeout " ] = browser [ " commandTimeoutMs " ] / / 1000
changed = True
if changed :
hermes_cfg [ " browser " ] = browser_hermes
if self . execute :
self . maybe_backup ( hermes_cfg_path )
dump_yaml_file ( hermes_cfg_path , hermes_cfg )
self . record ( " browser-config " , " openclaw.json browser.* " , " config.yaml browser " ,
" migrated " )
# Archive advanced browser settings
advanced = { k : v for k , v in browser . items ( )
if k not in ( " inactivityTimeoutMs " , " commandTimeoutMs " ) and v }
if advanced and self . archive_dir :
if self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " browser-config.json "
dest . write_text ( json . dumps ( advanced , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " browser-config " , " openclaw.json browser (advanced) " ,
" archive/browser-config.json " , " archived " )
# ── Tools config ──────────────────────────────────────────
def migrate_tools_config ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
tools = config . get ( " tools " ) or { }
if not tools :
self . record ( " tools-config " , None , None , " skipped " , " No tools configuration found " )
return
hermes_cfg_path = self . target_root / " config.yaml "
hermes_cfg = load_yaml_file ( hermes_cfg_path )
changed = False
# Map exec timeout -> terminal timeout
exec_cfg = tools . get ( " exec " ) or { }
if exec_cfg . get ( " timeout " ) :
terminal_cfg = hermes_cfg . get ( " terminal " ) or { }
terminal_cfg [ " timeout " ] = exec_cfg [ " timeout " ]
hermes_cfg [ " terminal " ] = terminal_cfg
changed = True
# Map web search API key
web_cfg = tools . get ( " webSearch " ) or tools . get ( " web " ) or { }
if web_cfg . get ( " braveApiKey " ) and self . migrate_secrets :
self . _set_env_var ( " BRAVE_API_KEY " , web_cfg [ " braveApiKey " ] , " tools.webSearch.braveApiKey " )
if changed and self . execute :
self . maybe_backup ( hermes_cfg_path )
dump_yaml_file ( hermes_cfg_path , hermes_cfg )
self . record ( " tools-config " , " openclaw.json tools.* " , " config.yaml terminal " ,
" migrated " )
# Archive full tools config
if self . archive_dir :
if self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " tools-config.json "
dest . write_text ( json . dumps ( tools , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " tools-config " , " openclaw.json tools (full) " , " archive/tools-config.json " ,
" archived " , " Full tools config archived for reference " )
# ── Approvals config ──────────────────────────────────────
def migrate_approvals_config ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
approvals = config . get ( " approvals " ) or { }
if not approvals :
self . record ( " approvals-config " , None , None , " skipped " , " No approvals configuration found " )
return
hermes_cfg_path = self . target_root / " config.yaml "
hermes_cfg = load_yaml_file ( hermes_cfg_path )
# Map approval mode
mode = approvals . get ( " mode " ) or approvals . get ( " defaultMode " )
if mode :
mode_map = { " auto " : " off " , " always " : " manual " , " smart " : " smart " , " manual " : " manual " }
hermes_mode = mode_map . get ( mode , " manual " )
hermes_cfg . setdefault ( " approvals " , { } ) [ " mode " ] = hermes_mode
if self . execute :
self . maybe_backup ( hermes_cfg_path )
dump_yaml_file ( hermes_cfg_path , hermes_cfg )
self . record ( " approvals-config " , " openclaw.json approvals.mode " ,
" config.yaml approvals.mode " , " migrated " , f " Mapped ' { mode } ' -> ' { hermes_mode } ' " )
# Archive full approvals config
if len ( approvals ) > 1 and self . archive_dir :
if self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " approvals-config.json "
dest . write_text ( json . dumps ( approvals , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " approvals-config " , " openclaw.json approvals (rules) " ,
" archive/approvals-config.json " , " archived " )
# ── Memory backend ────────────────────────────────────────
def migrate_memory_backend ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
memory = config . get ( " memory " ) or { }
if not memory :
self . record ( " memory-backend " , None , None , " skipped " , " No memory backend configuration found " )
return
if self . archive_dir and self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " memory-backend-config.json "
dest . write_text ( json . dumps ( memory , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " memory-backend " , " openclaw.json memory.* " , " archive/memory-backend-config.json " ,
" archived " , " Memory backend config (QMD, vector search, citations) archived for manual review " )
# ── Skills config ─────────────────────────────────────────
def migrate_skills_config ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
skills = config . get ( " skills " ) or { }
entries = skills . get ( " entries " ) or { }
if not entries and not skills :
self . record ( " skills-config " , None , None , " skipped " , " No skills registry configuration found " )
return
if self . archive_dir and self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " skills-registry-config.json "
dest . write_text ( json . dumps ( skills , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " skills-config " , " openclaw.json skills.* " , " archive/skills-registry-config.json " ,
" archived " , f " Skills registry config ( { len ( entries ) } entries) archived " )
# ── UI / Identity ─────────────────────────────────────────
def migrate_ui_identity ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
ui = config . get ( " ui " ) or { }
if not ui :
self . record ( " ui-identity " , None , None , " skipped " , " No UI/identity configuration found " )
return
if self . archive_dir and self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " ui-identity-config.json "
dest . write_text ( json . dumps ( ui , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " ui-identity " , " openclaw.json ui.* " , " archive/ui-identity-config.json " ,
" archived " , " UI theme and identity settings archived " )
# ── Logging / Diagnostics ─────────────────────────────────
def migrate_logging_config ( self , config : Optional [ Dict [ str , Any ] ] = None ) - > None :
config = config or self . load_openclaw_config ( )
logging_cfg = config . get ( " logging " ) or { }
diagnostics = config . get ( " diagnostics " ) or { }
combined = { }
if logging_cfg :
combined [ " logging " ] = logging_cfg
if diagnostics :
combined [ " diagnostics " ] = diagnostics
if not combined :
self . record ( " logging-config " , None , None , " skipped " , " No logging/diagnostics configuration found " )
return
if self . archive_dir and self . execute :
self . archive_dir . mkdir ( parents = True , exist_ok = True )
dest = self . archive_dir / " logging-diagnostics-config.json "
dest . write_text ( json . dumps ( combined , indent = 2 , ensure_ascii = False ) + " \n " , encoding = " utf-8 " )
self . record ( " logging-config " , " openclaw.json logging/diagnostics " ,
" archive/logging-diagnostics-config.json " , " archived " )
# ── Helper: set env var ───────────────────────────────────
def _set_env_var ( self , key : str , value : str , source_label : str ) - > None :
env_path = self . target_root / " .env "
if self . execute :
env_data = parse_env_file ( env_path )
if key in env_data and not self . overwrite :
self . record ( " env-var " , source_label , f " .env { key } " , " conflict " ,
f " Env var { key } already set " )
return
env_data [ key ] = value
save_env_file ( env_path , env_data )
self . record ( " env-var " , source_label , f " .env { key } " , " migrated " )
# ── Generate migration notes ──────────────────────────────
def generate_migration_notes ( self ) - > None :
if not self . output_dir :
return
notes = [
" # OpenClaw -> Hermes Migration Notes " ,
" " ,
" This document lists items that require manual attention after migration. " ,
" " ,
" ## PM2 / External Processes " ,
" " ,
" Your PM2 processes (Discord bots, Telegram bots, etc.) are NOT affected " ,
" by this migration. They run independently and will continue working. " ,
" No action needed for PM2-managed processes. " ,
" " ,
]
archived = [ i for i in self . items if i . status == " archived " ]
if archived :
notes . extend ( [
" ## Archived Items (Manual Review Needed) " ,
" " ,
" These OpenClaw configurations were archived because they don ' t have a " ,
" direct 1:1 mapping in Hermes. Review each file and recreate manually: " ,
" " ,
] )
for item in archived :
notes . append ( f " - ** { item . kind } **: ` { item . destination } ` -- { item . reason } " )
notes . append ( " " )
conflicts = [ i for i in self . items if i . status == " conflict " ]
if conflicts :
notes . extend ( [
" ## Conflicts (Existing Hermes Config Not Overwritten) " ,
" " ,
" These items already existed in your Hermes config. Re-run with " ,
" `--overwrite` to force, or merge manually: " ,
" " ,
] )
for item in conflicts :
notes . append ( f " - ** { item . kind } **: { item . reason } " )
notes . append ( " " )
notes . extend ( [
" ## Hermes-Specific Setup " ,
" " ,
" After migration, you may want to: " ,
" - Run `hermes setup` to configure any remaining settings " ,
" - Run `hermes mcp list` to verify MCP servers were imported correctly " ,
" - Run `hermes cron` to recreate scheduled tasks (see archive/cron-config.json) " ,
" - Run `hermes gateway install` if you need the gateway service " ,
" - Review `~/.hermes/config.yaml` for any adjustments " ,
" " ,
] )
if self . execute :
self . output_dir . mkdir ( parents = True , exist_ok = True )
( self . output_dir / " MIGRATION_NOTES.md " ) . write_text (
" \n " . join ( notes ) + " \n " , encoding = " utf-8 "
)
def parse_args ( ) - > argparse . Namespace :
parser = argparse . ArgumentParser ( description = " Migrate OpenClaw user state into Hermes Agent. " )
@@ -1524,8 +2396,101 @@ def main() -> int:
skill_conflict_mode = args . skill_conflict ,
)
report = migrator . migrate ( )
print ( json . dumps ( report , indent = 2 , ensure_ascii = False ) )
return 0 if report [ " summary " ] . get ( " error " , 0 ) == 0 else 1
# ── Human-readable terminal recap ─────────────────────────
s = report [ " summary " ]
items = report [ " items " ]
mode_label = " DRY RUN " if not args . execute else " EXECUTED "
total = sum ( s . values ( ) )
print ( )
print ( f " ╔══════════════════════════════════════════════════════╗ " )
print ( f " ║ OpenClaw -> Hermes Migration [ { mode_label : >8s } ] ║ " )
print ( f " ╠══════════════════════════════════════════════════════╣ " )
print ( f " ║ Source: { str ( report [ ' source_root ' ] ) [ : 42 ] : <42s } ║ " )
print ( f " ║ Target: { str ( report [ ' target_root ' ] ) [ : 42 ] : <42s } ║ " )
print ( f " ╠══════════════════════════════════════════════════════╣ " )
print ( f " ║ ✔ Migrated: { s . get ( ' migrated ' , 0 ) : >3d } ◆ Archived: { s . get ( ' archived ' , 0 ) : >3d } ║ " )
print ( f " ║ ⊘ Skipped: { s . get ( ' skipped ' , 0 ) : >3d } ⚠ Conflicts: { s . get ( ' conflict ' , 0 ) : >3d } ║ " )
print ( f " ║ ✖ Errors: { s . get ( ' error ' , 0 ) : >3d } Total: { total : >3d } ║ " )
print ( f " ╚══════════════════════════════════════════════════════╝ " )
# Show what was migrated
migrated = [ i for i in items if i [ " status " ] == " migrated " ]
if migrated :
print ( )
print ( " Migrated: " )
seen_kinds = set ( )
for item in migrated :
label = item [ " kind " ]
if label in seen_kinds :
continue
seen_kinds . add ( label )
dest = item . get ( " destination " ) or " "
if dest . startswith ( str ( report [ " target_root " ] ) ) :
dest = " ~/.hermes/ " + dest [ len ( str ( report [ " target_root " ] ) ) + 1 : ]
meta = MIGRATION_OPTION_METADATA . get ( label , { } )
display = meta . get ( " label " , label )
print ( f " ✔ { display : <35s } -> { dest } " )
# Show what was archived
archived = [ i for i in items if i [ " status " ] == " archived " ]
if archived :
print ( )
print ( " Archived (manual review needed): " )
seen_kinds = set ( )
for item in archived :
label = item [ " kind " ]
if label in seen_kinds :
continue
seen_kinds . add ( label )
reason = item . get ( " reason " , " " )
meta = MIGRATION_OPTION_METADATA . get ( label , { } )
display = meta . get ( " label " , label )
short_reason = reason [ : 50 ] + " ... " if len ( reason ) > 50 else reason
print ( f " ◆ { display : <35s } { short_reason } " )
# Show conflicts
conflicts = [ i for i in items if i [ " status " ] == " conflict " ]
if conflicts :
print ( )
print ( " Conflicts (use --overwrite to force): " )
for item in conflicts :
print ( f " ⚠ { item [ ' kind ' ] } : { item . get ( ' reason ' , ' ' ) } " )
# Show errors
errors = [ i for i in items if i [ " status " ] == " error " ]
if errors :
print ( )
print ( " Errors: " )
for item in errors :
print ( f " ✖ { item [ ' kind ' ] } : { item . get ( ' reason ' , ' ' ) } " )
# PM2 reassurance
print ( )
print ( " ℹ PM2 processes (Discord/Telegram bots) are NOT affected. " )
# Next steps
if args . execute :
print ( )
print ( " Next steps: " )
print ( " 1. Review ~/.hermes/config.yaml " )
print ( " 2. Run: hermes mcp list " )
if any ( i [ " kind " ] == " cron-jobs " and i [ " status " ] == " archived " for i in items ) :
print ( " 3. Recreate cron jobs: hermes cron " )
if report . get ( " output_dir " ) :
print ( f " → Full report: { report [ ' output_dir ' ] } /MIGRATION_NOTES.md " )
elif not args . execute :
print ( )
print ( " This was a dry run. Add --execute to apply changes. " )
print ( )
# Also dump JSON for programmatic use
if os . environ . get ( " MIGRATION_JSON_OUTPUT " ) :
print ( json . dumps ( report , indent = 2 , ensure_ascii = False ) )
return 0 if s . get ( " error " , 0 ) == 0 else 1
if __name__ == " __main__ " :