diff --git a/README.md b/README.md
index 74c24d9..55dc7fa 100644
--- a/README.md
+++ b/README.md
@@ -164,7 +164,7 @@ nanobot agent -m "Hello from my local LLM!"
## 💬 Chat Apps
-Talk to your nanobot through Telegram, Discord, WhatsApp, Feishu, or Moltchat — anytime, anywhere.
+Talk to your nanobot through Telegram, Discord, WhatsApp, Feishu, or Mochat — anytime, anywhere.
| Channel | Setup |
|---------|-------|
@@ -172,7 +172,7 @@ Talk to your nanobot through Telegram, Discord, WhatsApp, Feishu, or Moltchat
| **Discord** | Easy (bot token + intents) |
| **WhatsApp** | Medium (scan QR) |
| **Feishu** | Medium (app credentials) |
-| **Moltchat** | Medium (claw token + websocket) |
+| **Mochat** | Medium (claw token + websocket) |
Telegram (Recommended)
@@ -207,7 +207,7 @@ nanobot gateway
-Moltchat (Claw IM)
+Mochat (Claw IM)
Uses **Socket.IO WebSocket** by default, with HTTP polling fallback.
@@ -221,7 +221,7 @@ Uses **Socket.IO WebSocket** by default, with HTTP polling fallback.
```json
{
"channels": {
- "moltchat": {
+ "mochat": {
"enabled": true,
"baseUrl": "https://mochat.io",
"socketUrl": "https://mochat.io",
@@ -244,7 +244,7 @@ nanobot gateway
```
> [!TIP]
-> Keep `clawToken` private. It should only be sent in `X-Claw-Token` header to your Moltchat API endpoint.
+> Keep `clawToken` private. It should only be sent in `X-Claw-Token` header to your Mochat API endpoint.
@@ -456,7 +456,7 @@ docker run -v ~/.nanobot:/root/.nanobot --rm nanobot onboard
# Edit config on host to add API keys
vim ~/.nanobot/config.json
-# Run gateway (connects to enabled channels, e.g. Telegram/Discord/Moltchat)
+# Run gateway (connects to enabled channels, e.g. Telegram/Discord/Mochat)
docker run -v ~/.nanobot:/root/.nanobot -p 18790:18790 nanobot gateway
# Or run a single command
diff --git a/nanobot/channels/__init__.py b/nanobot/channels/__init__.py
index 4d77063..034d401 100644
--- a/nanobot/channels/__init__.py
+++ b/nanobot/channels/__init__.py
@@ -2,6 +2,6 @@
from nanobot.channels.base import BaseChannel
from nanobot.channels.manager import ChannelManager
-from nanobot.channels.moltchat import MoltchatChannel
+from nanobot.channels.mochat import MochatChannel
-__all__ = ["BaseChannel", "ChannelManager", "MoltchatChannel"]
+__all__ = ["BaseChannel", "ChannelManager", "MochatChannel"]
diff --git a/nanobot/channels/manager.py b/nanobot/channels/manager.py
index 11690ef..64214ce 100644
--- a/nanobot/channels/manager.py
+++ b/nanobot/channels/manager.py
@@ -78,17 +78,17 @@ class ChannelManager:
except ImportError as e:
logger.warning(f"Feishu channel not available: {e}")
- # Moltchat channel
- if self.config.channels.moltchat.enabled:
+ # Mochat channel
+ if self.config.channels.mochat.enabled:
try:
- from nanobot.channels.moltchat import MoltchatChannel
+ from nanobot.channels.mochat import MochatChannel
- self.channels["moltchat"] = MoltchatChannel(
- self.config.channels.moltchat, self.bus
+ self.channels["mochat"] = MochatChannel(
+ self.config.channels.mochat, self.bus
)
- logger.info("Moltchat channel enabled")
+ logger.info("Mochat channel enabled")
except ImportError as e:
- logger.warning(f"Moltchat channel not available: {e}")
+ logger.warning(f"Mochat channel not available: {e}")
async def start_all(self) -> None:
"""Start WhatsApp channel and the outbound dispatcher."""
diff --git a/nanobot/channels/moltchat.py b/nanobot/channels/mochat.py
similarity index 92%
rename from nanobot/channels/moltchat.py
rename to nanobot/channels/mochat.py
index cc590d4..6569cdd 100644
--- a/nanobot/channels/moltchat.py
+++ b/nanobot/channels/mochat.py
@@ -1,4 +1,4 @@
-"""Moltchat channel implementation using Socket.IO with HTTP polling fallback."""
+"""Mochat channel implementation using Socket.IO with HTTP polling fallback."""
from __future__ import annotations
@@ -15,7 +15,7 @@ from loguru import logger
from nanobot.bus.events import OutboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel
-from nanobot.config.schema import MoltchatConfig
+from nanobot.config.schema import MochatConfig
from nanobot.utils.helpers import get_data_path
try:
@@ -39,7 +39,7 @@ CURSOR_SAVE_DEBOUNCE_S = 0.5
@dataclass
-class MoltchatBufferedEntry:
+class MochatBufferedEntry:
"""Buffered inbound entry for delayed dispatch."""
raw_body: str
@@ -55,20 +55,20 @@ class MoltchatBufferedEntry:
class DelayState:
"""Per-target delayed message state."""
- entries: list[MoltchatBufferedEntry] = field(default_factory=list)
+ entries: list[MochatBufferedEntry] = field(default_factory=list)
lock: asyncio.Lock = field(default_factory=asyncio.Lock)
timer: asyncio.Task | None = None
@dataclass
-class MoltchatTarget:
+class MochatTarget:
"""Outbound target resolution result."""
id: str
is_panel: bool
-def normalize_moltchat_content(content: Any) -> str:
+def normalize_mochat_content(content: Any) -> str:
"""Normalize content payload to text."""
if isinstance(content, str):
return content.strip()
@@ -80,17 +80,17 @@ def normalize_moltchat_content(content: Any) -> str:
return str(content)
-def resolve_moltchat_target(raw: str) -> MoltchatTarget:
+def resolve_mochat_target(raw: str) -> MochatTarget:
"""Resolve id and target kind from user-provided target string."""
trimmed = (raw or "").strip()
if not trimmed:
- return MoltchatTarget(id="", is_panel=False)
+ return MochatTarget(id="", is_panel=False)
lowered = trimmed.lower()
cleaned = trimmed
forced_panel = False
- prefixes = ["moltchat:", "mochat:", "group:", "channel:", "panel:"]
+ prefixes = ["mochat:", "group:", "channel:", "panel:"]
for prefix in prefixes:
if lowered.startswith(prefix):
cleaned = trimmed[len(prefix) :].strip()
@@ -99,9 +99,9 @@ def resolve_moltchat_target(raw: str) -> MoltchatTarget:
break
if not cleaned:
- return MoltchatTarget(id="", is_panel=False)
+ return MochatTarget(id="", is_panel=False)
- return MoltchatTarget(id=cleaned, is_panel=forced_panel or not cleaned.startswith("session_"))
+ return MochatTarget(id=cleaned, is_panel=forced_panel or not cleaned.startswith("session_"))
def extract_mention_ids(value: Any) -> list[str]:
@@ -152,7 +152,7 @@ def resolve_was_mentioned(payload: dict[str, Any], agent_user_id: str) -> bool:
def resolve_require_mention(
- config: MoltchatConfig,
+ config: MochatConfig,
session_id: str,
group_id: str,
) -> bool:
@@ -167,7 +167,7 @@ def resolve_require_mention(
return bool(config.mention.require_in_groups)
-def build_buffered_body(entries: list[MoltchatBufferedEntry], is_group: bool) -> str:
+def build_buffered_body(entries: list[MochatBufferedEntry], is_group: bool) -> str:
"""Build text body from one or more buffered entries."""
if not entries:
return ""
@@ -200,20 +200,20 @@ def parse_timestamp(value: Any) -> int | None:
return None
-class MoltchatChannel(BaseChannel):
- """Moltchat channel using socket.io with fallback polling workers."""
+class MochatChannel(BaseChannel):
+ """Mochat channel using socket.io with fallback polling workers."""
- name = "moltchat"
+ name = "mochat"
- def __init__(self, config: MoltchatConfig, bus: MessageBus):
+ def __init__(self, config: MochatConfig, bus: MessageBus):
super().__init__(config, bus)
- self.config: MoltchatConfig = config
+ self.config: MochatConfig = config
self._http: httpx.AsyncClient | None = None
self._socket: Any = None
self._ws_connected = False
self._ws_ready = False
- self._state_dir = get_data_path() / "moltchat"
+ self._state_dir = get_data_path() / "mochat"
self._cursor_path = self._state_dir / "session_cursors.json"
self._session_cursor: dict[str, int] = {}
self._cursor_save_task: asyncio.Task | None = None
@@ -239,9 +239,9 @@ class MoltchatChannel(BaseChannel):
self._target_locks: dict[str, asyncio.Lock] = {}
async def start(self) -> None:
- """Start Moltchat channel workers and websocket connection."""
+ """Start Mochat channel workers and websocket connection."""
if not self.config.claw_token:
- logger.error("Moltchat claw_token not configured")
+ logger.error("Mochat claw_token not configured")
return
self._running = True
@@ -296,7 +296,7 @@ class MoltchatChannel(BaseChannel):
async def send(self, msg: OutboundMessage) -> None:
"""Send outbound message to session or panel."""
if not self.config.claw_token:
- logger.warning("Moltchat claw_token missing, skip send")
+ logger.warning("Mochat claw_token missing, skip send")
return
content_parts = [msg.content.strip()] if msg.content and msg.content.strip() else []
@@ -306,9 +306,9 @@ class MoltchatChannel(BaseChannel):
if not content:
return
- target = resolve_moltchat_target(msg.chat_id)
+ target = resolve_mochat_target(msg.chat_id)
if not target.id:
- logger.warning("Moltchat outbound target is empty")
+ logger.warning("Mochat outbound target is empty")
return
is_panel = target.is_panel or target.id in self._panel_set
@@ -330,7 +330,7 @@ class MoltchatChannel(BaseChannel):
reply_to=msg.reply_to,
)
except Exception as e:
- logger.error(f"Failed to send Moltchat message: {e}")
+ logger.error(f"Failed to send Mochat message: {e}")
def _seed_targets_from_config(self) -> None:
sessions, self._auto_discover_sessions = self._normalize_id_list(self.config.sessions)
@@ -351,7 +351,7 @@ class MoltchatChannel(BaseChannel):
async def _start_socket_client(self) -> bool:
if not SOCKETIO_AVAILABLE:
- logger.warning("python-socketio not installed, Moltchat using polling fallback")
+ logger.warning("python-socketio not installed, Mochat using polling fallback")
return False
serializer = "default"
@@ -385,7 +385,7 @@ class MoltchatChannel(BaseChannel):
async def connect() -> None:
self._ws_connected = True
self._ws_ready = False
- logger.info("Moltchat websocket connected")
+ logger.info("Mochat websocket connected")
subscribed = await self._subscribe_all()
self._ws_ready = subscribed
@@ -400,13 +400,13 @@ class MoltchatChannel(BaseChannel):
return
self._ws_connected = False
self._ws_ready = False
- logger.warning("Moltchat websocket disconnected")
+ logger.warning("Mochat websocket disconnected")
await self._ensure_fallback_workers()
@client.event
async def connect_error(data: Any) -> None:
message = str(data)
- logger.error(f"Moltchat websocket connect error: {message}")
+ logger.error(f"Mochat websocket connect error: {message}")
@client.on("claw.session.events")
async def on_session_events(payload: dict[str, Any]) -> None:
@@ -441,7 +441,7 @@ class MoltchatChannel(BaseChannel):
)
return True
except Exception as e:
- logger.error(f"Failed to connect Moltchat websocket: {e}")
+ logger.error(f"Failed to connect Mochat websocket: {e}")
try:
await client.disconnect()
except Exception:
@@ -486,7 +486,7 @@ class MoltchatChannel(BaseChannel):
},
)
if not ack.get("result"):
- logger.error(f"Moltchat subscribeSessions failed: {ack.get('message', 'unknown error')}")
+ logger.error(f"Mochat subscribeSessions failed: {ack.get('message', 'unknown error')}")
return False
data = ack.get("data")
@@ -516,7 +516,7 @@ class MoltchatChannel(BaseChannel):
},
)
if not ack.get("result"):
- logger.error(f"Moltchat subscribePanels failed: {ack.get('message', 'unknown error')}")
+ logger.error(f"Mochat subscribePanels failed: {ack.get('message', 'unknown error')}")
return False
return True
@@ -544,7 +544,7 @@ class MoltchatChannel(BaseChannel):
try:
await self._refresh_targets(subscribe_new=self._ws_ready)
except Exception as e:
- logger.warning(f"Moltchat refresh failed: {e}")
+ logger.warning(f"Mochat refresh failed: {e}")
if self._fallback_mode:
await self._ensure_fallback_workers()
@@ -560,7 +560,7 @@ class MoltchatChannel(BaseChannel):
try:
response = await self._list_sessions()
except Exception as e:
- logger.warning(f"Moltchat listSessions failed: {e}")
+ logger.warning(f"Mochat listSessions failed: {e}")
return
sessions = response.get("sessions")
@@ -599,7 +599,7 @@ class MoltchatChannel(BaseChannel):
try:
response = await self._get_workspace_group()
except Exception as e:
- logger.warning(f"Moltchat getWorkspaceGroup failed: {e}")
+ logger.warning(f"Mochat getWorkspaceGroup failed: {e}")
return
raw_panels = response.get("panels")
@@ -683,7 +683,7 @@ class MoltchatChannel(BaseChannel):
except asyncio.CancelledError:
break
except Exception as e:
- logger.warning(f"Moltchat watch fallback error ({session_id}): {e}")
+ logger.warning(f"Mochat watch fallback error ({session_id}): {e}")
await asyncio.sleep(max(0.1, self.config.retry_delay_ms / 1000.0))
async def _panel_poll_worker(self, panel_id: str) -> None:
@@ -723,7 +723,7 @@ class MoltchatChannel(BaseChannel):
except asyncio.CancelledError:
break
except Exception as e:
- logger.warning(f"Moltchat panel polling error ({panel_id}): {e}")
+ logger.warning(f"Mochat panel polling error ({panel_id}): {e}")
await asyncio.sleep(sleep_s)
@@ -803,7 +803,7 @@ class MoltchatChannel(BaseChannel):
if message_id and self._remember_message_id(seen_key, message_id):
return
- raw_body = normalize_moltchat_content(payload.get("content"))
+ raw_body = normalize_mochat_content(payload.get("content"))
if not raw_body:
raw_body = "[empty message]"
@@ -826,7 +826,7 @@ class MoltchatChannel(BaseChannel):
if require_mention and not was_mentioned and not use_delay:
return
- entry = MoltchatBufferedEntry(
+ entry = MochatBufferedEntry(
raw_body=raw_body,
author=author,
sender_name=sender_name,
@@ -883,7 +883,7 @@ class MoltchatChannel(BaseChannel):
key: str,
target_id: str,
target_kind: str,
- entry: MoltchatBufferedEntry,
+ entry: MochatBufferedEntry,
) -> None:
state = self._delay_states.setdefault(key, DelayState())
@@ -912,7 +912,7 @@ class MoltchatChannel(BaseChannel):
target_id: str,
target_kind: str,
reason: str,
- entry: MoltchatBufferedEntry | None,
+ entry: MochatBufferedEntry | None,
) -> None:
state = self._delay_states.setdefault(key, DelayState())
@@ -944,7 +944,7 @@ class MoltchatChannel(BaseChannel):
self,
target_id: str,
target_kind: str,
- entries: list[MoltchatBufferedEntry],
+ entries: list[MochatBufferedEntry],
was_mentioned: bool,
) -> None:
if not entries:
@@ -1092,7 +1092,7 @@ class MoltchatChannel(BaseChannel):
try:
data = json.loads(self._cursor_path.read_text("utf-8"))
except Exception as e:
- logger.warning(f"Failed to read Moltchat cursor file: {e}")
+ logger.warning(f"Failed to read Mochat cursor file: {e}")
return
cursors = data.get("cursors") if isinstance(data, dict) else None
@@ -1114,14 +1114,14 @@ class MoltchatChannel(BaseChannel):
self._state_dir.mkdir(parents=True, exist_ok=True)
self._cursor_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", "utf-8")
except Exception as e:
- logger.warning(f"Failed to save Moltchat cursor file: {e}")
+ logger.warning(f"Failed to save Mochat cursor file: {e}")
def _base_url(self) -> str:
return self.config.base_url.strip().rstrip("/")
async def _post_json(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
if not self._http:
- raise RuntimeError("Moltchat HTTP client not initialized")
+ raise RuntimeError("Mochat HTTP client not initialized")
url = f"{self._base_url()}{path}"
response = await self._http.post(
@@ -1135,7 +1135,7 @@ class MoltchatChannel(BaseChannel):
text = response.text
if not response.is_success:
- raise RuntimeError(f"Moltchat HTTP {response.status_code}: {text[:200]}")
+ raise RuntimeError(f"Mochat HTTP {response.status_code}: {text[:200]}")
parsed: Any
try:
@@ -1146,7 +1146,7 @@ class MoltchatChannel(BaseChannel):
if isinstance(parsed, dict) and isinstance(parsed.get("code"), int):
if parsed["code"] != 200:
message = str(parsed.get("message") or parsed.get("name") or "request failed")
- raise RuntimeError(f"Moltchat API error: {message} (code={parsed['code']})")
+ raise RuntimeError(f"Mochat API error: {message} (code={parsed['code']})")
data = parsed.get("data")
return data if isinstance(data, dict) else {}
diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py
index 2039f82..3094aa1 100644
--- a/nanobot/cli/commands.py
+++ b/nanobot/cli/commands.py
@@ -376,11 +376,11 @@ def channels_status():
fs_config
)
- # Moltchat
- mc = config.channels.moltchat
+ # Mochat
+ mc = config.channels.mochat
mc_base = mc.base_url or "[dim]not configured[/dim]"
table.add_row(
- "Moltchat",
+ "Mochat",
"✓" if mc.enabled else "✗",
mc_base
)
diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py
index 4df4251..1d6ca9e 100644
--- a/nanobot/config/schema.py
+++ b/nanobot/config/schema.py
@@ -39,18 +39,18 @@ class DiscordConfig(BaseModel):
intents: int = 37377 # GUILDS + GUILD_MESSAGES + DIRECT_MESSAGES + MESSAGE_CONTENT
-class MoltchatMentionConfig(BaseModel):
- """Moltchat mention behavior configuration."""
+class MochatMentionConfig(BaseModel):
+ """Mochat mention behavior configuration."""
require_in_groups: bool = False
-class MoltchatGroupRule(BaseModel):
- """Moltchat per-group mention requirement."""
+class MochatGroupRule(BaseModel):
+ """Mochat per-group mention requirement."""
require_mention: bool = False
-class MoltchatConfig(BaseModel):
- """Moltchat channel configuration."""
+class MochatConfig(BaseModel):
+ """Mochat channel configuration."""
enabled: bool = False
base_url: str = "http://localhost:11000"
socket_url: str = ""
@@ -69,8 +69,8 @@ class MoltchatConfig(BaseModel):
sessions: list[str] = Field(default_factory=list)
panels: list[str] = Field(default_factory=list)
allow_from: list[str] = Field(default_factory=list)
- mention: MoltchatMentionConfig = Field(default_factory=MoltchatMentionConfig)
- groups: dict[str, MoltchatGroupRule] = Field(default_factory=dict)
+ mention: MochatMentionConfig = Field(default_factory=MochatMentionConfig)
+ groups: dict[str, MochatGroupRule] = Field(default_factory=dict)
reply_delay_mode: str = "non-mention" # off | non-mention
reply_delay_ms: int = 120000
@@ -81,7 +81,7 @@ class ChannelsConfig(BaseModel):
telegram: TelegramConfig = Field(default_factory=TelegramConfig)
discord: DiscordConfig = Field(default_factory=DiscordConfig)
feishu: FeishuConfig = Field(default_factory=FeishuConfig)
- moltchat: MoltchatConfig = Field(default_factory=MoltchatConfig)
+ mochat: MochatConfig = Field(default_factory=MochatConfig)
class AgentDefaults(BaseModel):
diff --git a/tests/test_moltchat_channel.py b/tests/test_mochat_channel.py
similarity index 73%
rename from tests/test_moltchat_channel.py
rename to tests/test_mochat_channel.py
index 1f65a68..4d73840 100644
--- a/tests/test_moltchat_channel.py
+++ b/tests/test_mochat_channel.py
@@ -1,27 +1,27 @@
import pytest
from nanobot.bus.queue import MessageBus
-from nanobot.channels.moltchat import (
- MoltchatBufferedEntry,
- MoltchatChannel,
+from nanobot.channels.mochat import (
+ MochatBufferedEntry,
+ MochatChannel,
build_buffered_body,
- resolve_moltchat_target,
+ resolve_mochat_target,
resolve_require_mention,
resolve_was_mentioned,
)
-from nanobot.config.schema import MoltchatConfig, MoltchatGroupRule, MoltchatMentionConfig
+from nanobot.config.schema import MochatConfig, MochatGroupRule, MochatMentionConfig
-def test_resolve_moltchat_target_prefixes() -> None:
- t = resolve_moltchat_target("panel:abc")
+def test_resolve_mochat_target_prefixes() -> None:
+ t = resolve_mochat_target("panel:abc")
assert t.id == "abc"
assert t.is_panel is True
- t = resolve_moltchat_target("session_123")
+ t = resolve_mochat_target("session_123")
assert t.id == "session_123"
assert t.is_panel is False
- t = resolve_moltchat_target("mochat:session_456")
+ t = resolve_mochat_target("mochat:session_456")
assert t.id == "session_456"
assert t.is_panel is False
@@ -40,12 +40,12 @@ def test_resolve_was_mentioned_from_meta_and_text() -> None:
def test_resolve_require_mention_priority() -> None:
- cfg = MoltchatConfig(
+ cfg = MochatConfig(
groups={
- "*": MoltchatGroupRule(require_mention=False),
- "group-a": MoltchatGroupRule(require_mention=True),
+ "*": MochatGroupRule(require_mention=False),
+ "group-a": MochatGroupRule(require_mention=True),
},
- mention=MoltchatMentionConfig(require_in_groups=False),
+ mention=MochatMentionConfig(require_in_groups=False),
)
assert resolve_require_mention(cfg, session_id="panel-x", group_id="group-a") is True
@@ -55,14 +55,14 @@ def test_resolve_require_mention_priority() -> None:
@pytest.mark.asyncio
async def test_delay_buffer_flushes_on_mention() -> None:
bus = MessageBus()
- cfg = MoltchatConfig(
+ cfg = MochatConfig(
enabled=True,
claw_token="token",
agent_user_id="bot",
reply_delay_mode="non-mention",
reply_delay_ms=60_000,
)
- channel = MoltchatChannel(cfg, bus)
+ channel = MochatChannel(cfg, bus)
first = {
"type": "message.add",
@@ -94,7 +94,7 @@ async def test_delay_buffer_flushes_on_mention() -> None:
assert bus.inbound_size == 1
msg = await bus.consume_inbound()
- assert msg.channel == "moltchat"
+ assert msg.channel == "mochat"
assert msg.chat_id == "panel-1"
assert "user1: first" in msg.content
assert "user2: hello <@bot>" in msg.content
@@ -106,8 +106,8 @@ async def test_delay_buffer_flushes_on_mention() -> None:
def test_build_buffered_body_group_labels() -> None:
body = build_buffered_body(
entries=[
- MoltchatBufferedEntry(raw_body="a", author="u1", sender_name="Alice"),
- MoltchatBufferedEntry(raw_body="b", author="u2", sender_username="bot"),
+ MochatBufferedEntry(raw_body="a", author="u1", sender_name="Alice"),
+ MochatBufferedEntry(raw_body="b", author="u2", sender_username="bot"),
],
is_group=True,
)