Merge branch 'main' into serrrfirat/near-ai-provider
This commit is contained in:
commit
cb641962f2
@ -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.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -19,9 +19,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.1.25</string>
|
||||
<string>2026.1.26</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>20260125</string>
|
||||
<string>20260126</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||
|
||||
@ -17,8 +17,8 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.1.25</string>
|
||||
<string>2026.1.26</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>20260125</string>
|
||||
<string>20260126</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.1.25</string>
|
||||
<string>2026.1.26</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>202601250</string>
|
||||
<string>202601260</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Clawdbot</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@ -185,7 +185,7 @@ cat > /data/clawdbot.json << 'EOF'
|
||||
"bind": "auto"
|
||||
},
|
||||
"meta": {
|
||||
"lastTouchedVersion": "2026.1.25"
|
||||
"lastTouchedVersion": "2026.1.26"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
@ -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: <Developer Name> (<TEAMID>)" \
|
||||
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 "<apple-id>" --team-id "<team-id>" --password "<app-specific-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: <Developer Name> (<TEAMID>)" \
|
||||
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.
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
16
packages/clawdbot/package.json
Normal file
16
packages/clawdbot/package.json
Normal 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:*"
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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 = {
|
||||
|
||||
13
src/auto-reply/reply/commands-info.test.ts
Normal file
13
src/auto-reply/reply/commands-info.test.ts
Normal 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" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -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<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) => {
|
||||
if (!allowTextCommands) return null;
|
||||
const statusRequested =
|
||||
|
||||
@ -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 <name> [input]");
|
||||
expect(text).toContain("Skills");
|
||||
expect(text).toContain("/skill <name> [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");
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<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 {
|
||||
const options = [
|
||||
"/think <level>",
|
||||
"/verbose on|full|off",
|
||||
"/reasoning on|off",
|
||||
"/elevated on|off|ask|full",
|
||||
"/model <id>",
|
||||
"/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 <name> [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 <level>", "/model <id>", "/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 <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(
|
||||
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<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();
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<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 ({
|
||||
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;
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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<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 () => {
|
||||
const originalTz = process.env.TZ;
|
||||
process.env.TZ = "Europe/Vienna";
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user