| """hermes claw — OpenClaw migration commands. |
| |
| Usage: |
| hermes claw migrate # Interactive migration from ~/.openclaw |
| hermes claw migrate --dry-run # Preview what would be migrated |
| hermes claw migrate --preset full --overwrite # Full migration, overwrite conflicts |
| """ |
|
|
| import importlib.util |
| import logging |
| import sys |
| from pathlib import Path |
|
|
| from hermes_cli.config import get_hermes_home, get_config_path, load_config, save_config |
| from hermes_cli.setup import ( |
| Colors, |
| color, |
| print_header, |
| print_info, |
| print_success, |
| print_warning, |
| print_error, |
| prompt_yes_no, |
| prompt_choice, |
| ) |
|
|
| logger = logging.getLogger(__name__) |
|
|
| PROJECT_ROOT = Path(__file__).parent.parent.resolve() |
|
|
| _OPENCLAW_SCRIPT = ( |
| PROJECT_ROOT |
| / "optional-skills" |
| / "migration" |
| / "openclaw-migration" |
| / "scripts" |
| / "openclaw_to_hermes.py" |
| ) |
|
|
| |
| _OPENCLAW_SCRIPT_INSTALLED = ( |
| get_hermes_home() |
| / "skills" |
| / "migration" |
| / "openclaw-migration" |
| / "scripts" |
| / "openclaw_to_hermes.py" |
| ) |
|
|
|
|
| def _find_migration_script() -> Path | None: |
| """Find the openclaw_to_hermes.py script in known locations.""" |
| for candidate in [_OPENCLAW_SCRIPT, _OPENCLAW_SCRIPT_INSTALLED]: |
| if candidate.exists(): |
| return candidate |
| return None |
|
|
|
|
| def _load_migration_module(script_path: Path): |
| """Dynamically load the migration script as a module.""" |
| spec = importlib.util.spec_from_file_location("openclaw_to_hermes", script_path) |
| if spec is None or spec.loader is None: |
| return None |
| mod = importlib.util.module_from_spec(spec) |
| |
| |
| sys.modules[spec.name] = mod |
| try: |
| spec.loader.exec_module(mod) |
| except Exception: |
| sys.modules.pop(spec.name, None) |
| raise |
| return mod |
|
|
|
|
| def claw_command(args): |
| """Route hermes claw subcommands.""" |
| action = getattr(args, "claw_action", None) |
|
|
| if action == "migrate": |
| _cmd_migrate(args) |
| else: |
| print("Usage: hermes claw migrate [options]") |
| print() |
| print("Commands:") |
| print(" migrate Migrate settings from OpenClaw to Hermes") |
| print() |
| print("Run 'hermes claw migrate --help' for migration options.") |
|
|
|
|
| def _cmd_migrate(args): |
| """Run the OpenClaw → Hermes migration.""" |
| source_dir = Path(getattr(args, "source", None) or Path.home() / ".openclaw") |
| dry_run = getattr(args, "dry_run", False) |
| preset = getattr(args, "preset", "full") |
| overwrite = getattr(args, "overwrite", False) |
| migrate_secrets = getattr(args, "migrate_secrets", False) |
| workspace_target = getattr(args, "workspace_target", None) |
| skill_conflict = getattr(args, "skill_conflict", "skip") |
|
|
| |
| if preset == "full": |
| migrate_secrets = True |
|
|
| print() |
| print( |
| color( |
| "┌─────────────────────────────────────────────────────────┐", |
| Colors.MAGENTA, |
| ) |
| ) |
| print( |
| color( |
| "│ ⚕ Hermes — OpenClaw Migration │", |
| Colors.MAGENTA, |
| ) |
| ) |
| print( |
| color( |
| "└─────────────────────────────────────────────────────────┘", |
| Colors.MAGENTA, |
| ) |
| ) |
|
|
| |
| if not source_dir.is_dir(): |
| print() |
| print_error(f"OpenClaw directory not found: {source_dir}") |
| print_info("Make sure your OpenClaw installation is at the expected path.") |
| print_info(f"You can specify a custom path: hermes claw migrate --source /path/to/.openclaw") |
| return |
|
|
| |
| script_path = _find_migration_script() |
| if not script_path: |
| print() |
| print_error("Migration script not found.") |
| print_info("Expected at one of:") |
| print_info(f" {_OPENCLAW_SCRIPT}") |
| print_info(f" {_OPENCLAW_SCRIPT_INSTALLED}") |
| print_info("Make sure the openclaw-migration skill is installed.") |
| return |
|
|
| |
| hermes_home = get_hermes_home() |
| print() |
| print_header("Migration Settings") |
| print_info(f"Source: {source_dir}") |
| print_info(f"Target: {hermes_home}") |
| print_info(f"Preset: {preset}") |
| print_info(f"Mode: {'dry run (preview only)' if dry_run else 'execute'}") |
| print_info(f"Overwrite: {'yes' if overwrite else 'no (skip conflicts)'}") |
| print_info(f"Secrets: {'yes (allowlisted only)' if migrate_secrets else 'no'}") |
| if skill_conflict != "skip": |
| print_info(f"Skill conflicts: {skill_conflict}") |
| if workspace_target: |
| print_info(f"Workspace: {workspace_target}") |
| print() |
|
|
| |
| if not dry_run and not getattr(args, "yes", False): |
| if not prompt_yes_no("Proceed with migration?", default=True): |
| print_info("Migration cancelled.") |
| return |
|
|
| |
| config_path = get_config_path() |
| if not config_path.exists(): |
| save_config(load_config()) |
|
|
| |
| try: |
| mod = _load_migration_module(script_path) |
| if mod is None: |
| print_error("Could not load migration script.") |
| return |
|
|
| selected = mod.resolve_selected_options(None, None, preset=preset) |
| ws_target = Path(workspace_target).resolve() if workspace_target else None |
|
|
| migrator = mod.Migrator( |
| source_root=source_dir.resolve(), |
| target_root=hermes_home.resolve(), |
| execute=not dry_run, |
| workspace_target=ws_target, |
| overwrite=overwrite, |
| migrate_secrets=migrate_secrets, |
| output_dir=None, |
| selected_options=selected, |
| preset_name=preset, |
| skill_conflict_mode=skill_conflict, |
| ) |
| report = migrator.migrate() |
| except Exception as e: |
| print() |
| print_error(f"Migration failed: {e}") |
| logger.debug("OpenClaw migration error", exc_info=True) |
| return |
|
|
| |
| _print_migration_report(report, dry_run) |
|
|
|
|
| def _print_migration_report(report: dict, dry_run: bool): |
| """Print a formatted migration report.""" |
| summary = report.get("summary", {}) |
| migrated = summary.get("migrated", 0) |
| skipped = summary.get("skipped", 0) |
| conflicts = summary.get("conflict", 0) |
| errors = summary.get("error", 0) |
| total = migrated + skipped + conflicts + errors |
|
|
| print() |
| if dry_run: |
| print_header("Dry Run Results") |
| print_info("No files were modified. This is a preview of what would happen.") |
| else: |
| print_header("Migration Results") |
|
|
| print() |
|
|
| |
| items = report.get("items", []) |
| if items: |
| |
| migrated_items = [i for i in items if i.get("status") == "migrated"] |
| skipped_items = [i for i in items if i.get("status") == "skipped"] |
| conflict_items = [i for i in items if i.get("status") == "conflict"] |
| error_items = [i for i in items if i.get("status") == "error"] |
|
|
| if migrated_items: |
| label = "Would migrate" if dry_run else "Migrated" |
| print(color(f" ✓ {label}:", Colors.GREEN)) |
| for item in migrated_items: |
| kind = item.get("kind", "unknown") |
| dest = item.get("destination", "") |
| if dest: |
| dest_short = str(dest).replace(str(Path.home()), "~") |
| print(f" {kind:<22s} → {dest_short}") |
| else: |
| print(f" {kind}") |
| print() |
|
|
| if conflict_items: |
| print(color(f" ⚠ Conflicts (skipped — use --overwrite to force):", Colors.YELLOW)) |
| for item in conflict_items: |
| kind = item.get("kind", "unknown") |
| reason = item.get("reason", "already exists") |
| print(f" {kind:<22s} {reason}") |
| print() |
|
|
| if skipped_items: |
| print(color(f" ─ Skipped:", Colors.DIM)) |
| for item in skipped_items: |
| kind = item.get("kind", "unknown") |
| reason = item.get("reason", "") |
| print(f" {kind:<22s} {reason}") |
| print() |
|
|
| if error_items: |
| print(color(f" ✗ Errors:", Colors.RED)) |
| for item in error_items: |
| kind = item.get("kind", "unknown") |
| reason = item.get("reason", "unknown error") |
| print(f" {kind:<22s} {reason}") |
| print() |
|
|
| |
| parts = [] |
| if migrated: |
| action = "would migrate" if dry_run else "migrated" |
| parts.append(f"{migrated} {action}") |
| if conflicts: |
| parts.append(f"{conflicts} conflict(s)") |
| if skipped: |
| parts.append(f"{skipped} skipped") |
| if errors: |
| parts.append(f"{errors} error(s)") |
|
|
| if parts: |
| print_info(f"Summary: {', '.join(parts)}") |
| else: |
| print_info("Nothing to migrate.") |
|
|
| |
| output_dir = report.get("output_dir") |
| if output_dir: |
| print_info(f"Full report saved to: {output_dir}") |
|
|
| if dry_run: |
| print() |
| print_info("To execute the migration, run without --dry-run:") |
| print_info(f" hermes claw migrate --preset {report.get('preset', 'full')}") |
| elif migrated: |
| print() |
| print_success("Migration complete!") |
| |
| skipped_keys = [ |
| i for i in report.get("items", []) |
| if i.get("kind") == "provider-keys" and i.get("status") == "skipped" |
| ] |
| if skipped_keys: |
| print() |
| print(color(" ⚠ API keys were NOT migrated (secrets migration is disabled by default).", Colors.YELLOW)) |
| print(color(" Your OPENROUTER_API_KEY and other provider keys must be added manually.", Colors.YELLOW)) |
| print() |
| print_info("To migrate API keys, re-run with:") |
| print_info(" hermes claw migrate --migrate-secrets") |
| print() |
| print_info("Or add your key manually:") |
| print_info(" hermes config set OPENROUTER_API_KEY sk-or-v1-...") |
|
|