From 187122719867a7cb28388b8941bdb27ce8d97ce7 Mon Sep 17 00:00:00 2001 From: Teknium Date: Sat, 11 Apr 2026 23:47:37 -0700 Subject: [PATCH] feat: rebrand OpenClaw references to Hermes during migration - Add rebrand_text() that replaces OpenClaw, Open Claw, Open-Claw, ClawdBot, and MoltBot with Hermes (case-insensitive, word-boundary) - Apply rebranding to memory entries (MEMORY.md, USER.md, daily memory) - Apply rebranding to SOUL.md and workspace instructions via new transform parameter on copy_file() - Fix moldbot -> moltbot typo across codebase (claw.py, migration script, docs, tests) - Add unit tests for rebrand_text and integration tests for memory and soul migration rebranding --- docs/migration/openclaw.md | 2 +- hermes_cli/claw.py | 4 +- .../scripts/openclaw_to_hermes.py | 39 ++++++-- tests/hermes_cli/test_claw.py | 6 +- tests/skills/test_openclaw_migration.py | 95 +++++++++++++++++++ website/docs/guides/migrate-from-openclaw.md | 4 +- website/docs/reference/cli-commands.md | 2 +- 7 files changed, 137 insertions(+), 15 deletions(-) diff --git a/docs/migration/openclaw.md b/docs/migration/openclaw.md index 8545636ab..30f2f97e4 100644 --- a/docs/migration/openclaw.md +++ b/docs/migration/openclaw.md @@ -118,7 +118,7 @@ For executed migrations, the full report is saved to `~/.hermes/migration/opencl ## Troubleshooting ### "OpenClaw directory not found" -The migration looks for `~/.openclaw` by default, then tries `~/.clawdbot` and `~/.moldbot`. If your OpenClaw is installed elsewhere, use `--source`: +The migration looks for `~/.openclaw` by default, then tries `~/.clawdbot` and `~/.moltbot`. If your OpenClaw is installed elsewhere, use `--source`: ```bash hermes claw migrate --source /path/to/.openclaw ``` diff --git a/hermes_cli/claw.py b/hermes_cli/claw.py index d0bfd73d2..b2540dffe 100644 --- a/hermes_cli/claw.py +++ b/hermes_cli/claw.py @@ -50,7 +50,7 @@ _OPENCLAW_SCRIPT_INSTALLED = ( ) # Known OpenClaw directory names (current + legacy) -_OPENCLAW_DIR_NAMES = (".openclaw", ".clawdbot", ".moldbot") +_OPENCLAW_DIR_NAMES = (".openclaw", ".clawdbot", ".moltbot") def _warn_if_gateway_running(auto_yes: bool) -> None: """Check if a Hermes gateway is running with connected platforms. @@ -216,7 +216,7 @@ def _cmd_migrate(args): source_dir = Path.home() / ".openclaw" if not source_dir.is_dir(): # Try legacy directory names - for legacy in (".clawdbot", ".moldbot"): + for legacy in (".clawdbot", ".moltbot"): candidate = Path.home() / legacy if candidate.is_dir(): source_dir = candidate diff --git a/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py b/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py index 759b798a5..d06d64f93 100644 --- a/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py +++ b/optional-skills/migration/openclaw-migration/scripts/openclaw_to_hermes.py @@ -376,6 +376,24 @@ def backup_existing(path: Path, backup_root: Path) -> Optional[Path]: return dest +# ── Brand rewriting ───────────────────────────────────────── +# Replace OpenClaw brand names with Hermes in migrated text so that +# memory entries, user profiles, SOUL.md, and workspace instructions +# read as self-referential to the new agent identity. +_REBRAND_PATTERNS: List[Tuple[re.Pattern, str]] = [ + (re.compile(r'\bOpen[\s-]?Claw\b', re.IGNORECASE), 'Hermes'), + (re.compile(r'\bClawdBot\b', re.IGNORECASE), 'Hermes'), + (re.compile(r'\bMoltBot\b', re.IGNORECASE), 'Hermes'), +] + + +def rebrand_text(text: str) -> str: + """Replace OpenClaw / ClawdBot / MoltBot brand names with Hermes.""" + for pattern, replacement in _REBRAND_PATTERNS: + text = pattern.sub(replacement, text) + return text + + def parse_existing_memory_entries(path: Path) -> List[str]: if not path.exists(): return [] @@ -782,12 +800,13 @@ class Migrator: path.write_text("\n".join(entries) + "\n", encoding="utf-8") return path - def copy_file(self, source: Path, destination: Path, kind: str) -> None: + def copy_file(self, source: Path, destination: Path, kind: str, + transform: Optional[Any] = None) -> None: if not source or not source.exists(): return if destination.exists(): - if sha256_file(source) == sha256_file(destination): + if not transform and sha256_file(source) == sha256_file(destination): self.record(kind, source, destination, "skipped", "Target already matches source") return if not self.overwrite: @@ -797,7 +816,13 @@ class Migrator: if self.execute: backup_path = self.maybe_backup(destination) ensure_parent(destination) - shutil.copy2(source, destination) + if transform: + content = read_text(source) + content = transform(content) + destination.write_text(content, encoding="utf-8") + shutil.copystat(source, destination) + else: + shutil.copy2(source, destination) self.record(kind, source, destination, "migrated", backup=str(backup_path) if backup_path else None) else: self.record(kind, source, destination, "migrated", "Would copy") @@ -807,7 +832,7 @@ class Migrator: if not source: self.record("soul", None, self.target_root / "SOUL.md", "skipped", "No OpenClaw SOUL.md found") return - self.copy_file(source, self.target_root / "SOUL.md", kind="soul") + self.copy_file(source, self.target_root / "SOUL.md", kind="soul", transform=rebrand_text) def migrate_workspace_agents(self) -> None: source = self.source_candidate( @@ -821,7 +846,7 @@ class Migrator: self.record("workspace-agents", source, None, "skipped", "No workspace target was provided") return destination = self.workspace_target / WORKSPACE_INSTRUCTIONS_FILENAME - self.copy_file(source, destination, kind="workspace-agents") + self.copy_file(source, destination, kind="workspace-agents", transform=rebrand_text) def migrate_memory(self, source: Optional[Path], destination: Path, limit: int, kind: str) -> None: if not source or not source.exists(): @@ -832,6 +857,7 @@ class Migrator: if not incoming: self.record(kind, source, destination, "skipped", "No importable entries found") return + incoming = [rebrand_text(entry) for entry in incoming] existing = parse_existing_memory_entries(destination) merged, stats, overflowed = merge_entries(existing, incoming, limit) @@ -927,7 +953,7 @@ class Migrator: def load_openclaw_config(self) -> Dict[str, Any]: # Check current name and legacy config filenames - for name in ("openclaw.json", "clawdbot.json", "moldbot.json"): + for name in ("openclaw.json", "clawdbot.json", "moltbot.json"): config_path = self.source_root / name if config_path.exists(): try: @@ -1543,6 +1569,7 @@ class Migrator: if not all_incoming: self.record("daily-memory", source_dir, destination, "skipped", "No importable entries found in daily memory files") return + all_incoming = [rebrand_text(entry) for entry in all_incoming] existing = parse_existing_memory_entries(destination) merged, stats, overflowed = merge_entries(existing, all_incoming, self.memory_limit) diff --git a/tests/hermes_cli/test_claw.py b/tests/hermes_cli/test_claw.py index da3002f8c..0094efb84 100644 --- a/tests/hermes_cli/test_claw.py +++ b/tests/hermes_cli/test_claw.py @@ -58,13 +58,13 @@ class TestFindOpenclawDirs: def test_finds_legacy_dirs(self, tmp_path): clawdbot = tmp_path / ".clawdbot" clawdbot.mkdir() - moldbot = tmp_path / ".moldbot" - moldbot.mkdir() + moltbot = tmp_path / ".moltbot" + moltbot.mkdir() with patch("pathlib.Path.home", return_value=tmp_path): found = claw_mod._find_openclaw_dirs() assert len(found) == 2 assert clawdbot in found - assert moldbot in found + assert moltbot in found def test_returns_empty_when_none_exist(self, tmp_path): with patch("pathlib.Path.home", return_value=tmp_path): diff --git a/tests/skills/test_openclaw_migration.py b/tests/skills/test_openclaw_migration.py index 99d126bed..6dc5b50fe 100644 --- a/tests/skills/test_openclaw_migration.py +++ b/tests/skills/test_openclaw_migration.py @@ -722,3 +722,98 @@ def test_skill_installs_cleanly_under_skills_guard(): KNOWN_FALSE_POSITIVES = {"agent_config_mod", "python_os_environ", "hermes_config_mod"} for f in result.findings: assert f.pattern_id in KNOWN_FALSE_POSITIVES, f"Unexpected finding: {f}" + + +# ── rebrand_text tests ──────────────────────────────────────── + + +def test_rebrand_text_replaces_openclaw_variants(): + mod = load_module() + assert mod.rebrand_text("OpenClaw prefers Python 3.11") == "Hermes prefers Python 3.11" + assert mod.rebrand_text("I told Open Claw to use dark mode") == "I told Hermes to use dark mode" + assert mod.rebrand_text("Open-Claw config is great") == "Hermes config is great" + assert mod.rebrand_text("openclaw should always respond concisely") == "Hermes should always respond concisely" + assert mod.rebrand_text("OPENCLAW uses tools well") == "Hermes uses tools well" + + +def test_rebrand_text_replaces_legacy_bot_names(): + mod = load_module() + assert mod.rebrand_text("ClawdBot remembers my timezone") == "Hermes remembers my timezone" + assert mod.rebrand_text("clawdbot prefers tabs") == "Hermes prefers tabs" + assert mod.rebrand_text("MoltBot was configured for Spanish") == "Hermes was configured for Spanish" + assert mod.rebrand_text("moltbot uses Python") == "Hermes uses Python" + + +def test_rebrand_text_preserves_unrelated_content(): + mod = load_module() + text = "User prefers dark mode and lives in Las Vegas" + assert mod.rebrand_text(text) == text + + +def test_rebrand_text_handles_multiple_replacements(): + mod = load_module() + text = "OpenClaw said to ask ClawdBot about MoltBot settings" + assert mod.rebrand_text(text) == "Hermes said to ask Hermes about Hermes settings" + + +def test_migrate_memory_rebrands_entries(tmp_path): + mod = load_module() + source_root = tmp_path / "openclaw" + source_root.mkdir() + workspace = source_root / "workspace" + workspace.mkdir() + memory_md = workspace / "MEMORY.md" + memory_md.write_text( + "# Memory\n\n- OpenClaw should use Python 3.11\n- ClawdBot prefers dark mode\n", + encoding="utf-8", + ) + + target_root = tmp_path / "hermes" + target_root.mkdir() + (target_root / "memories").mkdir() + + migrator = mod.Migrator( + source_root=source_root, + target_root=target_root, + execute=True, + workspace_target=None, + overwrite=False, + migrate_secrets=False, + output_dir=tmp_path / "report", + selected_options={"memory"}, + ) + migrator.migrate() + + result = (target_root / "memories" / "MEMORY.md").read_text(encoding="utf-8") + assert "OpenClaw" not in result + assert "ClawdBot" not in result + assert "Hermes" in result + + +def test_migrate_soul_rebrands_content(tmp_path): + mod = load_module() + source_root = tmp_path / "openclaw" + source_root.mkdir() + workspace = source_root / "workspace" + workspace.mkdir() + soul_md = workspace / "SOUL.md" + soul_md.write_text("You are OpenClaw, an AI assistant made by SparkLab.", encoding="utf-8") + + target_root = tmp_path / "hermes" + target_root.mkdir() + + migrator = mod.Migrator( + source_root=source_root, + target_root=target_root, + execute=True, + workspace_target=None, + overwrite=False, + migrate_secrets=False, + output_dir=tmp_path / "report", + selected_options={"soul"}, + ) + migrator.migrate() + + result = (target_root / "SOUL.md").read_text(encoding="utf-8") + assert "OpenClaw" not in result + assert "You are Hermes" in result diff --git a/website/docs/guides/migrate-from-openclaw.md b/website/docs/guides/migrate-from-openclaw.md index 6322c725b..5cf2f8c96 100644 --- a/website/docs/guides/migrate-from-openclaw.md +++ b/website/docs/guides/migrate-from-openclaw.md @@ -23,7 +23,7 @@ hermes claw migrate --preset full --yes The migration always shows a full preview of what will be imported before making any changes. Review the list, then confirm to proceed. -Reads from `~/.openclaw/` by default. Legacy `~/.clawdbot/` or `~/.moldbot/` directories are detected automatically. Same for legacy config filenames (`clawdbot.json`, `moldbot.json`). +Reads from `~/.openclaw/` by default. Legacy `~/.clawdbot/` or `~/.moltbot/` directories are detected automatically. Same for legacy config filenames (`clawdbot.json`, `moltbot.json`). ## Options @@ -234,7 +234,7 @@ The migration resolves all three formats. For env templates and SecretRef object ### "OpenClaw directory not found" -The migration checks `~/.openclaw/`, then `~/.clawdbot/`, then `~/.moldbot/`. If your installation is elsewhere, use `--source /path/to/your/openclaw`. +The migration checks `~/.openclaw/`, then `~/.clawdbot/`, then `~/.moltbot/`. If your installation is elsewhere, use `--source /path/to/your/openclaw`. ### "No provider API keys found" diff --git a/website/docs/reference/cli-commands.md b/website/docs/reference/cli-commands.md index 12394ea44..07a2f76eb 100644 --- a/website/docs/reference/cli-commands.md +++ b/website/docs/reference/cli-commands.md @@ -660,7 +660,7 @@ hermes insights [--days N] [--source platform] hermes claw migrate [options] ``` -Migrate your OpenClaw setup to Hermes. Reads from `~/.openclaw` (or a custom path) and writes to `~/.hermes`. Automatically detects legacy directory names (`~/.clawdbot`, `~/.moldbot`) and config filenames (`clawdbot.json`, `moldbot.json`). +Migrate your OpenClaw setup to Hermes. Reads from `~/.openclaw` (or a custom path) and writes to `~/.hermes`. Automatically detects legacy directory names (`~/.clawdbot`, `~/.moltbot`) and config filenames (`clawdbot.json`, `moltbot.json`). | Option | Description | |--------|-------------|