Merge branch 'main' into serrrfirat/near-ai-provider

This commit is contained in:
firat.sertgoz 2026-01-27 13:26:12 +04:00 committed by GitHub
commit cb641962f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 753 additions and 180 deletions

View File

@ -2,10 +2,12 @@
Docs: https://docs.clawd.bot Docs: https://docs.clawd.bot
## 2026.1.25 ## 2026.1.26
Status: unreleased. Status: unreleased.
### Changes ### 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). - 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. - Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt.
- Agents: summarize dropped messages during compaction safeguard pruning. (#2509) Thanks @jogi47. - 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: add optional silent send flag (disable notifications). (#2382) Thanks @Suksham-sharma.
- Telegram: support editing sent messages via message(action="edit"). (#2394) Thanks @marcelomar21. - Telegram: support editing sent messages via message(action="edit"). (#2394) Thanks @marcelomar21.
- Telegram: add sticker receive/send with vision caching. (#2629) Thanks @longjos. - 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. - Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918.
- Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999. - Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999.
- macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal. - macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal.

View File

@ -21,8 +21,8 @@ android {
applicationId = "com.clawdbot.android" applicationId = "com.clawdbot.android"
minSdk = 31 minSdk = 31
targetSdk = 36 targetSdk = 36
versionCode = 202601250 versionCode = 202601260
versionName = "2026.1.25" versionName = "2026.1.26"
} }
buildTypes { buildTypes {

View File

@ -19,9 +19,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2026.1.25</string> <string>2026.1.26</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>20260125</string> <string>20260126</string>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoadsInWebContent</key> <key>NSAllowsArbitraryLoadsInWebContent</key>

View File

@ -17,8 +17,8 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>BNDL</string> <string>BNDL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2026.1.25</string> <string>2026.1.26</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>20260125</string> <string>20260126</string>
</dict> </dict>
</plist> </plist>

View File

@ -81,8 +81,8 @@ targets:
properties: properties:
CFBundleDisplayName: Clawdbot CFBundleDisplayName: Clawdbot
CFBundleIconName: AppIcon CFBundleIconName: AppIcon
CFBundleShortVersionString: "2026.1.25" CFBundleShortVersionString: "2026.1.26"
CFBundleVersion: "20260125" CFBundleVersion: "20260126"
UILaunchScreen: {} UILaunchScreen: {}
UIApplicationSceneManifest: UIApplicationSceneManifest:
UIApplicationSupportsMultipleScenes: false UIApplicationSupportsMultipleScenes: false
@ -130,5 +130,5 @@ targets:
path: Tests/Info.plist path: Tests/Info.plist
properties: properties:
CFBundleDisplayName: ClawdbotTests CFBundleDisplayName: ClawdbotTests
CFBundleShortVersionString: "2026.1.25" CFBundleShortVersionString: "2026.1.26"
CFBundleVersion: "20260125" CFBundleVersion: "20260126"

View File

@ -15,9 +15,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2026.1.25</string> <string>2026.1.26</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>202601250</string> <string>202601260</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>Clawdbot</string> <string>Clawdbot</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>

View File

@ -185,7 +185,7 @@ cat > /data/clawdbot.json << 'EOF'
"bind": "auto" "bind": "auto"
}, },
"meta": { "meta": {
"lastTouchedVersion": "2026.1.25" "lastTouchedVersion": "2026.1.26"
} }
} }
EOF EOF

View File

@ -30,17 +30,17 @@ Notes:
# From repo root; set release IDs so Sparkle feed is enabled. # From repo root; set release IDs so Sparkle feed is enabled.
# APP_BUILD must be numeric + monotonic for Sparkle compare. # APP_BUILD must be numeric + monotonic for Sparkle compare.
BUNDLE_ID=com.clawdbot.mac \ BUNDLE_ID=com.clawdbot.mac \
APP_VERSION=2026.1.25 \ APP_VERSION=2026.1.26 \
APP_BUILD="$(git rev-list --count HEAD)" \ APP_BUILD="$(git rev-list --count HEAD)" \
BUILD_CONFIG=release \ BUILD_CONFIG=release \
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \ SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
scripts/package-mac-app.sh scripts/package-mac-app.sh
# Zip for distribution (includes resource forks for Sparkle delta support) # 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) # 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 # Recommended: build + notarize/staple zip + DMG
# First, create a keychain profile once: # 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 "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>" # --apple-id "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>"
NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \ NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \
BUNDLE_ID=com.clawdbot.mac \ BUNDLE_ID=com.clawdbot.mac \
APP_VERSION=2026.1.25 \ APP_VERSION=2026.1.26 \
APP_BUILD="$(git rev-list --count HEAD)" \ APP_BUILD="$(git rev-list --count HEAD)" \
BUILD_CONFIG=release \ BUILD_CONFIG=release \
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \ SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
scripts/package-mac-dist.sh scripts/package-mac-dist.sh
# Optional: ship dSYM alongside the release # 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 ## Appcast entry
Use the release note generator so Sparkle renders formatted HTML notes: Use the release note generator so Sparkle renders formatted HTML notes:
```bash ```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. 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. Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing.
## Publish & verify ## 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`. - Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml`.
- Sanity checks: - Sanity checks:
- `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200. - `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200.

View File

