diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33dd3dafc..447e77846 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,12 @@
Docs: https://docs.clawd.bot
-## 2026.1.25
+## 2026.1.26
Status: unreleased.
### Changes
+- Rebrand: rename the npm package/CLI to `moltbot`, add a `clawdbot` compatibility shim, and move extensions to the `@moltbot/*` scope.
+- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev.
- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk).
- Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt.
- Agents: summarize dropped messages during compaction safeguard pruning. (#2509) Thanks @jogi47.
@@ -54,6 +56,7 @@ Status: unreleased.
- Telegram: add optional silent send flag (disable notifications). (#2382) Thanks @Suksham-sharma.
- Telegram: support editing sent messages via message(action="edit"). (#2394) Thanks @marcelomar21.
- Telegram: add sticker receive/send with vision caching. (#2629) Thanks @longjos.
+- Telegram: send sticker pixels to vision models. (#2650)
- Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918.
- Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999.
- macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal.
diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts
index a015c0e36..85dc9c566 100644
--- a/apps/android/app/build.gradle.kts
+++ b/apps/android/app/build.gradle.kts
@@ -21,8 +21,8 @@ android {
applicationId = "com.clawdbot.android"
minSdk = 31
targetSdk = 36
- versionCode = 202601250
- versionName = "2026.1.25"
+ versionCode = 202601260
+ versionName = "2026.1.26"
}
buildTypes {
diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist
index e1cf2b71d..fb5212e59 100644
--- a/apps/ios/Sources/Info.plist
+++ b/apps/ios/Sources/Info.plist
@@ -19,9 +19,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 2026.1.25
+ 2026.1.26
CFBundleVersion
- 20260125
+ 20260126
NSAppTransportSecurity
NSAllowsArbitraryLoadsInWebContent
diff --git a/apps/ios/Tests/Info.plist b/apps/ios/Tests/Info.plist
index 6ff977b05..7a6bc5cec 100644
--- a/apps/ios/Tests/Info.plist
+++ b/apps/ios/Tests/Info.plist
@@ -17,8 +17,8 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 2026.1.25
+ 2026.1.26
CFBundleVersion
- 20260125
+ 20260126
diff --git a/apps/ios/project.yml b/apps/ios/project.yml
index 0073b4ef9..c955bce24 100644
--- a/apps/ios/project.yml
+++ b/apps/ios/project.yml
@@ -81,8 +81,8 @@ targets:
properties:
CFBundleDisplayName: Clawdbot
CFBundleIconName: AppIcon
- CFBundleShortVersionString: "2026.1.25"
- CFBundleVersion: "20260125"
+ CFBundleShortVersionString: "2026.1.26"
+ CFBundleVersion: "20260126"
UILaunchScreen: {}
UIApplicationSceneManifest:
UIApplicationSupportsMultipleScenes: false
@@ -130,5 +130,5 @@ targets:
path: Tests/Info.plist
properties:
CFBundleDisplayName: ClawdbotTests
- CFBundleShortVersionString: "2026.1.25"
- CFBundleVersion: "20260125"
+ CFBundleShortVersionString: "2026.1.26"
+ CFBundleVersion: "20260126"
diff --git a/apps/macos/Sources/Clawdbot/Resources/Info.plist b/apps/macos/Sources/Clawdbot/Resources/Info.plist
index ee9e3113d..c3031805e 100644
--- a/apps/macos/Sources/Clawdbot/Resources/Info.plist
+++ b/apps/macos/Sources/Clawdbot/Resources/Info.plist
@@ -15,9 +15,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 2026.1.25
+ 2026.1.26
CFBundleVersion
- 202601250
+ 202601260
CFBundleIconFile
Clawdbot
CFBundleURLTypes
diff --git a/docs/platforms/fly.md b/docs/platforms/fly.md
index dee731ea7..a9c8bafb4 100644
--- a/docs/platforms/fly.md
+++ b/docs/platforms/fly.md
@@ -185,7 +185,7 @@ cat > /data/clawdbot.json << 'EOF'
"bind": "auto"
},
"meta": {
- "lastTouchedVersion": "2026.1.25"
+ "lastTouchedVersion": "2026.1.26"
}
}
EOF
diff --git a/docs/platforms/mac/release.md b/docs/platforms/mac/release.md
index d3bfd02c3..b1226a5ad 100644
--- a/docs/platforms/mac/release.md
+++ b/docs/platforms/mac/release.md
@@ -30,17 +30,17 @@ Notes:
# From repo root; set release IDs so Sparkle feed is enabled.
# APP_BUILD must be numeric + monotonic for Sparkle compare.
BUNDLE_ID=com.clawdbot.mac \
-APP_VERSION=2026.1.25 \
+APP_VERSION=2026.1.26 \
APP_BUILD="$(git rev-list --count HEAD)" \
BUILD_CONFIG=release \
SIGN_IDENTITY="Developer ID Application: ()" \
scripts/package-mac-app.sh
# Zip for distribution (includes resource forks for Sparkle delta support)
-ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.25.zip
+ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.26.zip
# Optional: also build a styled DMG for humans (drag to /Applications)
-scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.25.dmg
+scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.26.dmg
# Recommended: build + notarize/staple zip + DMG
# First, create a keychain profile once:
@@ -48,26 +48,26 @@ scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.25.dmg
# --apple-id "" --team-id "" --password ""
NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \
BUNDLE_ID=com.clawdbot.mac \
-APP_VERSION=2026.1.25 \
+APP_VERSION=2026.1.26 \
APP_BUILD="$(git rev-list --count HEAD)" \
BUILD_CONFIG=release \
SIGN_IDENTITY="Developer ID Application: ()" \
scripts/package-mac-dist.sh
# Optional: ship dSYM alongside the release
-ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.25.dSYM.zip
+ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.26.dSYM.zip
```
## Appcast entry
Use the release note generator so Sparkle renders formatted HTML notes:
```bash
-SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.25.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml
+SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.26.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml
```
Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry.
Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing.
## Publish & verify
-- Upload `Clawdbot-2026.1.25.zip` (and `Clawdbot-2026.1.25.dSYM.zip`) to the GitHub release for tag `v2026.1.25`.
+- Upload `Clawdbot-2026.1.26.zip` (and `Clawdbot-2026.1.26.dSYM.zip`) to the GitHub release for tag `v2026.1.26`.
- Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml`.
- Sanity checks:
- `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200.
diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md
index 244757a48..a4c68e3e9 100644
--- a/docs/reference/RELEASING.md
+++ b/docs/reference/RELEASING.md
@@ -17,7 +17,7 @@ When the operator says “release”, immediately do this preflight (no extra qu
- Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed.
1) **Version & metadata**
-- [ ] Bump `package.json` version (e.g., `2026.1.25`).
+- [ ] Bump `package.json` version (e.g., `2026.1.26`).
- [ ] Run `pnpm plugins:sync` to align extension package versions + changelogs.
- [ ] Update CLI/version strings: [`src/cli/program.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/cli/program.ts) and the Baileys user agent in [`src/provider-web.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/provider-web.ts).
- [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js) for `clawdbot`.
diff --git a/extensions/bluebubbles/package.json b/extensions/bluebubbles/package.json
index 7d82036a0..3f9b5995e 100644
--- a/extensions/bluebubbles/package.json
+++ b/extensions/bluebubbles/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/bluebubbles",
- "version": "2026.1.25",
+ "name": "@moltbot/bluebubbles",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot BlueBubbles channel plugin",
"clawdbot": {
@@ -25,7 +25,7 @@
"order": 75
},
"install": {
- "npmSpec": "@clawdbot/bluebubbles",
+ "npmSpec": "@moltbot/bluebubbles",
"localPath": "extensions/bluebubbles",
"defaultChoice": "npm"
}
diff --git a/extensions/copilot-proxy/package.json b/extensions/copilot-proxy/package.json
index 2a9a63c71..16e76ba6a 100644
--- a/extensions/copilot-proxy/package.json
+++ b/extensions/copilot-proxy/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/copilot-proxy",
- "version": "2026.1.25",
+ "name": "@moltbot/copilot-proxy",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Copilot Proxy provider plugin",
"clawdbot": {
diff --git a/extensions/diagnostics-otel/package.json b/extensions/diagnostics-otel/package.json
index 65a6bf0cd..f6fb4aea6 100644
--- a/extensions/diagnostics-otel/package.json
+++ b/extensions/diagnostics-otel/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/diagnostics-otel",
- "version": "2026.1.25",
+ "name": "@moltbot/diagnostics-otel",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot diagnostics OpenTelemetry exporter",
"clawdbot": {
diff --git a/extensions/discord/package.json b/extensions/discord/package.json
index 90a99d4d3..b5b1c9a30 100644
--- a/extensions/discord/package.json
+++ b/extensions/discord/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/discord",
- "version": "2026.1.25",
+ "name": "@moltbot/discord",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Discord channel plugin",
"clawdbot": {
diff --git a/extensions/google-antigravity-auth/package.json b/extensions/google-antigravity-auth/package.json
index f1d8f86bd..6ccd2157b 100644
--- a/extensions/google-antigravity-auth/package.json
+++ b/extensions/google-antigravity-auth/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/google-antigravity-auth",
- "version": "2026.1.25",
+ "name": "@moltbot/google-antigravity-auth",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Google Antigravity OAuth provider plugin",
"clawdbot": {
diff --git a/extensions/google-gemini-cli-auth/package.json b/extensions/google-gemini-cli-auth/package.json
index 7e3fef15b..1aa094a0a 100644
--- a/extensions/google-gemini-cli-auth/package.json
+++ b/extensions/google-gemini-cli-auth/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/google-gemini-cli-auth",
- "version": "2026.1.25",
+ "name": "@moltbot/google-gemini-cli-auth",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Gemini CLI OAuth provider plugin",
"clawdbot": {
diff --git a/extensions/googlechat/package.json b/extensions/googlechat/package.json
index af1ccf8e1..fd77bc189 100644
--- a/extensions/googlechat/package.json
+++ b/extensions/googlechat/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/googlechat",
- "version": "2026.1.25",
+ "name": "@moltbot/googlechat",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Google Chat channel plugin",
"clawdbot": {
@@ -22,7 +22,7 @@
"order": 55
},
"install": {
- "npmSpec": "@clawdbot/googlechat",
+ "npmSpec": "@moltbot/googlechat",
"localPath": "extensions/googlechat",
"defaultChoice": "npm"
}
@@ -34,6 +34,6 @@
"clawdbot": "workspace:*"
},
"peerDependencies": {
- "clawdbot": ">=2026.1.25"
+ "clawdbot": ">=2026.1.26"
}
}
diff --git a/extensions/imessage/package.json b/extensions/imessage/package.json
index 944ad06bf..4efab972f 100644
--- a/extensions/imessage/package.json
+++ b/extensions/imessage/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/imessage",
- "version": "2026.1.25",
+ "name": "@moltbot/imessage",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot iMessage channel plugin",
"clawdbot": {
diff --git a/extensions/line/package.json b/extensions/line/package.json
index 346d66415..b08c56b69 100644
--- a/extensions/line/package.json
+++ b/extensions/line/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/line",
- "version": "2026.1.25",
+ "name": "@moltbot/line",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot LINE channel plugin",
"clawdbot": {
@@ -18,7 +18,7 @@
"quickstartAllowFrom": true
},
"install": {
- "npmSpec": "@clawdbot/line",
+ "npmSpec": "@moltbot/line",
"localPath": "extensions/line",
"defaultChoice": "npm"
}
diff --git a/extensions/llm-task/package.json b/extensions/llm-task/package.json
index d6bfbb31d..f8f65df37 100644
--- a/extensions/llm-task/package.json
+++ b/extensions/llm-task/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/llm-task",
- "version": "2026.1.25",
+ "name": "@moltbot/llm-task",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot JSON-only LLM task plugin",
"clawdbot": {
diff --git a/extensions/lobster/package.json b/extensions/lobster/package.json
index b73dbac69..2640f0135 100644
--- a/extensions/lobster/package.json
+++ b/extensions/lobster/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/lobster",
- "version": "2026.1.25",
+ "name": "@moltbot/lobster",
+ "version": "2026.1.26",
"type": "module",
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
"clawdbot": {
diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json
index 625c92df0..9c17962fe 100644
--- a/extensions/matrix/package.json
+++ b/extensions/matrix/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/matrix",
- "version": "2026.1.25",
+ "name": "@moltbot/matrix",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Matrix channel plugin",
"clawdbot": {
@@ -18,7 +18,7 @@
"quickstartAllowFrom": true
},
"install": {
- "npmSpec": "@clawdbot/matrix",
+ "npmSpec": "@moltbot/matrix",
"localPath": "extensions/matrix",
"defaultChoice": "npm"
}
diff --git a/extensions/mattermost/package.json b/extensions/mattermost/package.json
index 60c02d50f..5244710bc 100644
--- a/extensions/mattermost/package.json
+++ b/extensions/mattermost/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/mattermost",
- "version": "2026.1.25",
+ "name": "@moltbot/mattermost",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Mattermost channel plugin",
"clawdbot": {
@@ -17,7 +17,7 @@
"order": 65
},
"install": {
- "npmSpec": "@clawdbot/mattermost",
+ "npmSpec": "@moltbot/mattermost",
"localPath": "extensions/mattermost",
"defaultChoice": "npm"
}
diff --git a/extensions/memory-core/package.json b/extensions/memory-core/package.json
index af6a3f9cd..307d5b208 100644
--- a/extensions/memory-core/package.json
+++ b/extensions/memory-core/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/memory-core",
- "version": "2026.1.25",
+ "name": "@moltbot/memory-core",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot core memory search plugin",
"clawdbot": {
@@ -9,6 +9,6 @@
]
},
"peerDependencies": {
- "clawdbot": ">=2026.1.24-3"
+ "clawdbot": ">=2026.1.26"
}
}
diff --git a/extensions/memory-lancedb/package.json b/extensions/memory-lancedb/package.json
index e003f5890..a443687dc 100644
--- a/extensions/memory-lancedb/package.json
+++ b/extensions/memory-lancedb/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/memory-lancedb",
- "version": "2026.1.25",
+ "name": "@moltbot/memory-lancedb",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot LanceDB-backed long-term memory plugin with auto-recall/capture",
"dependencies": {
diff --git a/extensions/msteams/package.json b/extensions/msteams/package.json
index b94f8e76a..29a6cdcdd 100644
--- a/extensions/msteams/package.json
+++ b/extensions/msteams/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/msteams",
- "version": "2026.1.25",
+ "name": "@moltbot/msteams",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Microsoft Teams channel plugin",
"clawdbot": {
@@ -20,7 +20,7 @@
"order": 60
},
"install": {
- "npmSpec": "@clawdbot/msteams",
+ "npmSpec": "@moltbot/msteams",
"localPath": "extensions/msteams",
"defaultChoice": "npm"
}
diff --git a/extensions/nextcloud-talk/package.json b/extensions/nextcloud-talk/package.json
index 2da3f3b2a..aa96bad9a 100644
--- a/extensions/nextcloud-talk/package.json
+++ b/extensions/nextcloud-talk/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/nextcloud-talk",
- "version": "2026.1.25",
+ "name": "@moltbot/nextcloud-talk",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Nextcloud Talk channel plugin",
"clawdbot": {
@@ -22,7 +22,7 @@
"quickstartAllowFrom": true
},
"install": {
- "npmSpec": "@clawdbot/nextcloud-talk",
+ "npmSpec": "@moltbot/nextcloud-talk",
"localPath": "extensions/nextcloud-talk",
"defaultChoice": "npm"
}
diff --git a/extensions/nostr/package.json b/extensions/nostr/package.json
index b2fb4b799..bde398392 100644
--- a/extensions/nostr/package.json
+++ b/extensions/nostr/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/nostr",
- "version": "2026.1.25",
+ "name": "@moltbot/nostr",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Nostr channel plugin for NIP-04 encrypted DMs",
"clawdbot": {
@@ -18,7 +18,7 @@
"quickstartAllowFrom": true
},
"install": {
- "npmSpec": "@clawdbot/nostr",
+ "npmSpec": "@moltbot/nostr",
"localPath": "extensions/nostr",
"defaultChoice": "npm"
}
diff --git a/extensions/open-prose/package.json b/extensions/open-prose/package.json
index 052201205..2637a55f7 100644
--- a/extensions/open-prose/package.json
+++ b/extensions/open-prose/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/open-prose",
- "version": "2026.1.25",
+ "name": "@moltbot/open-prose",
+ "version": "2026.1.26",
"type": "module",
"description": "OpenProse VM skill pack plugin (slash command + telemetry).",
"clawdbot": {
diff --git a/extensions/signal/package.json b/extensions/signal/package.json
index 65948eb7b..b598bf996 100644
--- a/extensions/signal/package.json
+++ b/extensions/signal/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/signal",
- "version": "2026.1.25",
+ "name": "@moltbot/signal",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Signal channel plugin",
"clawdbot": {
diff --git a/extensions/slack/package.json b/extensions/slack/package.json
index 5bd452d2e..8f0c7531c 100644
--- a/extensions/slack/package.json
+++ b/extensions/slack/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/slack",
- "version": "2026.1.25",
+ "name": "@moltbot/slack",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Slack channel plugin",
"clawdbot": {
diff --git a/extensions/telegram/package.json b/extensions/telegram/package.json
index 64d3d7dea..149517e46 100644
--- a/extensions/telegram/package.json
+++ b/extensions/telegram/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/telegram",
- "version": "2026.1.25",
+ "name": "@moltbot/telegram",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Telegram channel plugin",
"clawdbot": {
diff --git a/extensions/tlon/package.json b/extensions/tlon/package.json
index 06750126d..e1382e96b 100644
--- a/extensions/tlon/package.json
+++ b/extensions/tlon/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/tlon",
- "version": "2026.1.25",
+ "name": "@moltbot/tlon",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Tlon/Urbit channel plugin",
"clawdbot": {
@@ -18,7 +18,7 @@
"quickstartAllowFrom": true
},
"install": {
- "npmSpec": "@clawdbot/tlon",
+ "npmSpec": "@moltbot/tlon",
"localPath": "extensions/tlon",
"defaultChoice": "npm"
}
diff --git a/extensions/twitch/package.json b/extensions/twitch/package.json
index 2c9dd2683..1931a2979 100644
--- a/extensions/twitch/package.json
+++ b/extensions/twitch/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/twitch",
- "version": "2026.1.23",
+ "name": "@moltbot/twitch",
+ "version": "2026.1.26",
"description": "Clawdbot Twitch channel plugin",
"type": "module",
"dependencies": {
diff --git a/extensions/voice-call/CHANGELOG.md b/extensions/voice-call/CHANGELOG.md
index 588817858..a757abe64 100644
--- a/extensions/voice-call/CHANGELOG.md
+++ b/extensions/voice-call/CHANGELOG.md
@@ -1,6 +1,6 @@
# Changelog
-## 2026.1.25
+## 2026.1.26
### Changes
- Breaking: voice-call TTS now uses core `messages.tts` (plugin TTS config deep‑merges with core).
diff --git a/extensions/voice-call/package.json b/extensions/voice-call/package.json
index 31b171f76..4d2fee875 100644
--- a/extensions/voice-call/package.json
+++ b/extensions/voice-call/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/voice-call",
- "version": "2026.1.25",
+ "name": "@moltbot/voice-call",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot voice-call plugin",
"dependencies": {
diff --git a/extensions/whatsapp/package.json b/extensions/whatsapp/package.json
index b7b57eb51..c8aef82ff 100644
--- a/extensions/whatsapp/package.json
+++ b/extensions/whatsapp/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/whatsapp",
- "version": "2026.1.25",
+ "name": "@moltbot/whatsapp",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot WhatsApp channel plugin",
"clawdbot": {
diff --git a/extensions/zalo/package.json b/extensions/zalo/package.json
index 8f077a6b3..b011c9e89 100644
--- a/extensions/zalo/package.json
+++ b/extensions/zalo/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/zalo",
- "version": "2026.1.25",
+ "name": "@moltbot/zalo",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Zalo channel plugin",
"clawdbot": {
@@ -21,7 +21,7 @@
"quickstartAllowFrom": true
},
"install": {
- "npmSpec": "@clawdbot/zalo",
+ "npmSpec": "@moltbot/zalo",
"localPath": "extensions/zalo",
"defaultChoice": "npm"
}
diff --git a/extensions/zalouser/package.json b/extensions/zalouser/package.json
index 0ab93d1ce..90077bafd 100644
--- a/extensions/zalouser/package.json
+++ b/extensions/zalouser/package.json
@@ -1,6 +1,6 @@
{
- "name": "@clawdbot/zalouser",
- "version": "2026.1.25",
+ "name": "@moltbot/zalouser",
+ "version": "2026.1.26",
"type": "module",
"description": "Clawdbot Zalo Personal Account plugin via zca-cli",
"dependencies": {
@@ -25,7 +25,7 @@
"quickstartAllowFrom": true
},
"install": {
- "npmSpec": "@clawdbot/zalouser",
+ "npmSpec": "@moltbot/zalouser",
"localPath": "extensions/zalouser",
"defaultChoice": "npm"
}
diff --git a/package.json b/package.json
index 1a6d65178..6a88df982 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,17 @@
{
- "name": "clawdbot",
- "version": "2026.1.25",
+ "name": "moltbot",
+ "version": "2026.1.26",
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
"type": "module",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
"./plugin-sdk": "./dist/plugin-sdk/index.js",
- "./plugin-sdk/*": "./dist/plugin-sdk/*"
+ "./plugin-sdk/*": "./dist/plugin-sdk/*",
+ "./cli-entry": "./dist/entry.js"
},
"bin": {
+ "moltbot": "dist/entry.js",
"clawdbot": "dist/entry.js"
},
"files": [
@@ -90,6 +92,7 @@
"ui:build": "node scripts/ui.js build",
"start": "node scripts/run-node.mjs",
"clawdbot": "node scripts/run-node.mjs",
+ "moltbot": "node scripts/run-node.mjs",
"gateway:watch": "node scripts/watch-node.mjs gateway --force",
"gateway:dev": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway",
"gateway:dev:reset": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset",
diff --git a/packages/clawdbot/package.json b/packages/clawdbot/package.json
new file mode 100644
index 000000000..dad75eda4
--- /dev/null
+++ b/packages/clawdbot/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "clawdbot",
+ "version": "2026.1.26",
+ "type": "module",
+ "description": "Compatibility shim that forwards to moltbot",
+ "exports": {
+ ".": "./index.js",
+ "./plugin-sdk": "./plugin-sdk/index.js"
+ },
+ "bin": {
+ "clawdbot": "./bin/clawdbot.js"
+ },
+ "dependencies": {
+ "moltbot": "workspace:*"
+ }
+}
diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts
index 5ba6826fe..1e2ebeb57 100644
--- a/src/auto-reply/commands-registry.data.ts
+++ b/src/auto-reply/commands-registry.data.ts
@@ -2,7 +2,11 @@ import { listChannelDocks } from "../channels/dock.js";
import { getActivePluginRegistry } from "../plugins/runtime.js";
import { listThinkingLevels } from "./thinking.js";
import { COMMAND_ARG_FORMATTERS } from "./commands-args.js";
-import type { ChatCommandDefinition, CommandScope } from "./commands-registry.types.js";
+import type {
+ ChatCommandDefinition,
+ CommandCategory,
+ CommandScope,
+} from "./commands-registry.types.js";
type DefineChatCommandInput = {
key: string;
@@ -16,6 +20,7 @@ type DefineChatCommandInput = {
textAlias?: string;
textAliases?: string[];
scope?: CommandScope;
+ category?: CommandCategory;
};
function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefinition {
@@ -37,6 +42,7 @@ function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefiniti
argsMenu: command.argsMenu,
textAliases: aliases,
scope,
+ category: command.category,
};
}
@@ -48,6 +54,7 @@ function defineDockCommand(dock: ChannelDock): ChatCommandDefinition {
nativeName: `dock_${dock.id}`,
description: `Switch to ${dock.id} for replies.`,
textAliases: [`/dock-${dock.id}`, `/dock_${dock.id}`],
+ category: "docks",
});
}
@@ -124,18 +131,21 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "help",
description: "Show available commands.",
textAlias: "/help",
+ category: "status",
}),
defineChatCommand({
key: "commands",
nativeName: "commands",
description: "List all slash commands.",
textAlias: "/commands",
+ category: "status",
}),
defineChatCommand({
key: "skill",
nativeName: "skill",
description: "Run a skill by name.",
textAlias: "/skill",
+ category: "tools",
args: [
{
name: "name",
@@ -156,6 +166,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "status",
description: "Show current status.",
textAlias: "/status",
+ category: "status",
}),
defineChatCommand({
key: "allowlist",
@@ -163,6 +174,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
textAlias: "/allowlist",
acceptsArgs: true,
scope: "text",
+ category: "management",
}),
defineChatCommand({
key: "approve",
@@ -170,6 +182,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
description: "Approve or deny exec requests.",
textAlias: "/approve",
acceptsArgs: true,
+ category: "management",
}),
defineChatCommand({
key: "context",
@@ -177,12 +190,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
description: "Explain how context is built and used.",
textAlias: "/context",
acceptsArgs: true,
+ category: "status",
}),
defineChatCommand({
key: "tts",
nativeName: "tts",
description: "Control text-to-speech (TTS).",
textAlias: "/tts",
+ category: "media",
args: [
{
name: "action",
@@ -225,12 +240,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "whoami",
description: "Show your sender id.",
textAlias: "/whoami",
+ category: "status",
}),
defineChatCommand({
key: "subagents",
nativeName: "subagents",
description: "List/stop/log/info subagent runs for this session.",
textAlias: "/subagents",
+ category: "management",
args: [
{
name: "action",
@@ -257,6 +274,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "config",
description: "Show or set config values.",
textAlias: "/config",
+ category: "management",
args: [
{
name: "action",
@@ -284,6 +302,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "debug",
description: "Set runtime debug overrides.",
textAlias: "/debug",
+ category: "management",
args: [
{
name: "action",
@@ -311,6 +330,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "usage",
description: "Usage footer or cost summary.",
textAlias: "/usage",
+ category: "options",
args: [
{
name: "mode",
@@ -326,18 +346,21 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "stop",
description: "Stop the current run.",
textAlias: "/stop",
+ category: "session",
}),
defineChatCommand({
key: "restart",
nativeName: "restart",
description: "Restart Clawdbot.",
textAlias: "/restart",
+ category: "tools",
}),
defineChatCommand({
key: "activation",
nativeName: "activation",
description: "Set group activation mode.",
textAlias: "/activation",
+ category: "management",
args: [
{
name: "mode",
@@ -353,6 +376,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "send",
description: "Set send policy.",
textAlias: "/send",
+ category: "management",
args: [
{
name: "mode",
@@ -369,6 +393,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
description: "Reset the current session.",
textAlias: "/reset",
acceptsArgs: true,
+ category: "session",
}),
defineChatCommand({
key: "new",
@@ -376,12 +401,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
description: "Start a new session.",
textAlias: "/new",
acceptsArgs: true,
+ category: "session",
}),
defineChatCommand({
key: "compact",
description: "Compact the session context.",
textAlias: "/compact",
scope: "text",
+ category: "session",
args: [
{
name: "instructions",
@@ -396,6 +423,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "think",
description: "Set thinking level.",
textAlias: "/think",
+ category: "options",
args: [
{
name: "level",
@@ -411,6 +439,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "verbose",
description: "Toggle verbose mode.",
textAlias: "/verbose",
+ category: "options",
args: [
{
name: "mode",
@@ -426,6 +455,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "reasoning",
description: "Toggle reasoning visibility.",
textAlias: "/reasoning",
+ category: "options",
args: [
{
name: "mode",
@@ -441,6 +471,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "elevated",
description: "Toggle elevated mode.",
textAlias: "/elevated",
+ category: "options",
args: [
{
name: "mode",
@@ -456,6 +487,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "exec",
description: "Set exec defaults for this session.",
textAlias: "/exec",
+ category: "options",
args: [
{
name: "options",
@@ -470,6 +502,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "model",
description: "Show or set the model.",
textAlias: "/model",
+ category: "options",
args: [
{
name: "model",
@@ -485,12 +518,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
textAlias: "/models",
argsParsing: "none",
acceptsArgs: true,
+ category: "options",
}),
defineChatCommand({
key: "queue",
nativeName: "queue",
description: "Adjust queue settings.",
textAlias: "/queue",
+ category: "options",
args: [
{
name: "mode",
@@ -523,6 +558,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
description: "Run host shell commands (host-only).",
textAlias: "/bash",
scope: "text",
+ category: "tools",
args: [
{
name: "command",
diff --git a/src/auto-reply/commands-registry.types.ts b/src/auto-reply/commands-registry.types.ts
index 5e5bdd8cb..6b9371604 100644
--- a/src/auto-reply/commands-registry.types.ts
+++ b/src/auto-reply/commands-registry.types.ts
@@ -2,6 +2,15 @@ import type { ClawdbotConfig } from "../config/types.js";
export type CommandScope = "text" | "native" | "both";
+export type CommandCategory =
+ | "session"
+ | "options"
+ | "status"
+ | "management"
+ | "media"
+ | "tools"
+ | "docks";
+
export type CommandArgType = "string" | "number" | "boolean";
export type CommandArgChoiceContext = {
@@ -51,6 +60,7 @@ export type ChatCommandDefinition = {
formatArgs?: (values: CommandArgValues) => string | undefined;
argsMenu?: CommandArgMenuSpec | "auto";
scope: CommandScope;
+ category?: CommandCategory;
};
export type NativeCommandSpec = {
diff --git a/src/auto-reply/reply/commands-info.test.ts b/src/auto-reply/reply/commands-info.test.ts
new file mode 100644
index 000000000..9751c39cc
--- /dev/null
+++ b/src/auto-reply/reply/commands-info.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, it } from "vitest";
+import { buildCommandsPaginationKeyboard } from "./commands-info.js";
+
+describe("buildCommandsPaginationKeyboard", () => {
+ it("adds agent id to callback data when provided", () => {
+ const keyboard = buildCommandsPaginationKeyboard(2, 3, "agent-main");
+ expect(keyboard[0]).toEqual([
+ { text: "◀ Prev", callback_data: "commands_page_1:agent-main" },
+ { text: "2/3", callback_data: "commands_page_noop:agent-main" },
+ { text: "Next ▶", callback_data: "commands_page_3:agent-main" },
+ ]);
+ });
+});
diff --git a/src/auto-reply/reply/commands-info.ts b/src/auto-reply/reply/commands-info.ts
index 1a525150c..e7d8a8f6f 100644
--- a/src/auto-reply/reply/commands-info.ts
+++ b/src/auto-reply/reply/commands-info.ts
@@ -1,6 +1,10 @@
import { logVerbose } from "../../globals.js";
-import { listSkillCommandsForWorkspace } from "../skill-commands.js";
-import { buildCommandsMessage, buildHelpMessage } from "../status.js";
+import { listSkillCommandsForAgents } from "../skill-commands.js";
+import {
+ buildCommandsMessage,
+ buildCommandsMessagePaginated,
+ buildHelpMessage,
+} from "../status.js";
import { buildStatusReply } from "./commands-status.js";
import { buildContextReply } from "./commands-context-report.js";
import type { CommandHandler } from "./commands-types.js";
@@ -31,16 +35,78 @@ export const handleCommandsListCommand: CommandHandler = async (params, allowTex
}
const skillCommands =
params.skillCommands ??
- listSkillCommandsForWorkspace({
- workspaceDir: params.workspaceDir,
+ listSkillCommandsForAgents({
cfg: params.cfg,
+ agentIds: params.agentId ? [params.agentId] : undefined,
});
+ const surface = params.ctx.Surface;
+
+ if (surface === "telegram") {
+ const result = buildCommandsMessagePaginated(params.cfg, skillCommands, {
+ page: 1,
+ surface,
+ });
+
+ if (result.totalPages > 1) {
+ return {
+ shouldContinue: false,
+ reply: {
+ text: result.text,
+ channelData: {
+ telegram: {
+ buttons: buildCommandsPaginationKeyboard(
+ result.currentPage,
+ result.totalPages,
+ params.agentId,
+ ),
+ },
+ },
+ },
+ };
+ }
+
+ return {
+ shouldContinue: false,
+ reply: { text: result.text },
+ };
+ }
+
return {
shouldContinue: false,
- reply: { text: buildCommandsMessage(params.cfg, skillCommands) },
+ reply: { text: buildCommandsMessage(params.cfg, skillCommands, { surface }) },
};
};
+export function buildCommandsPaginationKeyboard(
+ currentPage: number,
+ totalPages: number,
+ agentId?: string,
+): Array> {
+ const buttons: Array<{ text: string; callback_data: string }> = [];
+ const suffix = agentId ? `:${agentId}` : "";
+
+ if (currentPage > 1) {
+ buttons.push({
+ text: "◀ Prev",
+ callback_data: `commands_page_${currentPage - 1}${suffix}`,
+ });
+ }
+
+ buttons.push({
+ text: `${currentPage}/${totalPages}`,
+ callback_data: `commands_page_noop${suffix}`,
+ });
+
+ if (currentPage < totalPages) {
+ buttons.push({
+ text: "Next ▶",
+ callback_data: `commands_page_${currentPage + 1}${suffix}`,
+ });
+ }
+
+ return [buttons];
+}
+
export const handleStatusCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
const statusRequested =
diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts
index 31b6b92ec..465352538 100644
--- a/src/auto-reply/status.test.ts
+++ b/src/auto-reply/status.test.ts
@@ -4,7 +4,20 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { normalizeTestText } from "../../test/helpers/normalize-text.js";
import { withTempHome } from "../../test/helpers/temp-home.js";
import type { ClawdbotConfig } from "../config/config.js";
-import { buildCommandsMessage, buildHelpMessage, buildStatusMessage } from "./status.js";
+import {
+ buildCommandsMessage,
+ buildCommandsMessagePaginated,
+ buildHelpMessage,
+ buildStatusMessage,
+} from "./status.js";
+
+const { listPluginCommands } = vi.hoisted(() => ({
+ listPluginCommands: vi.fn(() => []),
+}));
+
+vi.mock("../plugins/commands.js", () => ({
+ listPluginCommands,
+}));
afterEach(() => {
vi.restoreAllMocks();
@@ -400,10 +413,12 @@ describe("buildCommandsMessage", () => {
const text = buildCommandsMessage({
commands: { config: false, debug: false },
} as ClawdbotConfig);
+ expect(text).toContain("ℹ️ Slash commands");
+ expect(text).toContain("Status");
expect(text).toContain("/commands - List all slash commands.");
expect(text).toContain("/skill - Run a skill by name.");
- expect(text).toContain("/think (aliases: /thinking, /t) - Set thinking level.");
- expect(text).toContain("/compact (text-only) - Compact the session context.");
+ expect(text).toContain("/think (/thinking, /t) - Set thinking level.");
+ expect(text).toContain("/compact [text] - Compact the session context.");
expect(text).not.toContain("/config");
expect(text).not.toContain("/debug");
});
@@ -430,8 +445,39 @@ describe("buildHelpMessage", () => {
const text = buildHelpMessage({
commands: { config: false, debug: false },
} as ClawdbotConfig);
- expect(text).toContain("Skills: /skill [input]");
+ expect(text).toContain("Skills");
+ expect(text).toContain("/skill [input]");
expect(text).not.toContain("/config");
expect(text).not.toContain("/debug");
});
});
+
+describe("buildCommandsMessagePaginated", () => {
+ it("formats telegram output with pages", () => {
+ const result = buildCommandsMessagePaginated(
+ {
+ commands: { config: false, debug: false },
+ } as ClawdbotConfig,
+ undefined,
+ { surface: "telegram", page: 1 },
+ );
+ expect(result.text).toContain("ℹ️ Commands (1/");
+ expect(result.text).toContain("Session");
+ expect(result.text).toContain("/stop - Stop the current run.");
+ });
+
+ it("includes plugin commands in the paginated list", () => {
+ listPluginCommands.mockReturnValue([
+ { name: "plugin_cmd", description: "Plugin command", pluginId: "demo-plugin" },
+ ]);
+ const result = buildCommandsMessagePaginated(
+ {
+ commands: { config: false, debug: false },
+ } as ClawdbotConfig,
+ undefined,
+ { surface: "telegram", page: 99 },
+ );
+ expect(result.text).toContain("Plugins");
+ expect(result.text).toContain("/plugin_cmd (demo-plugin) - Plugin command");
+ });
+});
diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts
index 733205c8c..7344b7502 100644
--- a/src/auto-reply/status.ts
+++ b/src/auto-reply/status.ts
@@ -29,9 +29,14 @@ import {
resolveModelCostConfig,
} from "../utils/usage-format.js";
import { VERSION } from "../version.js";
-import { listChatCommands, listChatCommandsForConfig } from "./commands-registry.js";
+import {
+ listChatCommands,
+ listChatCommandsForConfig,
+ type ChatCommandDefinition,
+} from "./commands-registry.js";
import { listPluginCommands } from "../plugins/commands.js";
import type { SkillCommandSpec } from "../agents/skills.js";
+import type { CommandCategory } from "./commands-registry.types.js";
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js";
import type { MediaUnderstandingDecision } from "../media-understanding/types.js";
@@ -427,61 +432,203 @@ export function buildStatusMessage(args: StatusArgs): string {
.join("\n");
}
+const CATEGORY_LABELS: Record = {
+ session: "Session",
+ options: "Options",
+ status: "Status",
+ management: "Management",
+ media: "Media",
+ tools: "Tools",
+ docks: "Docks",
+};
+
+const CATEGORY_ORDER: CommandCategory[] = [
+ "session",
+ "options",
+ "status",
+ "management",
+ "media",
+ "tools",
+ "docks",
+];
+
+function groupCommandsByCategory(
+ commands: ChatCommandDefinition[],
+): Map {
+ const grouped = new Map();
+ for (const category of CATEGORY_ORDER) {
+ grouped.set(category, []);
+ }
+ for (const command of commands) {
+ const category = command.category ?? "tools";
+ const list = grouped.get(category) ?? [];
+ list.push(command);
+ grouped.set(category, list);
+ }
+ return grouped;
+}
+
export function buildHelpMessage(cfg?: ClawdbotConfig): string {
- const options = [
- "/think ",
- "/verbose on|full|off",
- "/reasoning on|off",
- "/elevated on|off|ask|full",
- "/model ",
- "/usage off|tokens|full",
- ];
- if (cfg?.commands?.config === true) options.push("/config show");
- if (cfg?.commands?.debug === true) options.push("/debug show");
- return [
- "ℹ️ Help",
- "Shortcuts: /new reset | /compact [instructions] | /restart relink (if enabled)",
- `Options: ${options.join(" | ")}`,
- "Skills: /skill [input]",
- "More: /commands for all slash commands",
- ].join("\n");
+ const lines = ["ℹ️ Help", ""];
+
+ lines.push("Session");
+ lines.push(" /new | /reset | /compact [instructions] | /stop");
+ lines.push("");
+
+ const optionParts = ["/think ", "/model ", "/verbose on|off"];
+ if (cfg?.commands?.config === true) optionParts.push("/config");
+ if (cfg?.commands?.debug === true) optionParts.push("/debug");
+ lines.push("Options");
+ lines.push(` ${optionParts.join(" | ")}`);
+ lines.push("");
+
+ lines.push("Status");
+ lines.push(" /status | /whoami | /context");
+ lines.push("");
+
+ lines.push("Skills");
+ lines.push(" /skill [input]");
+
+ lines.push("");
+ lines.push("More: /commands for full list");
+
+ return lines.join("\n");
+}
+
+const COMMANDS_PER_PAGE = 8;
+
+export type CommandsMessageOptions = {
+ page?: number;
+ surface?: string;
+};
+
+export type CommandsMessageResult = {
+ text: string;
+ totalPages: number;
+ currentPage: number;
+ hasNext: boolean;
+ hasPrev: boolean;
+};
+
+function formatCommandEntry(command: ChatCommandDefinition): string {
+ const primary = command.nativeName
+ ? `/${command.nativeName}`
+ : command.textAliases[0]?.trim() || `/${command.key}`;
+ const seen = new Set();
+ const aliases = command.textAliases
+ .map((alias) => alias.trim())
+ .filter(Boolean)
+ .filter((alias) => alias.toLowerCase() !== primary.toLowerCase())
+ .filter((alias) => {
+ const key = alias.toLowerCase();
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+ const aliasLabel = aliases.length ? ` (${aliases.join(", ")})` : "";
+ const scopeLabel = command.scope === "text" ? " [text]" : "";
+ return `${primary}${aliasLabel}${scopeLabel} - ${command.description}`;
+}
+
+type CommandsListItem = {
+ label: string;
+ text: string;
+};
+
+function buildCommandItems(
+ commands: ChatCommandDefinition[],
+ pluginCommands: ReturnType,
+): CommandsListItem[] {
+ const grouped = groupCommandsByCategory(commands);
+ const items: CommandsListItem[] = [];
+
+ for (const category of CATEGORY_ORDER) {
+ const categoryCommands = grouped.get(category) ?? [];
+ if (categoryCommands.length === 0) continue;
+ const label = CATEGORY_LABELS[category];
+ for (const command of categoryCommands) {
+ items.push({ label, text: formatCommandEntry(command) });
+ }
+ }
+
+ for (const command of pluginCommands) {
+ const pluginLabel = command.pluginId ? ` (${command.pluginId})` : "";
+ items.push({
+ label: "Plugins",
+ text: `/${command.name}${pluginLabel} - ${command.description}`,
+ });
+ }
+
+ return items;
+}
+
+function formatCommandList(items: CommandsListItem[]): string {
+ const lines: string[] = [];
+ let currentLabel: string | null = null;
+
+ for (const item of items) {
+ if (item.label !== currentLabel) {
+ if (lines.length > 0) lines.push("");
+ lines.push(item.label);
+ currentLabel = item.label;
+ }
+ lines.push(` ${item.text}`);
+ }
+
+ return lines.join("\n");
}
export function buildCommandsMessage(
cfg?: ClawdbotConfig,
skillCommands?: SkillCommandSpec[],
+ options?: CommandsMessageOptions,
): string {
- const lines = ["ℹ️ Slash commands"];
+ const result = buildCommandsMessagePaginated(cfg, skillCommands, options);
+ return result.text;
+}
+
+export function buildCommandsMessagePaginated(
+ cfg?: ClawdbotConfig,
+ skillCommands?: SkillCommandSpec[],
+ options?: CommandsMessageOptions,
+): CommandsMessageResult {
+ const page = Math.max(1, options?.page ?? 1);
+ const surface = options?.surface?.toLowerCase();
+ const isTelegram = surface === "telegram";
+
const commands = cfg
? listChatCommandsForConfig(cfg, { skillCommands })
: listChatCommands({ skillCommands });
- for (const command of commands) {
- const primary = command.nativeName
- ? `/${command.nativeName}`
- : command.textAliases[0]?.trim() || `/${command.key}`;
- const seen = new Set();
- const aliases = command.textAliases
- .map((alias) => alias.trim())
- .filter(Boolean)
- .filter((alias) => alias.toLowerCase() !== primary.toLowerCase())
- .filter((alias) => {
- const key = alias.toLowerCase();
- if (seen.has(key)) return false;
- seen.add(key);
- return true;
- });
- const aliasLabel = aliases.length ? ` (aliases: ${aliases.join(", ")})` : "";
- const scopeLabel = command.scope === "text" ? " (text-only)" : "";
- lines.push(`${primary}${aliasLabel}${scopeLabel} - ${command.description}`);
- }
const pluginCommands = listPluginCommands();
- if (pluginCommands.length > 0) {
- lines.push("");
- lines.push("Plugin commands:");
- for (const command of pluginCommands) {
- const pluginLabel = command.pluginId ? ` (plugin: ${command.pluginId})` : "";
- lines.push(`/${command.name}${pluginLabel} - ${command.description}`);
- }
+ const items = buildCommandItems(commands, pluginCommands);
+
+ if (!isTelegram) {
+ const lines = ["ℹ️ Slash commands", ""];
+ lines.push(formatCommandList(items));
+ return {
+ text: lines.join("\n").trim(),
+ totalPages: 1,
+ currentPage: 1,
+ hasNext: false,
+ hasPrev: false,
+ };
}
- return lines.join("\n");
+
+ const totalCommands = items.length;
+ const totalPages = Math.max(1, Math.ceil(totalCommands / COMMANDS_PER_PAGE));
+ const currentPage = Math.min(page, totalPages);
+ const startIndex = (currentPage - 1) * COMMANDS_PER_PAGE;
+ const endIndex = startIndex + COMMANDS_PER_PAGE;
+ const pageItems = items.slice(startIndex, endIndex);
+
+ const lines = [`ℹ️ Commands (${currentPage}/${totalPages})`, ""];
+ lines.push(formatCommandList(pageItems));
+
+ return {
+ text: lines.join("\n").trim(),
+ totalPages,
+ currentPage,
+ hasNext: currentPage < totalPages,
+ hasPrev: currentPage > 1,
+ };
}
diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts
index 7a5b88fd7..477b98280 100644
--- a/src/telegram/bot-handlers.ts
+++ b/src/telegram/bot-handlers.ts
@@ -4,6 +4,10 @@ import {
createInboundDebouncer,
resolveInboundDebounceMs,
} from "../auto-reply/inbound-debounce.js";
+import { buildCommandsPaginationKeyboard } from "../auto-reply/reply/commands-info.js";
+import { buildCommandsMessagePaginated } from "../auto-reply/status.js";
+import { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
+import { resolveDefaultAgentId } from "../agents/agent-scope.js";
import { loadConfig } from "../config/config.js";
import { writeConfigFile } from "../config/io.js";
import { danger, logVerbose, warn } from "../globals.js";
@@ -17,6 +21,7 @@ import { migrateTelegramGroupConfig } from "./group-migration.js";
import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
import { readTelegramAllowFromStore } from "./pairing-store.js";
import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js";
+import { buildInlineKeyboard } from "./send.js";
export const registerTelegramHandlers = ({
cfg,
@@ -323,6 +328,47 @@ export const registerTelegramHandlers = ({
}
}
+ const paginationMatch = data.match(/^commands_page_(\d+|noop)(?::(.+))?$/);
+ if (paginationMatch) {
+ const pageValue = paginationMatch[1];
+ if (pageValue === "noop") return;
+
+ const page = Number.parseInt(pageValue, 10);
+ if (Number.isNaN(page) || page < 1) return;
+
+ const agentId = paginationMatch[2]?.trim() || resolveDefaultAgentId(cfg) || undefined;
+ const skillCommands = listSkillCommandsForAgents({
+ cfg,
+ agentIds: agentId ? [agentId] : undefined,
+ });
+ const result = buildCommandsMessagePaginated(cfg, skillCommands, {
+ page,
+ surface: "telegram",
+ });
+
+ const keyboard =
+ result.totalPages > 1
+ ? buildInlineKeyboard(
+ buildCommandsPaginationKeyboard(result.currentPage, result.totalPages, agentId),
+ )
+ : undefined;
+
+ try {
+ await bot.api.editMessageText(
+ callbackMessage.chat.id,
+ callbackMessage.message_id,
+ result.text,
+ keyboard ? { reply_markup: keyboard } : undefined,
+ );
+ } catch (editErr) {
+ const errStr = String(editErr);
+ if (!errStr.includes("message is not modified")) {
+ throw editErr;
+ }
+ }
+ return;
+ }
+
const syntheticMessage: TelegramMessage = {
...callbackMessage,
from: callback.from,
diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts
index 71ac8a011..f978be7c2 100644
--- a/src/telegram/bot-message-context.ts
+++ b/src/telegram/bot-message-context.ts
@@ -1,6 +1,12 @@
import type { Bot } from "grammy";
import { resolveAckReaction } from "../agents/identity.js";
+import {
+ findModelInCatalog,
+ loadModelCatalog,
+ modelSupportsVision,
+} from "../agents/model-catalog.js";
+import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
import { hasControlCommand } from "../auto-reply/command-detection.js";
import { normalizeCommandBody } from "../auto-reply/commands-registry.js";
import { formatInboundEnvelope, resolveEnvelopeFormatOptions } from "../auto-reply/envelope.js";
@@ -104,6 +110,24 @@ type BuildTelegramMessageContextParams = {
resolveTelegramGroupConfig: ResolveTelegramGroupConfig;
};
+async function resolveStickerVisionSupport(params: {
+ cfg: ClawdbotConfig;
+ agentId?: string;
+}): Promise {
+ try {
+ const catalog = await loadModelCatalog({ config: params.cfg });
+ const defaultModel = resolveDefaultModelForAgent({
+ cfg: params.cfg,
+ agentId: params.agentId,
+ });
+ const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model);
+ if (!entry) return false;
+ return modelSupportsVision(entry);
+ } catch {
+ return false;
+ }
+}
+
export const buildTelegramMessageContext = async ({
primaryCtx,
allMedia,
@@ -316,7 +340,10 @@ export const buildTelegramMessageContext = async ({
// Check if sticker has a cached description - if so, use it instead of sending the image
const cachedStickerDescription = allMedia[0]?.stickerMetadata?.cachedDescription;
- const stickerCacheHit = Boolean(cachedStickerDescription);
+ const stickerSupportsVision = msg.sticker
+ ? await resolveStickerVisionSupport({ cfg, agentId: route.agentId })
+ : false;
+ const stickerCacheHit = Boolean(cachedStickerDescription) && !stickerSupportsVision;
if (stickerCacheHit) {
// Format cached description with sticker context
const emoji = allMedia[0]?.stickerMetadata?.emoji;
diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts
index a3e9c3faa..27c6a3bfa 100644
--- a/src/telegram/bot-message-dispatch.ts
+++ b/src/telegram/bot-message-dispatch.ts
@@ -1,5 +1,11 @@
// @ts-nocheck
import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js";
+import {
+ findModelInCatalog,
+ loadModelCatalog,
+ modelSupportsVision,
+} from "../agents/model-catalog.js";
+import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
import { resolveChunkMode } from "../auto-reply/chunk.js";
import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
@@ -15,6 +21,18 @@ import { createTelegramDraftStream } from "./draft-stream.js";
import { cacheSticker, describeStickerImage } from "./sticker-cache.js";
import { resolveAgentDir } from "../agents/agent-scope.js";
+async function resolveStickerVisionSupport(cfg, agentId) {
+ try {
+ const catalog = await loadModelCatalog({ config: cfg });
+ const defaultModel = resolveDefaultModelForAgent({ cfg, agentId });
+ const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model);
+ if (!entry) return false;
+ return modelSupportsVision(entry);
+ } catch {
+ return false;
+ }
+}
+
export const dispatchTelegramMessage = async ({
context,
bot,
@@ -133,14 +151,18 @@ export const dispatchTelegramMessage = async ({
// Handle uncached stickers: get a dedicated vision description before dispatch
// This ensures we cache a raw description rather than a conversational response
const sticker = ctxPayload.Sticker;
- if (sticker?.fileUniqueId && !sticker.cachedDescription && ctxPayload.MediaPath) {
+ if (sticker?.fileUniqueId && ctxPayload.MediaPath) {
const agentDir = resolveAgentDir(cfg, route.agentId);
- const description = await describeStickerImage({
- imagePath: ctxPayload.MediaPath,
- cfg,
- agentDir,
- agentId: route.agentId,
- });
+ const stickerSupportsVision = await resolveStickerVisionSupport(cfg, route.agentId);
+ let description = sticker.cachedDescription ?? null;
+ if (!description) {
+ description = await describeStickerImage({
+ imagePath: ctxPayload.MediaPath,
+ cfg,
+ agentDir,
+ agentId: route.agentId,
+ });
+ }
if (description) {
// Format the description with sticker context
const stickerContext = [sticker.emoji, sticker.setName ? `from "${sticker.setName}"` : null]
@@ -148,17 +170,19 @@ export const dispatchTelegramMessage = async ({
.join(" ");
const formattedDesc = `[Sticker${stickerContext ? ` ${stickerContext}` : ""}] ${description}`;
- // Update context to use description instead of image
sticker.cachedDescription = description;
- ctxPayload.Body = formattedDesc;
- ctxPayload.BodyForAgent = formattedDesc;
- // Clear media paths so native vision doesn't process the image again
- ctxPayload.MediaPath = undefined;
- ctxPayload.MediaType = undefined;
- ctxPayload.MediaUrl = undefined;
- ctxPayload.MediaPaths = undefined;
- ctxPayload.MediaUrls = undefined;
- ctxPayload.MediaTypes = undefined;
+ if (!stickerSupportsVision) {
+ // Update context to use description instead of image
+ ctxPayload.Body = formattedDesc;
+ ctxPayload.BodyForAgent = formattedDesc;
+ // Clear media paths so native vision doesn't process the image again
+ ctxPayload.MediaPath = undefined;
+ ctxPayload.MediaType = undefined;
+ ctxPayload.MediaUrl = undefined;
+ ctxPayload.MediaPaths = undefined;
+ ctxPayload.MediaUrls = undefined;
+ ctxPayload.MediaTypes = undefined;
+ }
// Cache the description for future encounters
cacheSticker({
diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts
index 274f7c6a9..c2de155b0 100644
--- a/src/telegram/bot.test.ts
+++ b/src/telegram/bot.test.ts
@@ -93,6 +93,7 @@ const commandSpy = vi.fn();
const botCtorSpy = vi.fn();
const answerCallbackQuerySpy = vi.fn(async () => undefined);
const sendChatActionSpy = vi.fn();
+const editMessageTextSpy = vi.fn(async () => ({ message_id: 88 }));
const setMessageReactionSpy = vi.fn(async () => undefined);
const setMyCommandsSpy = vi.fn(async () => undefined);
const sendMessageSpy = vi.fn(async () => ({ message_id: 77 }));
@@ -102,6 +103,7 @@ type ApiStub = {
config: { use: (arg: unknown) => void };
answerCallbackQuery: typeof answerCallbackQuerySpy;
sendChatAction: typeof sendChatActionSpy;
+ editMessageText: typeof editMessageTextSpy;
setMessageReaction: typeof setMessageReactionSpy;
setMyCommands: typeof setMyCommandsSpy;
sendMessage: typeof sendMessageSpy;
@@ -112,6 +114,7 @@ const apiStub: ApiStub = {
config: { use: useSpy },
answerCallbackQuery: answerCallbackQuerySpy,
sendChatAction: sendChatActionSpy,
+ editMessageText: editMessageTextSpy,
setMessageReaction: setMessageReactionSpy,
setMyCommands: setMyCommandsSpy,
sendMessage: sendMessageSpy,
@@ -192,6 +195,7 @@ describe("createTelegramBot", () => {
sendPhotoSpy.mockReset();
setMessageReactionSpy.mockReset();
answerCallbackQuerySpy.mockReset();
+ editMessageTextSpy.mockReset();
setMyCommandsSpy.mockReset();
wasSentByBot.mockReset();
middlewareUseSpy.mockReset();
@@ -424,6 +428,87 @@ describe("createTelegramBot", () => {
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-2");
});
+ it("edits commands list for pagination callbacks", async () => {
+ onSpy.mockReset();
+ listSkillCommandsForAgents.mockReset();
+
+ createTelegramBot({ token: "tok" });
+ const callbackHandler = onSpy.mock.calls.find((call) => call[0] === "callback_query")?.[1] as (
+ ctx: Record,
+ ) => Promise;
+ expect(callbackHandler).toBeDefined();
+
+ await callbackHandler({
+ callbackQuery: {
+ id: "cbq-3",
+ data: "commands_page_2:main",
+ from: { id: 9, first_name: "Ada", username: "ada_bot" },
+ message: {
+ chat: { id: 1234, type: "private" },
+ date: 1736380800,
+ message_id: 12,
+ },
+ },
+ me: { username: "clawdbot_bot" },
+ getFile: async () => ({ download: async () => new Uint8Array() }),
+ });
+
+ expect(listSkillCommandsForAgents).toHaveBeenCalledWith({
+ cfg: expect.any(Object),
+ agentIds: ["main"],
+ });
+ expect(editMessageTextSpy).toHaveBeenCalledTimes(1);
+ const [chatId, messageId, text, params] = editMessageTextSpy.mock.calls[0] ?? [];
+ expect(chatId).toBe(1234);
+ expect(messageId).toBe(12);
+ expect(String(text)).toContain("ℹ️ Commands");
+ expect(params).toEqual(
+ expect.objectContaining({
+ reply_markup: expect.any(Object),
+ }),
+ );
+ });
+
+ it("blocks pagination callbacks when allowlist rejects sender", async () => {
+ onSpy.mockReset();
+ editMessageTextSpy.mockReset();
+
+ createTelegramBot({
+ token: "tok",
+ config: {
+ channels: {
+ telegram: {
+ dmPolicy: "pairing",
+ capabilities: { inlineButtons: "allowlist" },
+ allowFrom: [],
+ },
+ },
+ },
+ });
+ const callbackHandler = onSpy.mock.calls.find((call) => call[0] === "callback_query")?.[1] as (
+ ctx: Record,
+ ) => Promise;
+ expect(callbackHandler).toBeDefined();
+
+ await callbackHandler({
+ callbackQuery: {
+ id: "cbq-4",
+ data: "commands_page_2",
+ from: { id: 9, first_name: "Ada", username: "ada_bot" },
+ message: {
+ chat: { id: 1234, type: "private" },
+ date: 1736380800,
+ message_id: 13,
+ },
+ },
+ me: { username: "clawdbot_bot" },
+ getFile: async () => ({ download: async () => new Uint8Array() }),
+ });
+
+ expect(editMessageTextSpy).not.toHaveBeenCalled();
+ expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-4");
+ });
+
it("wraps inbound message with Telegram envelope", async () => {
const originalTz = process.env.TZ;
process.env.TZ = "Europe/Vienna";
diff --git a/src/telegram/sticker-cache.ts b/src/telegram/sticker-cache.ts
index 38f421851..ab322e59e 100644
--- a/src/telegram/sticker-cache.ts
+++ b/src/telegram/sticker-cache.ts
@@ -4,11 +4,13 @@ import type { ClawdbotConfig } from "../config/config.js";
import { STATE_DIR_CLAWDBOT } from "../config/paths.js";
import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
import { logVerbose } from "../globals.js";
+import type { ModelCatalogEntry } from "../agents/model-catalog.js";
import {
findModelInCatalog,
loadModelCatalog,
modelSupportsVision,
} from "../agents/model-catalog.js";
+import { resolveApiKeyForProvider } from "../agents/model-auth.js";
import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
import { resolveAutoImageModel } from "../media-understanding/runner.js";
@@ -140,6 +142,7 @@ export function getCacheStats(): { count: number; oldestAt?: string; newestAt?:
const STICKER_DESCRIPTION_PROMPT =
"Describe this sticker image in 1-2 sentences. Focus on what the sticker depicts (character, object, action, emotion). Be concise and objective.";
+const VISION_PROVIDERS = ["openai", "anthropic", "google", "minimax"] as const;
export interface DescribeStickerParams {
imagePath: string;
@@ -158,31 +161,79 @@ export async function describeStickerImage(params: DescribeStickerParams): Promi
const defaultModel = resolveDefaultModelForAgent({ cfg, agentId });
let activeModel = undefined as { provider: string; model: string } | undefined;
+ let catalog: ModelCatalogEntry[] = [];
try {
- const catalog = await loadModelCatalog({ config: cfg });
+ catalog = await loadModelCatalog({ config: cfg });
const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model);
- if (modelSupportsVision(entry)) {
+ const supportsVision = modelSupportsVision(entry);
+ if (supportsVision) {
activeModel = { provider: defaultModel.provider, model: defaultModel.model };
}
} catch {
// Ignore catalog failures; fall back to auto selection.
}
- const resolved = await resolveAutoImageModel({
- cfg,
- agentDir,
- activeModel,
- });
+ const hasProviderKey = async (provider: string) => {
+ try {
+ await resolveApiKeyForProvider({ provider, cfg, agentDir });
+ return true;
+ } catch {
+ return false;
+ }
+ };
+
+ const selectCatalogModel = (provider: string) => {
+ const entries = catalog.filter(
+ (entry) =>
+ entry.provider.toLowerCase() === provider.toLowerCase() && modelSupportsVision(entry),
+ );
+ if (entries.length === 0) return undefined;
+ const defaultId =
+ provider === "openai"
+ ? "gpt-5-mini"
+ : provider === "anthropic"
+ ? "claude-opus-4-5"
+ : provider === "google"
+ ? "gemini-3-flash-preview"
+ : "MiniMax-VL-01";
+ const preferred = entries.find((entry) => entry.id === defaultId);
+ return preferred ?? entries[0];
+ };
+
+ let resolved = null as { provider: string; model?: string } | null;
+ if (
+ activeModel &&
+ VISION_PROVIDERS.includes(activeModel.provider as (typeof VISION_PROVIDERS)[number]) &&
+ (await hasProviderKey(activeModel.provider))
+ ) {
+ resolved = activeModel;
+ }
+
if (!resolved) {
+ for (const provider of VISION_PROVIDERS) {
+ if (!(await hasProviderKey(provider))) continue;
+ const entry = selectCatalogModel(provider);
+ if (entry) {
+ resolved = { provider, model: entry.id };
+ break;
+ }
+ }
+ }
+
+ if (!resolved) {
+ resolved = await resolveAutoImageModel({
+ cfg,
+ agentDir,
+ activeModel,
+ });
+ }
+
+ if (!resolved?.model) {
logVerbose("telegram: no vision provider available for sticker description");
return null;
}
const { provider, model } = resolved;
- if (!model) {
- logVerbose(`telegram: no vision model available for ${provider}`);
- return null;
- }
logVerbose(`telegram: describing sticker with ${provider}/${model}`);
try {