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
This commit is contained in:
Teknium
2026-04-11 23:47:37 -07:00
committed by Teknium
parent eb2a49f95a
commit 1871227198
7 changed files with 137 additions and 15 deletions

View File

@@ -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
```

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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"

View File

@@ -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 |
|--------|-------------|