@ -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. - Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed.
1) **Version & metadata** 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. - [ ] 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). - [ ] 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`. - [ ] 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`.

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/bluebubbles", "name": "@moltbot/bluebubbles",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot BlueBubbles channel plugin", "description": "Clawdbot BlueBubbles channel plugin",
"clawdbot": { "clawdbot": {
@ -25,7 +25,7 @@
"order": 75 "order": 75
}, },
"install": { "install": {
"npmSpec": "@clawdbot/bluebubbles", "npmSpec": "@moltbot/bluebubbles",
"localPath": "extensions/bluebubbles", "localPath": "extensions/bluebubbles",
"defaultChoice": "npm" "defaultChoice": "npm"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/copilot-proxy", "name": "@moltbot/copilot-proxy",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Copilot Proxy provider plugin", "description": "Clawdbot Copilot Proxy provider plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/diagnostics-otel", "name": "@moltbot/diagnostics-otel",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot diagnostics OpenTelemetry exporter", "description": "Clawdbot diagnostics OpenTelemetry exporter",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/discord", "name": "@moltbot/discord",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Discord channel plugin", "description": "Clawdbot Discord channel plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/google-antigravity-auth", "name": "@moltbot/google-antigravity-auth",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Google Antigravity OAuth provider plugin", "description": "Clawdbot Google Antigravity OAuth provider plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/google-gemini-cli-auth", "name": "@moltbot/google-gemini-cli-auth",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Gemini CLI OAuth provider plugin", "description": "Clawdbot Gemini CLI OAuth provider plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/googlechat", "name": "@moltbot/googlechat",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Google Chat channel plugin", "description": "Clawdbot Google Chat channel plugin",
"clawdbot": { "clawdbot": {
@ -22,7 +22,7 @@
"order": 55 "order": 55
}, },
"install": { "install": {
"npmSpec": "@clawdbot/googlechat", "npmSpec": "@moltbot/googlechat",
"localPath": "extensions/googlechat", "localPath": "extensions/googlechat",
"defaultChoice": "npm" "defaultChoice": "npm"
} }
@ -34,6 +34,6 @@
"clawdbot": "workspace:*" "clawdbot": "workspace:*"
}, },
"peerDependencies": { "peerDependencies": {
"clawdbot": ">=2026.1.25" "clawdbot": ">=2026.1.26"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/imessage", "name": "@moltbot/imessage",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot iMessage channel plugin", "description": "Clawdbot iMessage channel plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/line", "name": "@moltbot/line",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot LINE channel plugin", "description": "Clawdbot LINE channel plugin",
"clawdbot": { "clawdbot": {
@ -18,7 +18,7 @@
"quickstartAllowFrom": true "quickstartAllowFrom": true
}, },
"install": { "install": {
"npmSpec": "@clawdbot/line", "npmSpec": "@moltbot/line",
"localPath": "extensions/line", "localPath": "extensions/line",
"defaultChoice": "npm" "defaultChoice": "npm"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/llm-task", "name": "@moltbot/llm-task",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot JSON-only LLM task plugin", "description": "Clawdbot JSON-only LLM task plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/lobster", "name": "@moltbot/lobster",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)", "description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/matrix", "name": "@moltbot/matrix",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Matrix channel plugin", "description": "Clawdbot Matrix channel plugin",
"clawdbot": { "clawdbot": {
@ -18,7 +18,7 @@
"quickstartAllowFrom": true "quickstartAllowFrom": true
}, },
"install": { "install": {
"npmSpec": "@clawdbot/matrix", "npmSpec": "@moltbot/matrix",
"localPath": "extensions/matrix", "localPath": "extensions/matrix",
"defaultChoice": "npm" "defaultChoice": "npm"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/mattermost", "name": "@moltbot/mattermost",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Mattermost channel plugin", "description": "Clawdbot Mattermost channel plugin",
"clawdbot": { "clawdbot": {
@ -17,7 +17,7 @@
"order": 65 "order": 65
}, },
"install": { "install": {
"npmSpec": "@clawdbot/mattermost", "npmSpec": "@moltbot/mattermost",
"localPath": "extensions/mattermost", "localPath": "extensions/mattermost",
"defaultChoice": "npm" "defaultChoice": "npm"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/memory-core", "name": "@moltbot/memory-core",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot core memory search plugin", "description": "Clawdbot core memory search plugin",
"clawdbot": { "clawdbot": {
@ -9,6 +9,6 @@
] ]
}, },
"peerDependencies": { "peerDependencies": {
"clawdbot": ">=2026.1.24-3" "clawdbot": ">=2026.1.26"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/memory-lancedb", "name": "@moltbot/memory-lancedb",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot LanceDB-backed long-term memory plugin with auto-recall/capture", "description": "Clawdbot LanceDB-backed long-term memory plugin with auto-recall/capture",
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/msteams", "name": "@moltbot/msteams",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Microsoft Teams channel plugin", "description": "Clawdbot Microsoft Teams channel plugin",
"clawdbot": { "clawdbot": {
@ -20,7 +20,7 @@
"order": 60 "order": 60
}, },
"install": { "install": {
"npmSpec": "@clawdbot/msteams", "npmSpec": "@moltbot/msteams",
"localPath": "extensions/msteams", "localPath": "extensions/msteams",
"defaultChoice": "npm" "defaultChoice": "npm"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/nextcloud-talk", "name": "@moltbot/nextcloud-talk",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Nextcloud Talk channel plugin", "description": "Clawdbot Nextcloud Talk channel plugin",
"clawdbot": { "clawdbot": {
@ -22,7 +22,7 @@
"quickstartAllowFrom": true "quickstartAllowFrom": true
}, },
"install": { "install": {
"npmSpec": "@clawdbot/nextcloud-talk", "npmSpec": "@moltbot/nextcloud-talk",
"localPath": "extensions/nextcloud-talk", "localPath": "extensions/nextcloud-talk",
"defaultChoice": "npm" "defaultChoice": "npm"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/nostr", "name": "@moltbot/nostr",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Nostr channel plugin for NIP-04 encrypted DMs", "description": "Clawdbot Nostr channel plugin for NIP-04 encrypted DMs",
"clawdbot": { "clawdbot": {
@ -18,7 +18,7 @@
"quickstartAllowFrom": true "quickstartAllowFrom": true
}, },
"install": { "install": {
"npmSpec": "@clawdbot/nostr", "npmSpec": "@moltbot/nostr",
"localPath": "extensions/nostr", "localPath": "extensions/nostr",
"defaultChoice": "npm" "defaultChoice": "npm"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/open-prose", "name": "@moltbot/open-prose",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "OpenProse VM skill pack plugin (slash command + telemetry).", "description": "OpenProse VM skill pack plugin (slash command + telemetry).",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/signal", "name": "@moltbot/signal",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Signal channel plugin", "description": "Clawdbot Signal channel plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/slack", "name": "@moltbot/slack",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Slack channel plugin", "description": "Clawdbot Slack channel plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/telegram", "name": "@moltbot/telegram",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Telegram channel plugin", "description": "Clawdbot Telegram channel plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/tlon", "name": "@moltbot/tlon",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Tlon/Urbit channel plugin", "description": "Clawdbot Tlon/Urbit channel plugin",
"clawdbot": { "clawdbot": {
@ -18,7 +18,7 @@
"quickstartAllowFrom": true "quickstartAllowFrom": true
}, },
"install": { "install": {
"npmSpec": "@clawdbot/tlon", "npmSpec": "@moltbot/tlon",
"localPath": "extensions/tlon", "localPath": "extensions/tlon",
"defaultChoice": "npm" "defaultChoice": "npm"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/twitch", "name": "@moltbot/twitch",
"version": "2026.1.23", "version": "2026.1.26",
"description": "Clawdbot Twitch channel plugin", "description": "Clawdbot Twitch channel plugin",
"type": "module", "type": "module",
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
# Changelog # Changelog
## 2026.1.25 ## 2026.1.26
### Changes ### Changes
- Breaking: voice-call TTS now uses core `messages.tts` (plugin TTS config deepmerges with core). - Breaking: voice-call TTS now uses core `messages.tts` (plugin TTS config deepmerges with core).

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/voice-call", "name": "@moltbot/voice-call",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot voice-call plugin", "description": "Clawdbot voice-call plugin",
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/whatsapp", "name": "@moltbot/whatsapp",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot WhatsApp channel plugin", "description": "Clawdbot WhatsApp channel plugin",
"clawdbot": { "clawdbot": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/zalo", "name": "@moltbot/zalo",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Zalo channel plugin", "description": "Clawdbot Zalo channel plugin",
"clawdbot": { "clawdbot": {
@ -21,7 +21,7 @@
"quickstartAllowFrom": true "quickstartAllowFrom": true
}, },
"install": { "install": {
"npmSpec": "@clawdbot/zalo", "npmSpec": "@moltbot/zalo",
"localPath": "extensions/zalo", "localPath": "extensions/zalo",
"defaultChoice": "npm" "defaultChoice": "npm"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@clawdbot/zalouser", "name": "@moltbot/zalouser",
"version": "2026.1.25", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot Zalo Personal Account plugin via zca-cli", "description": "Clawdbot Zalo Personal Account plugin via zca-cli",
"dependencies": { "dependencies": {
@ -25,7 +25,7 @@
"quickstartAllowFrom": true "quickstartAllowFrom": true
}, },
"install": { "install": {
"npmSpec": "@clawdbot/zalouser", "npmSpec": "@moltbot/zalouser",
"localPath": "extensions/zalouser", "localPath": "extensions/zalouser",
"defaultChoice": "npm" "defaultChoice": "npm"
} }

View File

@ -1,15 +1,17 @@
{ {
"name": "clawdbot", "name": "moltbot",
"version": "2026.1.25", "version": "2026.1.26",
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent", "description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"exports": { "exports": {
".": "./dist/index.js", ".": "./dist/index.js",
"./plugin-sdk": "./dist/plugin-sdk/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": { "bin": {
"moltbot": "dist/entry.js",
"clawdbot": "dist/entry.js" "clawdbot": "dist/entry.js"
}, },
"files": [ "files": [
@ -90,6 +92,7 @@
"ui:build": "node scripts/ui.js build", "ui:build": "node scripts/ui.js build",
"start": "node scripts/run-node.mjs", "start": "node scripts/run-node.mjs",
"clawdbot": "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:watch": "node scripts/watch-node.mjs gateway --force",
"gateway:dev": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway", "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", "gateway:dev:reset": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset",

View File

@ -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:*"
}
}

View File

@ -2,7 +2,11 @@ import { listChannelDocks } from "../channels/dock.js";
import { getActivePluginRegistry } from "../plugins/runtime.js"; import { getActivePluginRegistry } from "../plugins/runtime.js";
import { listThinkingLevels } from "./thinking.js"; import { listThinkingLevels } from "./thinking.js";
import { COMMAND_ARG_FORMATTERS } from "./commands-args.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 = { type DefineChatCommandInput = {
key: string; key: string;
@ -16,6 +20,7 @@ type DefineChatCommandInput = {
textAlias?: string; textAlias?: string;
textAliases?: string[]; textAliases?: string[];
scope?: CommandScope; scope?: CommandScope;
category?: CommandCategory;
}; };
function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefinition { function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefinition {
@ -37,6 +42,7 @@ function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefiniti
argsMenu: command.argsMenu, argsMenu: command.argsMenu,
textAliases: aliases, textAliases: aliases,
scope, scope,
category: command.category,
}; };
} }
@ -48,6 +54,7 @@ function defineDockCommand(dock: ChannelDock): ChatCommandDefinition {
nativeName: `dock_${dock.id}`, nativeName: `dock_${dock.id}`,
description: `Switch to ${dock.id} for replies.`, description: `Switch to ${dock.id} for replies.`,
textAliases: [`/dock-${dock.id}`, `/dock_${dock.id}`], textAliases: [`/dock-${dock.id}`, `/dock_${dock.id}`],
category: "docks",
}); });
} }
@ -124,18 +131,21 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "help", nativeName: "help",
description: "Show available commands.", description: "Show available commands.",
textAlias: "/help", textAlias: "/help",
category: "status",
}), }),
defineChatCommand({ defineChatCommand({
key: "commands", key: "commands",
nativeName: "commands", nativeName: "commands",
description: "List all slash commands.", description: "List all slash commands.",
textAlias: "/commands", textAlias: "/commands",
category: "status",
}), }),
defineChatCommand({ defineChatCommand({
key: "skill", key: "skill",
nativeName: "skill", nativeName: "skill",
description: "Run a skill by name.", description: "Run a skill by name.",
textAlias: "/skill", textAlias: "/skill",
category: "tools",
args: [ args: [
{ {
name: "name", name: "name",
@ -156,6 +166,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "status", nativeName: "status",
description: "Show current status.", description: "Show current status.",
textAlias: "/status", textAlias: "/status",
category: "status",
}), }),
defineChatCommand({ defineChatCommand({
key: "allowlist", key: "allowlist",
@ -163,6 +174,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
textAlias: "/allowlist", textAlias: "/allowlist",
acceptsArgs: true, acceptsArgs: true,
scope: "text", scope: "text",
category: "management",
}), }),
defineChatCommand({ defineChatCommand({
key: "approve", key: "approve",
@ -170,6 +182,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
description: "Approve or deny exec requests.", description: "Approve or deny exec requests.",
textAlias: "/approve", textAlias: "/approve",
acceptsArgs: true, acceptsArgs: true,
category: "management",
}), }),
defineChatCommand({ defineChatCommand({
key: "context", key: "context",
@ -177,12 +190,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
description: "Explain how context is built and used.", description: "Explain how context is built and used.",
textAlias: "/context", textAlias: "/context",
acceptsArgs: true, acceptsArgs: true,
category: "status",
}), }),
defineChatCommand({ defineChatCommand({
key: "tts", key: "tts",
nativeName: "tts", nativeName: "tts",
description: "Control text-to-speech (TTS).", description: "Control text-to-speech (TTS).",
textAlias: "/tts", textAlias: "/tts",
category: "media",
args: [ args: [
{ {
name: "action", name: "action",
@ -225,12 +240,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "whoami", nativeName: "whoami",
description: "Show your sender id.", description: "Show your sender id.",
textAlias: "/whoami", textAlias: "/whoami",
category: "status",
}), }),
defineChatCommand({ defineChatCommand({
key: "subagents", key: "subagents",
nativeName: "subagents", nativeName: "subagents",
description: "List/stop/log/info subagent runs for this session.", description: "List/stop/log/info subagent runs for this session.",
textAlias: "/subagents", textAlias: "/subagents",
category: "management",
args: [ args: [
{ {
name: "action", name: "action",
@ -257,6 +274,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "config", nativeName: "config",
description: "Show or set config values.", description: "Show or set config values.",
textAlias: "/config", textAlias: "/config",
category: "management",
args: [ args: [
{ {
name: "action", name: "action",
@ -284,6 +302,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "debug", nativeName: "debug",
description: "Set runtime debug overrides.", description: "Set runtime debug overrides.",
textAlias: "/debug", textAlias: "/debug",
category: "management",
args: [ args: [
{ {
name: "action", name: "action",
@ -311,6 +330,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "usage", nativeName: "usage",
description: "Usage footer or cost summary.", description: "Usage footer or cost summary.",
textAlias: "/usage", textAlias: "/usage",
category: "options",
args: [ args: [
{ {
name: "mode", name: "mode",
@ -326,18 +346,21 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "stop", nativeName: "stop",
description: "Stop the current run.", description: "Stop the current run.",
textAlias: "/stop", textAlias: "/stop",
category: "session",
}), }),
defineChatCommand({ defineChatCommand({
key: "restart", key: "restart",
nativeName: "restart", nativeName: "restart",
description: "Restart Clawdbot.", description: "Restart Clawdbot.",
textAlias: "/restart", textAlias: "/restart",
category: "tools",
}), }),
defineChatCommand({ defineChatCommand({
key: "activation", key: "activation",
nativeName: "activation", nativeName: "activation",
description: "Set group activation mode.", description: "Set group activation mode.",
textAlias: "/activation", textAlias: "/activation",
category: "management",
args: [ args: [
{ {
name: "mode", name: "mode",
@ -353,6 +376,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "send", nativeName: "send",
description: "Set send policy.", description: "Set send policy.",
textAlias: "/send", textAlias: "/send",
category: "management",
args: [ args: [
{ {
name: "mode", name: "mode",
@ -369,6 +393,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
description: "Reset the current session.", description: "Reset the current session.",
textAlias: "/reset", textAlias: "/reset",
acceptsArgs: true, acceptsArgs: true,
category: "session",
}), }),
defineChatCommand({ defineChatCommand({
key: "new", key: "new",
@ -376,12 +401,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
description: "Start a new session.", description: "Start a new session.",
textAlias: "/new", textAlias: "/new",
acceptsArgs: true, acceptsArgs: true,
category: "session",
}), }),
defineChatCommand({ defineChatCommand({
key: "compact", key: "compact",
description: "Compact the session context.", description: "Compact the session context.",
textAlias: "/compact", textAlias: "/compact",
scope: "text", scope: "text",
category: "session",
args: [ args: [
{ {
name: "instructions", name: "instructions",
@ -396,6 +423,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "think", nativeName: "think",
description: "Set thinking level.", description: "Set thinking level.",
textAlias: "/think", textAlias: "/think",
category: "options",
args: [ args: [
{ {
name: "level", name: "level",
@ -411,6 +439,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "verbose", nativeName: "verbose",
description: "Toggle verbose mode.", description: "Toggle verbose mode.",
textAlias: "/verbose", textAlias: "/verbose",
category: "options",
args: [ args: [
{ {
name: "mode", name: "mode",
@ -426,6 +455,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "reasoning", nativeName: "reasoning",
description: "Toggle reasoning visibility.", description: "Toggle reasoning visibility.",
textAlias: "/reasoning", textAlias: "/reasoning",
category: "options",
args: [ args: [
{ {
name: "mode", name: "mode",
@ -441,6 +471,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "elevated", nativeName: "elevated",
description: "Toggle elevated mode.", description: "Toggle elevated mode.",
textAlias: "/elevated", textAlias: "/elevated",
category: "options",
args: [ args: [
{ {
name: "mode", name: "mode",
@ -456,6 +487,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "exec", nativeName: "exec",
description: "Set exec defaults for this session.", description: "Set exec defaults for this session.",
textAlias: "/exec", textAlias: "/exec",
category: "options",
args: [ args: [
{ {
name: "options", name: "options",
@ -470,6 +502,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "model", nativeName: "model",
description: "Show or set the model.", description: "Show or set the model.",
textAlias: "/model", textAlias: "/model",
category: "options",
args: [ args: [
{ {
name: "model", name: "model",
@ -485,12 +518,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
textAlias: "/models", textAlias: "/models",
argsParsing: "none", argsParsing: "none",
acceptsArgs: true, acceptsArgs: true,
category: "options",
}), }),
defineChatCommand({ defineChatCommand({
key: "queue", key: "queue",
nativeName: "queue", nativeName: "queue",
description: "Adjust queue settings.", description: "Adjust queue settings.",
textAlias: "/queue", textAlias: "/queue",
category: "options",
args: [ args: [
{ {
name: "mode", name: "mode",
@ -523,6 +558,7 @@ function buildChatCommands(): ChatCommandDefinition[] {
description: "Run host shell commands (host-only).", description: "Run host shell commands (host-only).",
textAlias: "/bash", textAlias: "/bash",
scope: "text", scope: "text",
category: "tools",
args: [ args: [
{ {
name: "command", name: "command",

View File

@ -2,6 +2,15 @@ import type { ClawdbotConfig } from "../config/types.js";
export type CommandScope = "text" | "native" | "both"; export type CommandScope = "text" | "native" | "both";
export type CommandCategory =
| "session"
| "options"
| "status"
| "management"
| "media"
| "tools"
| "docks";
export type CommandArgType = "string" | "number" | "boolean"; export type CommandArgType = "string" | "number" | "boolean";
export type CommandArgChoiceContext = { export type CommandArgChoiceContext = {
@ -51,6 +60,7 @@ export type ChatCommandDefinition = {
formatArgs?: (values: CommandArgValues) => string | undefined; formatArgs?: (values: CommandArgValues) => string | undefined;
argsMenu?: CommandArgMenuSpec | "auto"; argsMenu?: CommandArgMenuSpec | "auto";
scope: CommandScope; scope: CommandScope;
category?: CommandCategory;
}; };
export type NativeCommandSpec = { export type NativeCommandSpec = {

View File

@ -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" },
]);
});
});

View File

@ -1,6 +1,10 @@
import { logVerbose } from "../../globals.js"; import { logVerbose } from "../../globals.js";
import { listSkillCommandsForWorkspace } from "../skill-commands.js"; import { listSkillCommandsForAgents } from "../skill-commands.js";
import { buildCommandsMessage, buildHelpMessage } from "../status.js"; import {
buildCommandsMessage,
buildCommandsMessagePaginated,
buildHelpMessage,
} from "../status.js";
import { buildStatusReply } from "./commands-status.js"; import { buildStatusReply } from "./commands-status.js";
import { buildContextReply } from "./commands-context-report.js"; import { buildContextReply } from "./commands-context-report.js";
import type { CommandHandler } from "./commands-types.js"; import type { CommandHandler } from "./commands-types.js";
@ -31,16 +35,78 @@ export const handleCommandsListCommand: CommandHandler = async (params, allowTex
} }
const skillCommands = const skillCommands =
params.skillCommands ?? params.skillCommands ??
listSkillCommandsForWorkspace({ listSkillCommandsForAgents({
workspaceDir: params.workspaceDir,
cfg: params.cfg, 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 { return {
shouldContinue: false, 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<Array<{ text: string; callback_data: string }>> {
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) => { export const handleStatusCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null; if (!allowTextCommands) return null;
const statusRequested = const statusRequested =

View File

@ -4,7 +4,20 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { normalizeTestText } from "../../test/helpers/normalize-text.js"; import { normalizeTestText } from "../../test/helpers/normalize-text.js";
import { withTempHome } from "../../test/helpers/temp-home.js"; import { withTempHome } from "../../test/helpers/temp-home.js";
import type { ClawdbotConfig } from "../config/config.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(() => { afterEach(() => {
vi.restoreAllMocks(); vi.restoreAllMocks();
@ -400,10 +413,12 @@ describe("buildCommandsMessage", () => {
const text = buildCommandsMessage({ const text = buildCommandsMessage({
commands: { config: false, debug: false }, commands: { config: false, debug: false },
} as ClawdbotConfig); } as ClawdbotConfig);
expect(text).toContain(" Slash commands");
expect(text).toContain("Status");
expect(text).toContain("/commands - List all slash commands."); expect(text).toContain("/commands - List all slash commands.");
expect(text).toContain("/skill - Run a skill by name."); expect(text).toContain("/skill - Run a skill by name.");
expect(text).toContain("/think (aliases: /thinking, /t) - Set thinking level."); expect(text).toContain("/think (/thinking, /t) - Set thinking level.");
expect(text).toContain("/compact (text-only) - Compact the session context."); expect(text).toContain("/compact [text] - Compact the session context.");
expect(text).not.toContain("/config"); expect(text).not.toContain("/config");
expect(text).not.toContain("/debug"); expect(text).not.toContain("/debug");
}); });
@ -430,8 +445,39 @@ describe("buildHelpMessage", () => {
const text = buildHelpMessage({ const text = buildHelpMessage({
commands: { config: false, debug: false }, commands: { config: false, debug: false },
} as ClawdbotConfig); } as ClawdbotConfig);
expect(text).toContain("Skills: /skill <name> [input]"); expect(text).toContain("Skills");
expect(text).toContain("/skill <name> [input]");
expect(text).not.toContain("/config"); expect(text).not.toContain("/config");
expect(text).not.toContain("/debug"); 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");
});
});

View File

@ -29,9 +29,14 @@ import {
resolveModelCostConfig, resolveModelCostConfig,
} from "../utils/usage-format.js"; } from "../utils/usage-format.js";
import { VERSION } from "../version.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 { listPluginCommands } from "../plugins/commands.js";
import type { SkillCommandSpec } from "../agents/skills.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 { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js";
import type { MediaUnderstandingDecision } from "../media-understanding/types.js"; import type { MediaUnderstandingDecision } from "../media-understanding/types.js";
@ -427,61 +432,203 @@ export function buildStatusMessage(args: StatusArgs): string {
.join("\n"); .join("\n");
} }
const CATEGORY_LABELS: Record<CommandCategory, string> = {
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<CommandCategory, ChatCommandDefinition[]> {
const grouped = new Map<CommandCategory, ChatCommandDefinition[]>();
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 { export function buildHelpMessage(cfg?: ClawdbotConfig): string {
const options = [ const lines = [" Help", ""];
"/think <level>",
"/verbose on|full|off", lines.push("Session");
"/reasoning on|off", lines.push(" /new | /reset | /compact [instructions] | /stop");
"/elevated on|off|ask|full", lines.push("");
"/model <id>",
"/usage off|tokens|full", const optionParts = ["/think <level>", "/model <id>", "/verbose on|off"];
]; if (cfg?.commands?.config === true) optionParts.push("/config");
if (cfg?.commands?.config === true) options.push("/config show"); if (cfg?.commands?.debug === true) optionParts.push("/debug");
if (cfg?.commands?.debug === true) options.push("/debug show"); lines.push("Options");
return [ lines.push(` ${optionParts.join(" | ")}`);
" Help", lines.push("");
"Shortcuts: /new reset | /compact [instructions] | /restart relink (if enabled)",
`Options: ${options.join(" | ")}`, lines.push("Status");
"Skills: /skill <name> [input]", lines.push(" /status | /whoami | /context");
"More: /commands for all slash commands", lines.push("");
].join("\n");
lines.push("Skills");
lines.push(" /skill <name> [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<string>();
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<typeof listPluginCommands>,
): 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( export function buildCommandsMessage(
cfg?: ClawdbotConfig, cfg?: ClawdbotConfig,
skillCommands?: SkillCommandSpec[], skillCommands?: SkillCommandSpec[],
options?: CommandsMessageOptions,
): string { ): 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 const commands = cfg
? listChatCommandsForConfig(cfg, { skillCommands }) ? listChatCommandsForConfig(cfg, { skillCommands })
: listChatCommands({ skillCommands }); : listChatCommands({ skillCommands });
for (const command of commands) {
const primary = command.nativeName
? `/${command.nativeName}`
: command.textAliases[0]?.trim() || `/${command.key}`;
const seen = new Set<string>();
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(); const pluginCommands = listPluginCommands();
if (pluginCommands.length > 0) { const items = buildCommandItems(commands, pluginCommands);
lines.push("");
lines.push("Plugin commands:"); if (!isTelegram) {
for (const command of pluginCommands) { const lines = [" Slash commands", ""];
const pluginLabel = command.pluginId ? ` (plugin: ${command.pluginId})` : ""; lines.push(formatCommandList(items));
lines.push(`/${command.name}${pluginLabel} - ${command.description}`); 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,
};
} }

View File

@ -4,6 +4,10 @@ import {
createInboundDebouncer, createInboundDebouncer,
resolveInboundDebounceMs, resolveInboundDebounceMs,
} from "../auto-reply/inbound-debounce.js"; } 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 { loadConfig } from "../config/config.js";
import { writeConfigFile } from "../config/io.js"; import { writeConfigFile } from "../config/io.js";
import { danger, logVerbose, warn } from "../globals.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 { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
import { readTelegramAllowFromStore } from "./pairing-store.js"; import { readTelegramAllowFromStore } from "./pairing-store.js";
import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js"; import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js";
import { buildInlineKeyboard } from "./send.js";
export const registerTelegramHandlers = ({ export const registerTelegramHandlers = ({
cfg, 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 = { const syntheticMessage: TelegramMessage = {
...callbackMessage, ...callbackMessage,
from: callback.from, from: callback.from,

View File

@ -1,6 +1,12 @@
import type { Bot } from "grammy"; import type { Bot } from "grammy";
import { resolveAckReaction } from "../agents/identity.js"; 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 { hasControlCommand } from "../auto-reply/command-detection.js";
import { normalizeCommandBody } from "../auto-reply/commands-registry.js"; import { normalizeCommandBody } from "../auto-reply/commands-registry.js";
import { formatInboundEnvelope, resolveEnvelopeFormatOptions } from "../auto-reply/envelope.js"; import { formatInboundEnvelope, resolveEnvelopeFormatOptions } from "../auto-reply/envelope.js";
@ -104,6 +110,24 @@ type BuildTelegramMessageContextParams = {
resolveTelegramGroupConfig: ResolveTelegramGroupConfig; resolveTelegramGroupConfig: ResolveTelegramGroupConfig;
}; };
async function resolveStickerVisionSupport(params: {
cfg: ClawdbotConfig;
agentId?: string;
}): Promise<boolean> {
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 ({ export const buildTelegramMessageContext = async ({
primaryCtx, primaryCtx,
allMedia, 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 // Check if sticker has a cached description - if so, use it instead of sending the image
const cachedStickerDescription = allMedia[0]?.stickerMetadata?.cachedDescription; 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) { if (stickerCacheHit) {
// Format cached description with sticker context // Format cached description with sticker context
const emoji = allMedia[0]?.stickerMetadata?.emoji; const emoji = allMedia[0]?.stickerMetadata?.emoji;

View File

@ -1,5 +1,11 @@
// @ts-nocheck // @ts-nocheck
import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js"; 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 { resolveChunkMode } from "../auto-reply/chunk.js";
import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js"; import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.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 { cacheSticker, describeStickerImage } from "./sticker-cache.js";
import { resolveAgentDir } from "../agents/agent-scope.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 ({ export const dispatchTelegramMessage = async ({
context, context,
bot, bot,
@ -133,14 +151,18 @@ export const dispatchTelegramMessage = async ({
// Handle uncached stickers: get a dedicated vision description before dispatch // Handle uncached stickers: get a dedicated vision description before dispatch
// This ensures we cache a raw description rather than a conversational response // This ensures we cache a raw description rather than a conversational response
const sticker = ctxPayload.Sticker; const sticker = ctxPayload.Sticker;
if (sticker?.fileUniqueId && !sticker.cachedDescription && ctxPayload.MediaPath) { if (sticker?.fileUniqueId && ctxPayload.MediaPath) {
const agentDir = resolveAgentDir(cfg, route.agentId); const agentDir = resolveAgentDir(cfg, route.agentId);
const description = await describeStickerImage({ const stickerSupportsVision = await resolveStickerVisionSupport(cfg, route.agentId);
imagePath: ctxPayload.MediaPath, let description = sticker.cachedDescription ?? null;
cfg, if (!description) {
agentDir, description = await describeStickerImage({
agentId: route.agentId, imagePath: ctxPayload.MediaPath,
}); cfg,
agentDir,
agentId: route.agentId,
});
}
if (description) { if (description) {
// Format the description with sticker context // Format the description with sticker context
const stickerContext = [sticker.emoji, sticker.setName ? `from "${sticker.setName}"` : null] const stickerContext = [sticker.emoji, sticker.setName ? `from "${sticker.setName}"` : null]
@ -148,17 +170,19 @@ export const dispatchTelegramMessage = async ({
.join(" "); .join(" ");
const formattedDesc = `[Sticker${stickerContext ? ` ${stickerContext}` : ""}] ${description}`; const formattedDesc = `[Sticker${stickerContext ? ` ${stickerContext}` : ""}] ${description}`;
// Update context to use description instead of image
sticker.cachedDescription = description; sticker.cachedDescription = description;
ctxPayload.Body = formattedDesc; if (!stickerSupportsVision) {
ctxPayload.BodyForAgent = formattedDesc; // Update context to use description instead of image
// Clear media paths so native vision doesn't process the image again ctxPayload.Body = formattedDesc;
ctxPayload.MediaPath = undefined; ctxPayload.BodyForAgent = formattedDesc;
ctxPayload.MediaType = undefined; // Clear media paths so native vision doesn't process the image again
ctxPayload.MediaUrl = undefined; ctxPayload.MediaPath = undefined;
ctxPayload.MediaPaths = undefined; ctxPayload.MediaType = undefined;
ctxPayload.MediaUrls = undefined; ctxPayload.MediaUrl = undefined;
ctxPayload.MediaTypes = undefined; ctxPayload.MediaPaths = undefined;
ctxPayload.MediaUrls = undefined;
ctxPayload.MediaTypes = undefined;
}
// Cache the description for future encounters // Cache the description for future encounters
cacheSticker({ cacheSticker({

View File

@ -93,6 +93,7 @@ const commandSpy = vi.fn();
const botCtorSpy = vi.fn(); const botCtorSpy = vi.fn();
const answerCallbackQuerySpy = vi.fn(async () => undefined); const answerCallbackQuerySpy = vi.fn(async () => undefined);
const sendChatActionSpy = vi.fn(); const sendChatActionSpy = vi.fn();
const editMessageTextSpy = vi.fn(async () => ({ message_id: 88 }));
const setMessageReactionSpy = vi.fn(async () => undefined); const setMessageReactionSpy = vi.fn(async () => undefined);
const setMyCommandsSpy = vi.fn(async () => undefined); const setMyCommandsSpy = vi.fn(async () => undefined);
const sendMessageSpy = vi.fn(async () => ({ message_id: 77 })); const sendMessageSpy = vi.fn(async () => ({ message_id: 77 }));
@ -102,6 +103,7 @@ type ApiStub = {
config: { use: (arg: unknown) => void }; config: { use: (arg: unknown) => void };
answerCallbackQuery: typeof answerCallbackQuerySpy; answerCallbackQuery: typeof answerCallbackQuerySpy;
sendChatAction: typeof sendChatActionSpy; sendChatAction: typeof sendChatActionSpy;
editMessageText: typeof editMessageTextSpy;
setMessageReaction: typeof setMessageReactionSpy; setMessageReaction: typeof setMessageReactionSpy;
setMyCommands: typeof setMyCommandsSpy; setMyCommands: typeof setMyCommandsSpy;
sendMessage: typeof sendMessageSpy; sendMessage: typeof sendMessageSpy;
@ -112,6 +114,7 @@ const apiStub: ApiStub = {
config: { use: useSpy }, config: { use: useSpy },
answerCallbackQuery: answerCallbackQuerySpy, answerCallbackQuery: answerCallbackQuerySpy,
sendChatAction: sendChatActionSpy, sendChatAction: sendChatActionSpy,
editMessageText: editMessageTextSpy,
setMessageReaction: setMessageReactionSpy, setMessageReaction: setMessageReactionSpy,
setMyCommands: setMyCommandsSpy, setMyCommands: setMyCommandsSpy,
sendMessage: sendMessageSpy, sendMessage: sendMessageSpy,
@ -192,6 +195,7 @@ describe("createTelegramBot", () => {
sendPhotoSpy.mockReset(); sendPhotoSpy.mockReset();
setMessageReactionSpy.mockReset(); setMessageReactionSpy.mockReset();
answerCallbackQuerySpy.mockReset(); answerCallbackQuerySpy.mockReset();
editMessageTextSpy.mockReset();
setMyCommandsSpy.mockReset(); setMyCommandsSpy.mockReset();
wasSentByBot.mockReset(); wasSentByBot.mockReset();
middlewareUseSpy.mockReset(); middlewareUseSpy.mockReset();
@ -424,6 +428,87 @@ describe("createTelegramBot", () => {
expect(answerCallbackQuerySpy).toHaveBeenCalledWith("cbq-2"); 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<string, unknown>,
) => Promise<void>;
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<string, unknown>,
) => Promise<void>;
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 () => { it("wraps inbound message with Telegram envelope", async () => {
const originalTz = process.env.TZ; const originalTz = process.env.TZ;
process.env.TZ = "Europe/Vienna"; process.env.TZ = "Europe/Vienna";

View File

@ -4,11 +4,13 @@ import type { ClawdbotConfig } from "../config/config.js";
import { STATE_DIR_CLAWDBOT } from "../config/paths.js"; import { STATE_DIR_CLAWDBOT } from "../config/paths.js";
import { loadJsonFile, saveJsonFile } from "../infra/json-file.js"; import { loadJsonFile, saveJsonFile } from "../infra/json-file.js";
import { logVerbose } from "../globals.js"; import { logVerbose } from "../globals.js";
import type { ModelCatalogEntry } from "../agents/model-catalog.js";
import { import {
findModelInCatalog, findModelInCatalog,
loadModelCatalog, loadModelCatalog,
modelSupportsVision, modelSupportsVision,
} from "../agents/model-catalog.js"; } from "../agents/model-catalog.js";
import { resolveApiKeyForProvider } from "../agents/model-auth.js";
import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
import { resolveAutoImageModel } from "../media-understanding/runner.js"; import { resolveAutoImageModel } from "../media-understanding/runner.js";
@ -140,6 +142,7 @@ export function getCacheStats(): { count: number; oldestAt?: string; newestAt?:
const STICKER_DESCRIPTION_PROMPT = 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."; "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 { export interface DescribeStickerParams {
imagePath: string; imagePath: string;
@ -158,31 +161,79 @@ export async function describeStickerImage(params: DescribeStickerParams): Promi
const defaultModel = resolveDefaultModelForAgent({ cfg, agentId }); const defaultModel = resolveDefaultModelForAgent({ cfg, agentId });
let activeModel = undefined as { provider: string; model: string } | undefined; let activeModel = undefined as { provider: string; model: string } | undefined;
let catalog: ModelCatalogEntry[] = [];
try { try {
const catalog = await loadModelCatalog({ config: cfg }); catalog = await loadModelCatalog({ config: cfg });
const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model); const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model);
if (modelSupportsVision(entry)) { const supportsVision = modelSupportsVision(entry);
if (supportsVision) {
activeModel = { provider: defaultModel.provider, model: defaultModel.model }; activeModel = { provider: defaultModel.provider, model: defaultModel.model };
} }
} catch { } catch {
// Ignore catalog failures; fall back to auto selection. // Ignore catalog failures; fall back to auto selection.
} }
const resolved = await resolveAutoImageModel({ const hasProviderKey = async (provider: string) => {
cfg, try {
agentDir, await resolveApiKeyForProvider({ provider, cfg, agentDir });
activeModel, 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) { 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"); logVerbose("telegram: no vision provider available for sticker description");
return null; return null;
} }
const { provider, model } = resolved; const { provider, model } = resolved;
if (!model) {
logVerbose(`telegram: no vision model available for ${provider}`);
return null;
}
logVerbose(`telegram: describing sticker with ${provider}/${model}`); logVerbose(`telegram: describing sticker with ${provider}/${model}`);
try { try {