mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
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:
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|--------|-------------|
|
||||
|
||||
Reference in New Issue
Block a user