Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ac064139d | ||
|
|
2045812a65 | ||
|
|
fe3dd0c5e3 | ||
|
|
1e7737b732 | ||
|
|
41929129ce | ||
|
|
6d79c6cd26 | ||
|
|
bcedeb4e1f | ||
|
|
f076eba98a | ||
|
|
f3bd6bf342 | ||
|
|
c29c9a1e3e | ||
|
|
7a384ea07c | ||
|
|
5fc866e8fe | ||
|
|
437535ee94 | ||
|
|
3b929ff843 | ||
|
|
93737ee152 | ||
|
|
765626b492 | ||
|
|
42b8fce4e5 | ||
|
|
c27294133e | ||
|
|
94095386b3 | ||
|
|
7a524e8667 | ||
|
|
876bbb742a | ||
|
|
ef7971e3a4 | ||
|
|
9d742ba51f | ||
|
|
386d21b6d1 | ||
|
|
c8afa8207c | ||
|
|
d0e21f05a6 | ||
|
|
11f039ef85 | ||
|
|
e90e3ba954 | ||
|
|
0de7852d46 | ||
|
|
834663dfef | ||
|
|
a72d7a9f36 | ||
|
|
67e57e7c99 | ||
|
|
4c98d6c121 | ||
|
|
174a1cb68a | ||
|
|
a4d56bd06e |
No files matched your search
@@ -0,0 +1,139 @@
|
|||||||
|
name: Docker Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Build amd64 image
|
||||||
|
build-amd64:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
outputs:
|
||||||
|
image-digest: ${{ steps.build.outputs.digest }}
|
||||||
|
image-metadata: ${{ steps.meta.outputs.json }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}},suffix=-amd64
|
||||||
|
type=ref,event=branch,suffix=-amd64
|
||||||
|
|
||||||
|
- name: Build and push amd64 image
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
provenance: false
|
||||||
|
push: true
|
||||||
|
|
||||||
|
# Build arm64 image
|
||||||
|
build-arm64:
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
outputs:
|
||||||
|
image-digest: ${{ steps.build.outputs.digest }}
|
||||||
|
image-metadata: ${{ steps.meta.outputs.json }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}},suffix=-arm64
|
||||||
|
type=ref,event=branch,suffix=-arm64
|
||||||
|
|
||||||
|
- name: Build and push arm64 image
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/arm64
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
provenance: false
|
||||||
|
push: true
|
||||||
|
|
||||||
|
# Create multi-platform manifest
|
||||||
|
create-manifest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
needs: [build-amd64, build-arm64]
|
||||||
|
steps:
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for manifest
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=raw,value=latest,enable={{is_tag}}
|
||||||
|
|
||||||
|
- name: Create and push manifest
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-amd64.outputs.image-digest }} \
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-arm64.outputs.image-digest }}
|
||||||
|
env:
|
||||||
|
DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }}
|
||||||
@@ -106,6 +106,7 @@
|
|||||||
## Agent-Specific Notes
|
## Agent-Specific Notes
|
||||||
- Vocabulary: "makeup" = "mac app".
|
- Vocabulary: "makeup" = "mac app".
|
||||||
- Never edit `node_modules` (global/Homebrew/npm/git installs too). Updates overwrite. Skill notes go in `tools.md` or `AGENTS.md`.
|
- Never edit `node_modules` (global/Homebrew/npm/git installs too). Updates overwrite. Skill notes go in `tools.md` or `AGENTS.md`.
|
||||||
|
- Signal: "update fly" => `fly ssh console -a flawd-bot -C "bash -lc 'cd /data/clawd/clawdbot && git pull --rebase origin main'"` then `fly machines restart e825232f34d058 -a flawd-bot`.
|
||||||
- When working on a GitHub Issue or PR, print the full URL at the end of the task.
|
- When working on a GitHub Issue or PR, print the full URL at the end of the task.
|
||||||
- When answering questions, respond with high-confidence answers only: verify in code; do not guess.
|
- When answering questions, respond with high-confidence answers only: verify in code; do not guess.
|
||||||
- Never update the Carbon dependency.
|
- Never update the Carbon dependency.
|
||||||
|
|||||||
+64
-64
@@ -2,81 +2,81 @@
|
|||||||
|
|
||||||
Docs: https://docs.clawd.bot
|
Docs: https://docs.clawd.bot
|
||||||
|
|
||||||
## 2026.1.23 (Unreleased)
|
## 2026.1.24
|
||||||
|
|
||||||
### Highlights
|
|
||||||
- TTS: allow model-driven TTS tags by default for expressive audio replies (laughter, singing cues, etc.).
|
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Gateway: add /tools/invoke HTTP endpoint for direct tool calls and document it. (#1575) Thanks @vignesh07.
|
- Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround).
|
||||||
- Agents: keep system prompt time zone-only and move current time to `session_status` for better cache hits.
|
- Docs: add verbose installer troubleshooting guidance.
|
||||||
- Agents: remove redundant bash tool alias from tool registration/display. (#1571) Thanks @Takhoffman.
|
- Docs: update Fly.io guide notes.
|
||||||
- Browser: add node-host proxy auto-routing for remote gateways (configurable per gateway/node).
|
|
||||||
- Heartbeat: add per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer.
|
### Fixes
|
||||||
- Plugins: add optional llm-task JSON-only tool for workflows. (#1498) Thanks @vignesh07.
|
- Web UI: hide internal `message_id` hints in chat bubbles.
|
||||||
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it.
|
- Heartbeat: normalize target identifiers for consistent routing.
|
||||||
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
|
|
||||||
- CLI: add `clawdbot system` for system events + heartbeat controls; remove standalone `wake`.
|
## 2026.1.23-1
|
||||||
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3.
|
|
||||||
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
|
### Fixes
|
||||||
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc.
|
- Packaging: include dist/tts output in npm tarball (fixes missing dist/tts/tts.js).
|
||||||
|
- CI: publish multi-arch Docker images on main and release tags. (#1602) Thanks @denysvitali.
|
||||||
|
|
||||||
|
## 2026.1.23
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
- TTS: move Telegram TTS into core + enable model-driven TTS tags by default for expressive audio replies. (#1559) Thanks @Glucksberg. https://docs.clawd.bot/tts
|
||||||
|
- Gateway: add `/tools/invoke` HTTP endpoint for direct tool calls (auth + tool policy enforced). (#1575) Thanks @vignesh07. https://docs.clawd.bot/gateway/tools-invoke-http-api
|
||||||
|
- Heartbeat: per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer. https://docs.clawd.bot/gateway/heartbeat
|
||||||
|
- Deploy: add Fly.io deployment support + guide. (#1570) https://docs.clawd.bot/platforms/fly
|
||||||
|
- Channels: add Tlon/Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a. https://docs.clawd.bot/channels/tlon
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt. https://docs.clawd.bot/multi-agent-sandbox-tools
|
||||||
|
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3. https://docs.clawd.bot/bedrock
|
||||||
|
- CLI: add `clawdbot system` for system events + heartbeat controls; remove standalone `wake`. (commit 71203829d) https://docs.clawd.bot/cli/system
|
||||||
|
- CLI: add live auth probes to `clawdbot models status` for per-profile verification. (commit 40181afde) https://docs.clawd.bot/cli/models
|
||||||
|
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it. (commit 2c85b1b40)
|
||||||
|
- Browser: add node-host proxy auto-routing for remote gateways (configurable per gateway/node). (commit c3cb26f7c)
|
||||||
|
- Plugins: add optional `llm-task` JSON-only tool for workflows. (#1498) Thanks @vignesh07. https://docs.clawd.bot/tools/llm-task
|
||||||
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
||||||
- Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
|
- Agents: keep system prompt time zone-only and move current time to `session_status` for better cache hits. (commit 66eec295b)
|
||||||
- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt.
|
- Agents: remove redundant bash tool alias from tool registration/display. (#1571) Thanks @Takhoffman.
|
||||||
- TTS: move Telegram TTS into core with auto-replies, commands, and gateway methods. (#1559) Thanks @Glucksberg.
|
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc. https://docs.clawd.bot/automation/cron-vs-heartbeat
|
||||||
|
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc. https://docs.clawd.bot/gateway/heartbeat
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
|
- Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
|
||||||
- Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete.
|
- Heartbeat: accept plugin channel ids for heartbeat target validation + UI hints.
|
||||||
- Messaging: mirror outbound sends into target session keys (threads + dmScope) and create session entries on send. (#1520)
|
- Messaging/Sessions: mirror outbound sends into target session keys (threads + dmScope), create session entries on send, and normalize session key casing. (#1520, commit 4b6cdd1d3)
|
||||||
- Sessions: normalize session key casing to lowercase for consistent routing.
|
|
||||||
- BlueBubbles: normalize group session keys for outbound mirroring. (#1520)
|
|
||||||
- Slack: match auto-thread mirroring channel ids case-insensitively. (#1520)
|
|
||||||
- Gateway: lowercase provided session keys when mirroring outbound sends. (#1520)
|
|
||||||
- Skills: gate bird Homebrew install to macOS. (#1569) Thanks @bradleypriest.
|
|
||||||
- Slack: honor open groupPolicy for unlisted channels in message + slash gating. (#1563) Thanks @itsjaydesu.
|
|
||||||
- Agents: show tool error fallback when the last assistant turn only invoked tools (prevents silent stops).
|
|
||||||
- Agents: ignore IDENTITY.md template placeholders when parsing identity to avoid placeholder replies. (#1556)
|
|
||||||
- Agents: drop orphaned OpenAI Responses reasoning blocks on model switches. (#1562) Thanks @roshanasingh4.
|
|
||||||
- Docker: update gateway command in docker-compose and Hetzner guide. (#1514)
|
|
||||||
- Sessions: reject array-backed session stores to prevent silent wipes. (#1469)
|
- Sessions: reject array-backed session stores to prevent silent wipes. (#1469)
|
||||||
- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
|
- Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete.
|
||||||
- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
|
|
||||||
- UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank.
|
|
||||||
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
|
|
||||||
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
|
||||||
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
|
||||||
- Discord: retry rate-limited allowlist resolution + command deploy to avoid gateway crashes.
|
|
||||||
- Mentions: ignore mentionPattern matches when another explicit mention is present in group chats (Slack/Discord/Telegram/WhatsApp).
|
|
||||||
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
||||||
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
|
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
|
||||||
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts). (commit 5662a9cdf)
|
||||||
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
- Daemon: use platform PATH delimiters when building minimal service paths. (commit a4e57d3ac)
|
||||||
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
|
||||||
- CLI: suppress diagnostic session/run noise during auth probes.
|
|
||||||
- CLI: hide auth probe timeout warnings from embedded runs.
|
|
||||||
- CLI: render auth probe results as a table in `clawdbot models status`.
|
|
||||||
- CLI: suppress probe-only embedded logs unless `--verbose` is set.
|
|
||||||
- CLI: move auth probe errors below the table to reduce wrapping.
|
|
||||||
- CLI: prevent ANSI color bleed when table cells wrap.
|
|
||||||
- CLI: explain when auth profiles are excluded by auth.order in probe details.
|
|
||||||
- CLI: drop the em dash when the banner tagline wraps to a second line.
|
|
||||||
- CLI: inline auth probe errors in status rows to reduce wrapping.
|
|
||||||
- Telegram: render markdown in media captions. (#1478)
|
|
||||||
- Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests.
|
|
||||||
- Agents: trigger model fallback when auth profiles are all in cooldown or unavailable. (#1522)
|
|
||||||
- Daemon: use platform PATH delimiters when building minimal service paths.
|
|
||||||
- Tests: skip embedded runner ordering assertion on Windows to avoid CI timeouts.
|
|
||||||
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
|
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
|
||||||
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
|
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
|
||||||
|
- Docker: update gateway command in docker-compose and Hetzner guide. (#1514)
|
||||||
|
- Agents: show tool error fallback when the last assistant turn only invoked tools (prevents silent stops). (commit 8ea8801d0)
|
||||||
|
- Agents: ignore IDENTITY.md template placeholders when parsing identity. (#1556)
|
||||||
|
- Agents: drop orphaned OpenAI Responses reasoning blocks on model switches. (#1562) Thanks @roshanasingh4.
|
||||||
|
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
||||||
|
- Agents: warn and ignore tool allowlists that only reference unknown or unloaded plugin tools. (#1566)
|
||||||
|
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
|
||||||
|
- Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests. (commit 084002998)
|
||||||
|
- Slack: honor open groupPolicy for unlisted channels in message + slash gating. (#1563) Thanks @itsjaydesu.
|
||||||
|
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
||||||
|
- Discord: retry rate-limited allowlist resolution + command deploy to avoid gateway crashes. (commit f70ac0c7c)
|
||||||
|
- Mentions: ignore mentionPattern matches when another explicit mention is present in group chats (Slack/Discord/Telegram/WhatsApp). (commit d905ca0e0)
|
||||||
|
- Telegram: render markdown in media captions. (#1478)
|
||||||
|
- MS Teams: remove `.default` suffix from Graph scopes and Bot Framework probe scopes. (#1507, #1574) Thanks @Evizero.
|
||||||
|
- Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160)
|
||||||
|
- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS. (commit 69f645c66)
|
||||||
|
- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
|
||||||
|
- UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank. (commit d57cb2e1a)
|
||||||
|
- TUI: forward unknown slash commands, include Gateway commands in autocomplete, and render slash replies as system output. (commit 1af227b61, commit 8195497ce, commit 6fba598ea)
|
||||||
|
- CLI: auth probe output polish (table output, inline errors, reduced noise, and wrap fixes in `clawdbot models status`). (commit da3f2b489, commit 00ae21bed, commit 31e59cd58, commit f7dc27f2d, commit 438e782f8, commit 886752217, commit aabe0bed3, commit 81535d512, commit c63144ab1)
|
||||||
- Media: only parse `MEDIA:` tags when they start the line to avoid stripping prose mentions. (#1206)
|
- Media: only parse `MEDIA:` tags when they start the line to avoid stripping prose mentions. (#1206)
|
||||||
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
|
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
|
||||||
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
|
- Skills: gate bird Homebrew install to macOS. (#1569) Thanks @bradleypriest.
|
||||||
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
|
|
||||||
- MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
|
|
||||||
- MS Teams (plugin): remove `.default` suffix from Bot Framework probe scope to avoid double-appending. (#1574) Thanks @Evizero.
|
|
||||||
- Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160)
|
|
||||||
- Agents: warn and ignore tool allowlists that only reference unknown or unloaded plugin tools. (#1566)
|
|
||||||
|
|
||||||
## 2026.1.22
|
## 2026.1.22
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ android {
|
|||||||
applicationId = "com.clawdbot.android"
|
applicationId = "com.clawdbot.android"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 202601230
|
versionCode = 202601240
|
||||||
versionName = "2026.1.23"
|
versionName = "2026.1.24"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -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.23</string>
|
<string>2026.1.24</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>20260123</string>
|
<string>20260124</string>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||||
|
|||||||
@@ -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.23</string>
|
<string>2026.1.24</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>20260123</string>
|
<string>20260124</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -81,8 +81,8 @@ targets:
|
|||||||
properties:
|
properties:
|
||||||
CFBundleDisplayName: Clawdbot
|
CFBundleDisplayName: Clawdbot
|
||||||
CFBundleIconName: AppIcon
|
CFBundleIconName: AppIcon
|
||||||
CFBundleShortVersionString: "2026.1.23"
|
CFBundleShortVersionString: "2026.1.24"
|
||||||
CFBundleVersion: "20260123"
|
CFBundleVersion: "20260124"
|
||||||
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.23"
|
CFBundleShortVersionString: "2026.1.24"
|
||||||
CFBundleVersion: "20260123"
|
CFBundleVersion: "20260124"
|
||||||
@@ -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.23</string>
|
<string>2026.1.24</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>202601230</string>
|
<string>202601240</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>Clawdbot</string>
|
<string>Clawdbot</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ and logged; a message that is only `HEARTBEAT_OK` is dropped.
|
|||||||
every: "30m", // default: 30m (0m disables)
|
every: "30m", // default: 30m (0m disables)
|
||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-5",
|
||||||
includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
|
includeReasoning: false, // default: false (deliver separate Reasoning: message when available)
|
||||||
target: "last", // last | whatsapp | telegram | discord | slack | signal | imessage | none
|
target: "last", // last | none | <channel id> (core or plugin, e.g. "bluebubbles")
|
||||||
to: "+15551234567", // optional channel-specific override
|
to: "+15551234567", // optional channel-specific override
|
||||||
prompt: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
|
prompt: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
|
||||||
ackMaxChars: 300 // max chars allowed after HEARTBEAT_OK
|
ackMaxChars: 300 // max chars allowed after HEARTBEAT_OK
|
||||||
|
|||||||
+168
-1
@@ -14,14 +14,24 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
|||||||
- [How do I open the dashboard after onboarding?](#how-do-i-open-the-dashboard-after-onboarding)
|
- [How do I open the dashboard after onboarding?](#how-do-i-open-the-dashboard-after-onboarding)
|
||||||
- [How do I authenticate the dashboard (token) on localhost vs remote?](#how-do-i-authenticate-the-dashboard-token-on-localhost-vs-remote)
|
- [How do I authenticate the dashboard (token) on localhost vs remote?](#how-do-i-authenticate-the-dashboard-token-on-localhost-vs-remote)
|
||||||
- [What runtime do I need?](#what-runtime-do-i-need)
|
- [What runtime do I need?](#what-runtime-do-i-need)
|
||||||
|
- [Does it run on Raspberry Pi?](#does-it-run-on-raspberry-pi)
|
||||||
|
- [Can I migrate my setup to a new machine (Mac mini) without redoing onboarding?](#can-i-migrate-my-setup-to-a-new-machine-mac-mini-without-redoing-onboarding)
|
||||||
|
- [Where do I see what’s new in the latest version?](#where-do-i-see-whats-new-in-the-latest-version)
|
||||||
|
- [I can't access docs.clawd.bot (SSL error). What now?](#i-cant-access-docsclawdbot-ssl-error-what-now)
|
||||||
|
- [How do I install the beta version, and what’s the difference between beta and dev?](#how-do-i-install-the-beta-version-and-whats-the-difference-between-beta-and-dev)
|
||||||
|
- [The docs didn’t answer my question — how do I get a better answer?](#the-docs-didnt-answer-my-question--how-do-i-get-a-better-answer)
|
||||||
|
- [How do I install Clawdbot on a VPS?](#how-do-i-install-clawdbot-on-a-vps)
|
||||||
- [What does the onboarding wizard actually do?](#what-does-the-onboarding-wizard-actually-do)
|
- [What does the onboarding wizard actually do?](#what-does-the-onboarding-wizard-actually-do)
|
||||||
- [How does Anthropic "setup-token" auth work?](#how-does-anthropic-setup-token-auth-work)
|
- [How does Anthropic "setup-token" auth work?](#how-does-anthropic-setup-token-auth-work)
|
||||||
- [Where do I find an Anthropic setup-token?](#where-do-i-find-an-anthropic-setup-token)
|
- [Where do I find an Anthropic setup-token?](#where-do-i-find-an-anthropic-setup-token)
|
||||||
- [Do you support Claude subscription auth (Claude Code OAuth)?](#do-you-support-claude-subscription-auth-claude-code-oauth)
|
- [Do you support Claude subscription auth (Claude Code OAuth)?](#do-you-support-claude-subscription-auth-claude-code-oauth)
|
||||||
|
- [Why am I seeing `HTTP 429: rate_limit_error` from Anthropic?](#why-am-i-seeing-http-429-rate_limit_error-from-anthropic)
|
||||||
- [Is AWS Bedrock supported?](#is-aws-bedrock-supported)
|
- [Is AWS Bedrock supported?](#is-aws-bedrock-supported)
|
||||||
- [How does Codex auth work?](#how-does-codex-auth-work)
|
- [How does Codex auth work?](#how-does-codex-auth-work)
|
||||||
|
- [Do you support OpenAI subscription auth (Codex OAuth)?](#do-you-support-openai-subscription-auth-codex-oauth)
|
||||||
- [Is a local model OK for casual chats?](#is-a-local-model-ok-for-casual-chats)
|
- [Is a local model OK for casual chats?](#is-a-local-model-ok-for-casual-chats)
|
||||||
- [How do I keep hosted model traffic in a specific region?](#how-do-i-keep-hosted-model-traffic-in-a-specific-region)
|
- [How do I keep hosted model traffic in a specific region?](#how-do-i-keep-hosted-model-traffic-in-a-specific-region)
|
||||||
|
- [Do I have to buy a Mac Mini to install this?](#do-i-have-to-buy-a-mac-mini-to-install-this)
|
||||||
- [Can I use Bun?](#can-i-use-bun)
|
- [Can I use Bun?](#can-i-use-bun)
|
||||||
- [Telegram: what goes in `allowFrom`?](#telegram-what-goes-in-allowfrom)
|
- [Telegram: what goes in `allowFrom`?](#telegram-what-goes-in-allowfrom)
|
||||||
- [Can multiple people use one WhatsApp number with different Clawdbots?](#can-multiple-people-use-one-whatsapp-number-with-different-clawdbots)
|
- [Can multiple people use one WhatsApp number with different Clawdbots?](#can-multiple-people-use-one-whatsapp-number-with-different-clawdbots)
|
||||||
@@ -34,6 +44,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
|||||||
- [Can I load skills from a custom folder?](#can-i-load-skills-from-a-custom-folder)
|
- [Can I load skills from a custom folder?](#can-i-load-skills-from-a-custom-folder)
|
||||||
- [How can I use different models for different tasks?](#how-can-i-use-different-models-for-different-tasks)
|
- [How can I use different models for different tasks?](#how-can-i-use-different-models-for-different-tasks)
|
||||||
- [How do I install skills on Linux?](#how-do-i-install-skills-on-linux)
|
- [How do I install skills on Linux?](#how-do-i-install-skills-on-linux)
|
||||||
|
- [Can Clawdbot run tasks on a schedule or continuously in the background?](#can-clawdbot-run-tasks-on-a-schedule-or-continuously-in-the-background)
|
||||||
- [Can I run Apple/macOS-only skills from Linux?](#can-i-run-applemacos-only-skills-from-linux)
|
- [Can I run Apple/macOS-only skills from Linux?](#can-i-run-applemacos-only-skills-from-linux)
|
||||||
- [Do you have a Notion or HeyGen integration?](#do-you-have-a-notion-or-heygen-integration)
|
- [Do you have a Notion or HeyGen integration?](#do-you-have-a-notion-or-heygen-integration)
|
||||||
- [How do I install the Chrome extension for browser takeover?](#how-do-i-install-the-chrome-extension-for-browser-takeover)
|
- [How do I install the Chrome extension for browser takeover?](#how-do-i-install-the-chrome-extension-for-browser-takeover)
|
||||||
@@ -81,8 +92,10 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
|||||||
- [Why doesn’t Clawdbot reply in a group?](#why-doesnt-clawdbot-reply-in-a-group)
|
- [Why doesn’t Clawdbot reply in a group?](#why-doesnt-clawdbot-reply-in-a-group)
|
||||||
- [Do groups/threads share context with DMs?](#do-groupsthreads-share-context-with-dms)
|
- [Do groups/threads share context with DMs?](#do-groupsthreads-share-context-with-dms)
|
||||||
- [How many workspaces and agents can I create?](#how-many-workspaces-and-agents-can-i-create)
|
- [How many workspaces and agents can I create?](#how-many-workspaces-and-agents-can-i-create)
|
||||||
|
- [Can I run multiple bots or chats at the same time (Slack), and how should I set that up?](#can-i-run-multiple-bots-or-chats-at-the-same-time-slack-and-how-should-i-set-that-up)
|
||||||
- [Models: defaults, selection, aliases, switching](#models-defaults-selection-aliases-switching)
|
- [Models: defaults, selection, aliases, switching](#models-defaults-selection-aliases-switching)
|
||||||
- [What is the “default model”?](#what-is-the-default-model)
|
- [What is the “default model”?](#what-is-the-default-model)
|
||||||
|
- [What model do you recommend?](#what-model-do-you-recommend)
|
||||||
- [How do I switch models on the fly (without restarting)?](#how-do-i-switch-models-on-the-fly-without-restarting)
|
- [How do I switch models on the fly (without restarting)?](#how-do-i-switch-models-on-the-fly-without-restarting)
|
||||||
- [Why do I see “Model … is not allowed” and then no reply?](#why-do-i-see-model-is-not-allowed-and-then-no-reply)
|
- [Why do I see “Model … is not allowed” and then no reply?](#why-do-i-see-model-is-not-allowed-and-then-no-reply)
|
||||||
- [Why do I see “Unknown model: minimax/MiniMax-M2.1”?](#why-do-i-see-unknown-model-minimaxminimax-m21)
|
- [Why do I see “Unknown model: minimax/MiniMax-M2.1”?](#why-do-i-see-unknown-model-minimaxminimax-m21)
|
||||||
@@ -231,6 +244,86 @@ See [Dashboard](/web/dashboard) and [Web surfaces](/web) for bind modes and auth
|
|||||||
|
|
||||||
Node **>= 22** is required. `pnpm` is recommended. Bun is **not recommended** for the Gateway.
|
Node **>= 22** is required. `pnpm` is recommended. Bun is **not recommended** for the Gateway.
|
||||||
|
|
||||||
|
### Does it run on Raspberry Pi?
|
||||||
|
|
||||||
|
Yes. The Gateway is lightweight — docs list **512MB–1GB RAM**, **1 core**, and about **500MB**
|
||||||
|
disk as enough for personal use, and note that a **Raspberry Pi 4 can run it**.
|
||||||
|
|
||||||
|
If you want extra headroom (logs, media, other services), **2GB is recommended**, but it’s
|
||||||
|
not a hard minimum.
|
||||||
|
|
||||||
|
### Can I migrate my setup to a new machine (Mac mini) without redoing onboarding?
|
||||||
|
|
||||||
|
Yes. Copy the **state directory** and **workspace**, then run Doctor once:
|
||||||
|
|
||||||
|
1) Install Clawdbot on the new machine.
|
||||||
|
2) Copy `$CLAWDBOT_STATE_DIR` (default: `~/.clawdbot`) from the old machine.
|
||||||
|
3) Copy your workspace (default: `~/clawd`).
|
||||||
|
4) Run `clawdbot doctor` and restart the Gateway service.
|
||||||
|
|
||||||
|
That preserves config, auth profiles, WhatsApp creds, sessions, and memory. If you’re in
|
||||||
|
remote mode, remember the gateway host owns the session store and workspace.
|
||||||
|
|
||||||
|
Related: [Where things live on disk](/help/faq#where-does-clawdbot-store-its-data),
|
||||||
|
[Agent workspace](/concepts/agent-workspace), [Doctor](/gateway/doctor),
|
||||||
|
[Remote mode](/gateway/remote).
|
||||||
|
|
||||||
|
### Where do I see what’s new in the latest version?
|
||||||
|
|
||||||
|
Check the GitHub changelog:
|
||||||
|
https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md
|
||||||
|
|
||||||
|
Newest entries are at the top. If the top section is marked **Unreleased**, the next dated
|
||||||
|
section is the latest shipped version. Entries are grouped by **Highlights**, **Changes**, and
|
||||||
|
**Fixes** (plus docs/other sections when needed).
|
||||||
|
|
||||||
|
### I can't access docs.clawd.bot (SSL error). What now?
|
||||||
|
|
||||||
|
Some Comcast/Xfinity connections incorrectly block `docs.clawd.bot` via Xfinity
|
||||||
|
Advanced Security. Disable it or allowlist `docs.clawd.bot`, then retry. More
|
||||||
|
detail: [Troubleshooting](/help/troubleshooting#docsclawdbot-shows-an-ssl-error-comcastxfinity).
|
||||||
|
|
||||||
|
If you still can't reach the site, the docs are mirrored on GitHub:
|
||||||
|
https://github.com/clawdbot/clawdbot/tree/main/docs
|
||||||
|
|
||||||
|
### How do I install the beta version, and what’s the difference between beta and dev?
|
||||||
|
|
||||||
|
**Beta** is a prerelease tag (`vYYYY.M.D-beta.N`) published to the npm dist‑tag `beta`.
|
||||||
|
**Dev** is the moving head of `main` (git); when published, it uses the npm dist‑tag `dev`.
|
||||||
|
|
||||||
|
One‑liners (macOS/Linux):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL --proto '=https' --tlsv1.2 https://clawd.bot/install.sh | bash -s -- --beta
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL --proto '=https' --tlsv1.2 https://clawd.bot/install.sh | bash -s -- --install-method git
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows installer (PowerShell):
|
||||||
|
https://clawd.bot/install.ps1
|
||||||
|
|
||||||
|
More detail: [Development channels](/install/development-channels) and [Installer flags](/install/installer).
|
||||||
|
|
||||||
|
### The docs didn’t answer my question — how do I get a better answer?
|
||||||
|
|
||||||
|
Use the **hackable (git) install** so you have the full source and docs locally, then ask
|
||||||
|
your bot (or Claude/Codex) *from that folder* so it can read the repo and answer precisely.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://clawd.bot/install.sh | bash -s -- --install-method git
|
||||||
|
```
|
||||||
|
|
||||||
|
More detail: [Install](/install) and [Installer flags](/install/installer).
|
||||||
|
|
||||||
|
### How do I install Clawdbot on a VPS?
|
||||||
|
|
||||||
|
Any Linux VPS works. Install on the server, then use SSH/Tailscale to reach the Gateway.
|
||||||
|
|
||||||
|
Guides: [exe.dev](/platforms/exe-dev), [Hetzner](/platforms/hetzner), [Fly.io](/platforms/fly).
|
||||||
|
Remote access: [Gateway remote](/gateway/remote).
|
||||||
|
|
||||||
### What does the onboarding wizard actually do?
|
### What does the onboarding wizard actually do?
|
||||||
|
|
||||||
`clawdbot onboard` is the recommended setup path. In **local mode** it walks you through:
|
`clawdbot onboard` is the recommended setup path. In **local mode** it walks you through:
|
||||||
@@ -268,6 +361,16 @@ Yes. Clawdbot can **reuse Claude Code CLI credentials** (OAuth) and also support
|
|||||||
|
|
||||||
Note: Claude subscription access is governed by Anthropic’s terms. For production or multi‑user workloads, API keys are usually the safer choice.
|
Note: Claude subscription access is governed by Anthropic’s terms. For production or multi‑user workloads, API keys are usually the safer choice.
|
||||||
|
|
||||||
|
### Why am I seeing `HTTP 429: rate_limit_error` from Anthropic?
|
||||||
|
|
||||||
|
That means your **Anthropic quota/rate limit** is exhausted for the current window. If you
|
||||||
|
use a **Claude subscription** (setup‑token or Claude Code OAuth), wait for the window to
|
||||||
|
reset or upgrade your plan. If you use an **Anthropic API key**, check the Anthropic Console
|
||||||
|
for usage/billing and raise limits as needed.
|
||||||
|
|
||||||
|
Tip: set a **fallback model** so Clawdbot can keep replying while a provider is rate‑limited.
|
||||||
|
See [Models](/cli/models) and [OAuth](/concepts/oauth).
|
||||||
|
|
||||||
### Is AWS Bedrock supported?
|
### Is AWS Bedrock supported?
|
||||||
|
|
||||||
Yes — via pi‑ai’s **Amazon Bedrock (Converse)** provider with **manual config**. You must supply AWS credentials/region on the gateway host and add a Bedrock provider entry in your models config. See [Amazon Bedrock](/bedrock) and [Model providers](/providers/models). If you prefer a managed key flow, an OpenAI‑compatible proxy in front of Bedrock is still a valid option.
|
Yes — via pi‑ai’s **Amazon Bedrock (Converse)** provider with **manual config**. You must supply AWS credentials/region on the gateway host and add a Bedrock provider entry in your models config. See [Amazon Bedrock](/bedrock) and [Model providers](/providers/models). If you prefer a managed key flow, an OpenAI‑compatible proxy in front of Bedrock is still a valid option.
|
||||||
@@ -276,6 +379,14 @@ Yes — via pi‑ai’s **Amazon Bedrock (Converse)** provider with **manual con
|
|||||||
|
|
||||||
Clawdbot supports **OpenAI Code (Codex)** via OAuth or by reusing your Codex CLI login (`~/.codex/auth.json`). The wizard can import the CLI login or run the OAuth flow and will set the default model to `openai-codex/gpt-5.2` when appropriate. See [Model providers](/concepts/model-providers) and [Wizard](/start/wizard).
|
Clawdbot supports **OpenAI Code (Codex)** via OAuth or by reusing your Codex CLI login (`~/.codex/auth.json`). The wizard can import the CLI login or run the OAuth flow and will set the default model to `openai-codex/gpt-5.2` when appropriate. See [Model providers](/concepts/model-providers) and [Wizard](/start/wizard).
|
||||||
|
|
||||||
|
### Do you support OpenAI subscription auth (Codex OAuth)?
|
||||||
|
|
||||||
|
Yes. Clawdbot fully supports **OpenAI Code (Codex) subscription OAuth** and can also reuse an
|
||||||
|
existing Codex CLI login (`~/.codex/auth.json`) on the gateway host. The onboarding wizard
|
||||||
|
can import the CLI login or run the OAuth flow for you.
|
||||||
|
|
||||||
|
See [OAuth](/concepts/oauth), [Model providers](/concepts/model-providers), and [Wizard](/start/wizard).
|
||||||
|
|
||||||
### Is a local model OK for casual chats?
|
### Is a local model OK for casual chats?
|
||||||
|
|
||||||
Usually no. Clawdbot needs large context + strong safety; small cards truncate and leak. If you must, run the **largest** MiniMax M2.1 build you can locally (LM Studio) and see [/gateway/local-models](/gateway/local-models). Smaller/quantized models increase prompt-injection risk — see [Security](/gateway/security).
|
Usually no. Clawdbot needs large context + strong safety; small cards truncate and leak. If you must, run the **largest** MiniMax M2.1 build you can locally (LM Studio) and see [/gateway/local-models](/gateway/local-models). Smaller/quantized models increase prompt-injection risk — see [Security](/gateway/security).
|
||||||
@@ -284,6 +395,17 @@ Usually no. Clawdbot needs large context + strong safety; small cards truncate a
|
|||||||
|
|
||||||
Pick region-pinned endpoints. OpenRouter exposes US-hosted options for MiniMax, Kimi, and GLM; choose the US-hosted variant to keep data in-region. You can still list Anthropic/OpenAI alongside these by using `models.mode: "merge"` so fallbacks stay available while respecting the regioned provider you select.
|
Pick region-pinned endpoints. OpenRouter exposes US-hosted options for MiniMax, Kimi, and GLM; choose the US-hosted variant to keep data in-region. You can still list Anthropic/OpenAI alongside these by using `models.mode: "merge"` so fallbacks stay available while respecting the regioned provider you select.
|
||||||
|
|
||||||
|
### Do I have to buy a Mac Mini to install this?
|
||||||
|
|
||||||
|
No. Clawdbot runs on macOS or Linux (Windows via WSL2). A Mac mini is optional — some people
|
||||||
|
buy one as an always‑on host, but a small VPS, home server, or Raspberry Pi‑class box works too.
|
||||||
|
|
||||||
|
You only need a Mac **for macOS‑only tools**. For iMessage, you can keep the Gateway on Linux
|
||||||
|
and run `imsg` on any Mac over SSH by pointing `channels.imessage.cliPath` at an SSH wrapper.
|
||||||
|
If you want other macOS‑only tools, run the Gateway on a Mac or pair a macOS node.
|
||||||
|
|
||||||
|
Docs: [iMessage](/channels/imessage), [Nodes](/nodes), [Mac remote mode](/platforms/mac/remote).
|
||||||
|
|
||||||
### Can I use Bun?
|
### Can I use Bun?
|
||||||
|
|
||||||
Bun is **not recommended**. We see runtime bugs, especially with WhatsApp and Telegram.
|
Bun is **not recommended**. We see runtime bugs, especially with WhatsApp and Telegram.
|
||||||
@@ -405,6 +527,17 @@ npm i -g clawdhub
|
|||||||
pnpm add -g clawdhub
|
pnpm add -g clawdhub
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Can Clawdbot run tasks on a schedule or continuously in the background?
|
||||||
|
|
||||||
|
Yes. Use the Gateway scheduler:
|
||||||
|
|
||||||
|
- **Cron jobs** for scheduled or recurring tasks (persist across restarts).
|
||||||
|
- **Heartbeat** for “main session” periodic checks.
|
||||||
|
- **Isolated jobs** for autonomous agents that post summaries or deliver to chats.
|
||||||
|
|
||||||
|
Docs: [Cron jobs](/automation/cron-jobs), [Cron vs Heartbeat](/automation/cron-vs-heartbeat),
|
||||||
|
[Heartbeat](/gateway/heartbeat).
|
||||||
|
|
||||||
### Is there a way to run Apple/macOS-only skills if my Gateway runs on Linux?
|
### Is there a way to run Apple/macOS-only skills if my Gateway runs on Linux?
|
||||||
|
|
||||||
Not directly. macOS skills are gated by `metadata.clawdbot.os` plus required binaries, and skills only appear in the system prompt when they are eligible on the **Gateway host**. On Linux, `darwin`-only skills (like `imsg`, `apple-notes`, `apple-reminders`) will not load unless you override the gating.
|
Not directly. macOS skills are gated by `metadata.clawdbot.os` plus required binaries, and skills only appear in the system prompt when they are eligible on the **Gateway host**. On Linux, `darwin`-only skills (like `imsg`, `apple-notes`, `apple-reminders`) will not load unless you override the gating.
|
||||||
@@ -774,7 +907,8 @@ Docs: [Nodes](/nodes), [Nodes CLI](/cli/nodes), [Chrome extension](/tools/chrome
|
|||||||
### Do nodes run a gateway service?
|
### Do nodes run a gateway service?
|
||||||
|
|
||||||
No. Only **one gateway** should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)). Nodes are peripherals that connect
|
No. Only **one gateway** should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)). Nodes are peripherals that connect
|
||||||
to the gateway (iOS/Android nodes, or macOS “node mode” in the menubar app).
|
to the gateway (iOS/Android nodes, or macOS “node mode” in the menubar app). For headless node
|
||||||
|
hosts and CLI control, see [Node host CLI](/cli/node).
|
||||||
|
|
||||||
A full restart is required for `gateway`, `discovery`, and `canvasHost` changes.
|
A full restart is required for `gateway`, `discovery`, and `canvasHost` changes.
|
||||||
|
|
||||||
@@ -1041,6 +1175,24 @@ Tips:
|
|||||||
- Prune old sessions (delete JSONL or store entries) if disk grows.
|
- Prune old sessions (delete JSONL or store entries) if disk grows.
|
||||||
- Use `clawdbot doctor` to spot stray workspaces and profile mismatches.
|
- Use `clawdbot doctor` to spot stray workspaces and profile mismatches.
|
||||||
|
|
||||||
|
### Can I run multiple bots or chats at the same time (Slack), and how should I set that up?
|
||||||
|
|
||||||
|
Yes. Use **Multi‑Agent Routing** to run multiple isolated agents and route inbound messages by
|
||||||
|
channel/account/peer. Slack is supported as a channel and can be bound to specific agents.
|
||||||
|
|
||||||
|
Browser access is powerful but not “do anything a human can” — anti‑bot, CAPTCHAs, and MFA can
|
||||||
|
still block automation. For the most reliable browser control, use the Chrome extension relay
|
||||||
|
on the machine that runs the browser (and keep the Gateway anywhere).
|
||||||
|
|
||||||
|
Best‑practice setup:
|
||||||
|
- Always‑on Gateway host (VPS/Mac mini).
|
||||||
|
- One agent per role (bindings).
|
||||||
|
- Slack channel(s) bound to those agents.
|
||||||
|
- Local browser via extension relay (or a node) when needed.
|
||||||
|
|
||||||
|
Docs: [Multi‑Agent Routing](/concepts/multi-agent), [Slack](/channels/slack),
|
||||||
|
[Browser](/tools/browser), [Chrome extension](/tools/chrome-extension), [Nodes](/nodes).
|
||||||
|
|
||||||
## Models: defaults, selection, aliases, switching
|
## Models: defaults, selection, aliases, switching
|
||||||
|
|
||||||
### What is the “default model”?
|
### What is the “default model”?
|
||||||
@@ -1053,6 +1205,21 @@ agents.defaults.model.primary
|
|||||||
|
|
||||||
Models are referenced as `provider/model` (example: `anthropic/claude-opus-4-5`). If you omit the provider, Clawdbot currently assumes `anthropic` as a temporary deprecation fallback — but you should still **explicitly** set `provider/model`.
|
Models are referenced as `provider/model` (example: `anthropic/claude-opus-4-5`). If you omit the provider, Clawdbot currently assumes `anthropic` as a temporary deprecation fallback — but you should still **explicitly** set `provider/model`.
|
||||||
|
|
||||||
|
### What model do you recommend?
|
||||||
|
|
||||||
|
**Recommended default:** `anthropic/claude-opus-4-5`.
|
||||||
|
**Good alternative:** `anthropic/claude-sonnet-4-5`.
|
||||||
|
**Reliable (less character):** `openai/gpt-5.2` — nearly as good as Opus, just less personality.
|
||||||
|
**Budget:** `zai/glm-4.7`.
|
||||||
|
|
||||||
|
MiniMax M2.1 has its own docs: [MiniMax](/providers/minimax) and
|
||||||
|
[Local models](/gateway/local-models).
|
||||||
|
|
||||||
|
Strong warning: weaker/over-quantized models are more vulnerable to prompt
|
||||||
|
injection and unsafe behavior. See [Security](/gateway/security).
|
||||||
|
|
||||||
|
More context: [Models](/concepts/models).
|
||||||
|
|
||||||
### How do I switch models on the fly (without restarting)?
|
### How do I switch models on the fly (without restarting)?
|
||||||
|
|
||||||
Use the `/model` command as a standalone message:
|
Use the `/model` command as a standalone message:
|
||||||
|
|||||||
@@ -33,6 +33,22 @@ Almost always a Node/npm PATH issue. Start here:
|
|||||||
|
|
||||||
- [Install (Node/npm PATH sanity)](/install#nodejs--npm-path-sanity)
|
- [Install (Node/npm PATH sanity)](/install#nodejs--npm-path-sanity)
|
||||||
|
|
||||||
|
### Installer fails (or you need full logs)
|
||||||
|
|
||||||
|
Re-run the installer in verbose mode to see the full trace and npm output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://clawd.bot/install.sh | bash -s -- --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
For beta installs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://clawd.bot/install.sh | bash -s -- --beta --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also set `CLAWDBOT_VERBOSE=1` instead of the flag.
|
||||||
|
|
||||||
### Gateway “unauthorized”, can’t connect, or keeps reconnecting
|
### Gateway “unauthorized”, can’t connect, or keeps reconnecting
|
||||||
|
|
||||||
- [Gateway troubleshooting](/gateway/troubleshooting)
|
- [Gateway troubleshooting](/gateway/troubleshooting)
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ cat > /data/clawdbot.json << 'EOF'
|
|||||||
"bind": "auto"
|
"bind": "auto"
|
||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"lastTouchedVersion": "2026.1.23"
|
"lastTouchedVersion": "2026.1.24"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -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.23 \
|
APP_VERSION=2026.1.24 \
|
||||||
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.23.zip
|
ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.24.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.23.dmg
|
scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.24.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.23.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.23 \
|
APP_VERSION=2026.1.24 \
|
||||||
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.23.dSYM.zip
|
ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.24.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.23.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.24.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.23.zip` (and `Clawdbot-2026.1.23.dSYM.zip`) to the GitHub release for tag `v2026.1.23`.
|
- Upload `Clawdbot-2026.1.24.zip` (and `Clawdbot-2026.1.24.dSYM.zip`) to the GitHub release for tag `v2026.1.24`.
|
||||||
- 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.
|
||||||
|
|||||||
@@ -92,6 +92,21 @@ In group chats where you receive every message, be **smart about when to contrib
|
|||||||
|
|
||||||
Participate, don't dominate.
|
Participate, don't dominate.
|
||||||
|
|
||||||
|
### 😊 React Like a Human!
|
||||||
|
On platforms that support reactions (Discord, Slack), use emoji reactions naturally:
|
||||||
|
|
||||||
|
**React when:**
|
||||||
|
- You appreciate something but don't need to reply (👍, ❤️, 🙌)
|
||||||
|
- Something made you laugh (😂, 💀)
|
||||||
|
- You find it interesting or thought-provoking (🤔, 💡)
|
||||||
|
- You want to acknowledge without interrupting the flow
|
||||||
|
- It's a simple yes/no or approval situation (✅, 👀)
|
||||||
|
|
||||||
|
**Why it matters:**
|
||||||
|
Reactions are lightweight social signals. Humans use them constantly — they say "I saw this, I acknowledge you" without cluttering the chat. You should too.
|
||||||
|
|
||||||
|
**Don't overdo it:** One reaction per message max. Pick the one that fits best.
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
Skills provide your tools. When you need one, check its `SKILL.md`. Keep local notes (camera names, SSH details, voice preferences) in `TOOLS.md`.
|
Skills provide your tools. When you need one, check its `SKILL.md`. Keep local notes (camera names, SSH details, voice preferences) in `TOOLS.md`.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"./index.ts"
|
"./index.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"peerDependencies": {
|
||||||
"clawdbot": "workspace:*"
|
"clawdbot": ">=2026.1.23-1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clawdbot",
|
"name": "clawdbot",
|
||||||
"version": "2026.1.23",
|
"version": "2026.1.24-0",
|
||||||
"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",
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
"dist/slack/**",
|
"dist/slack/**",
|
||||||
"dist/telegram/**",
|
"dist/telegram/**",
|
||||||
"dist/tui/**",
|
"dist/tui/**",
|
||||||
|
"dist/tts/**",
|
||||||
"dist/web/**",
|
"dist/web/**",
|
||||||
"dist/wizard/**",
|
"dist/wizard/**",
|
||||||
"dist/*.js",
|
"dist/*.js",
|
||||||
|
|||||||
Generated
+83
-2
@@ -335,8 +335,8 @@ importers:
|
|||||||
extensions/memory-core:
|
extensions/memory-core:
|
||||||
dependencies:
|
dependencies:
|
||||||
clawdbot:
|
clawdbot:
|
||||||
specifier: workspace:*
|
specifier: '>=2026.1.23-1'
|
||||||
version: link:../..
|
version: 2026.1.23(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3)
|
||||||
|
|
||||||
extensions/memory-lancedb:
|
extensions/memory-lancedb:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2987,6 +2987,11 @@ packages:
|
|||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
|
clawdbot@2026.1.23:
|
||||||
|
resolution: {integrity: sha512-w8RjScbxj3YbJYtcB0GBITqmyUYegVbBXDgu/zRxB4AB/SEErR6BNWGeZDWQNFaMftIyJhjttACxSd8G20aREA==}
|
||||||
|
engines: {node: '>=22.12.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
cli-cursor@5.0.0:
|
cli-cursor@5.0.0:
|
||||||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -8368,6 +8373,82 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
|
clawdbot@2026.1.23(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3):
|
||||||
|
dependencies:
|
||||||
|
'@agentclientprotocol/sdk': 0.13.1(zod@4.3.6)
|
||||||
|
'@aws-sdk/client-bedrock': 3.975.0
|
||||||
|
'@buape/carbon': 0.14.0(hono@4.11.4)
|
||||||
|
'@clack/prompts': 0.11.0
|
||||||
|
'@grammyjs/runner': 2.0.3(grammy@1.39.3)
|
||||||
|
'@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3)
|
||||||
|
'@homebridge/ciao': 1.3.4
|
||||||
|
'@lydell/node-pty': 1.2.0-beta.3
|
||||||
|
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||||
|
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||||
|
'@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
||||||
|
'@mariozechner/pi-tui': 0.49.3
|
||||||
|
'@mozilla/readability': 0.6.0
|
||||||
|
'@sinclair/typebox': 0.34.47
|
||||||
|
'@slack/bolt': 4.6.0(@types/express@5.0.6)
|
||||||
|
'@slack/web-api': 7.13.0
|
||||||
|
'@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5)
|
||||||
|
ajv: 8.17.1
|
||||||
|
body-parser: 2.2.2
|
||||||
|
chalk: 5.6.2
|
||||||
|
chokidar: 5.0.0
|
||||||
|
chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482)
|
||||||
|
cli-highlight: 2.1.11
|
||||||
|
commander: 14.0.2
|
||||||
|
croner: 9.1.0
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
discord-api-types: 0.38.37
|
||||||
|
dotenv: 17.2.3
|
||||||
|
express: 5.2.1
|
||||||
|
file-type: 21.3.0
|
||||||
|
grammy: 1.39.3
|
||||||
|
hono: 4.11.4
|
||||||
|
jiti: 2.6.1
|
||||||
|
json5: 2.2.3
|
||||||
|
jszip: 3.10.1
|
||||||
|
linkedom: 0.18.12
|
||||||
|
long: 5.3.2
|
||||||
|
markdown-it: 14.1.0
|
||||||
|
osc-progress: 0.3.0
|
||||||
|
pdfjs-dist: 5.4.530
|
||||||
|
playwright-core: 1.58.0
|
||||||
|
proper-lockfile: 4.1.2
|
||||||
|
qrcode-terminal: 0.12.0
|
||||||
|
sharp: 0.34.5
|
||||||
|
sqlite-vec: 0.1.7-alpha.2
|
||||||
|
tar: 7.5.4
|
||||||
|
tslog: 4.10.2
|
||||||
|
undici: 7.19.0
|
||||||
|
ws: 8.19.0
|
||||||
|
yaml: 2.8.2
|
||||||
|
zod: 4.3.6
|
||||||
|
optionalDependencies:
|
||||||
|
'@napi-rs/canvas': 0.1.88
|
||||||
|
node-llama-cpp: 3.15.0(typescript@5.9.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@discordjs/opus'
|
||||||
|
- '@modelcontextprotocol/sdk'
|
||||||
|
- '@types/express'
|
||||||
|
- audio-decode
|
||||||
|
- aws-crt
|
||||||
|
- bufferutil
|
||||||
|
- canvas
|
||||||
|
- debug
|
||||||
|
- devtools-protocol
|
||||||
|
- encoding
|
||||||
|
- ffmpeg-static
|
||||||
|
- jimp
|
||||||
|
- link-preview-js
|
||||||
|
- node-opus
|
||||||
|
- opusscript
|
||||||
|
- supports-color
|
||||||
|
- typescript
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
cli-cursor@5.0.0:
|
cli-cursor@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
restore-cursor: 5.1.0
|
restore-cursor: 5.1.0
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ COPY scripts ./scripts
|
|||||||
COPY docs ./docs
|
COPY docs ./docs
|
||||||
COPY skills ./skills
|
COPY skills ./skills
|
||||||
COPY patches ./patches
|
COPY patches ./patches
|
||||||
|
COPY ui ./ui
|
||||||
COPY extensions/memory-core ./extensions/memory-core
|
COPY extensions/memory-core ./extensions/memory-core
|
||||||
|
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
RUN pnpm ui:build
|
||||||
|
|
||||||
CMD ["bash"]
|
CMD ["bash"]
|
||||||
@@ -69,6 +69,9 @@ export function isModernModelRef(ref: ModelRef): boolean {
|
|||||||
if (provider === "opencode" && id.endsWith("-free")) {
|
if (provider === "opencode" && id.endsWith("-free")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (provider === "opencode" && id === "alpha-glm-4.7") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (provider === "openrouter" || provider === "opencode") {
|
if (provider === "openrouter" || provider === "opencode") {
|
||||||
return matchesAny(id, [
|
return matchesAny(id, [
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ describe("getOpencodeZenStaticFallbackModels", () => {
|
|||||||
it("returns an array of models", () => {
|
it("returns an array of models", () => {
|
||||||
const models = getOpencodeZenStaticFallbackModels();
|
const models = getOpencodeZenStaticFallbackModels();
|
||||||
expect(Array.isArray(models)).toBe(true);
|
expect(Array.isArray(models)).toBe(true);
|
||||||
expect(models.length).toBe(10);
|
expect(models.length).toBe(9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes Claude, GPT, Gemini, and GLM models", () => {
|
it("includes Claude, GPT, Gemini, and GLM models", () => {
|
||||||
|
|||||||
@@ -67,10 +67,9 @@ export const OPENCODE_ZEN_MODEL_ALIASES: Record<string, string> = {
|
|||||||
"gemini-2.5-pro": "gemini-3-pro",
|
"gemini-2.5-pro": "gemini-3-pro",
|
||||||
"gemini-2.5-flash": "gemini-3-flash",
|
"gemini-2.5-flash": "gemini-3-flash",
|
||||||
|
|
||||||
// GLM (free + alpha)
|
// GLM (free)
|
||||||
glm: "glm-4.7",
|
glm: "glm-4.7",
|
||||||
"glm-free": "glm-4.7",
|
"glm-free": "glm-4.7",
|
||||||
"alpha-glm": "alpha-glm-4.7",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,7 +121,6 @@ const MODEL_COSTS: Record<
|
|||||||
},
|
},
|
||||||
"claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
"claude-opus-4-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||||
"gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 },
|
"gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 },
|
||||||
"alpha-glm-4.7": { input: 0.6, output: 2.2, cacheRead: 0.6, cacheWrite: 0 },
|
|
||||||
"gpt-5.1-codex-mini": {
|
"gpt-5.1-codex-mini": {
|
||||||
input: 0.25,
|
input: 0.25,
|
||||||
output: 2,
|
output: 2,
|
||||||
@@ -147,7 +145,6 @@ const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|||||||
"gpt-5.1-codex": 400000,
|
"gpt-5.1-codex": 400000,
|
||||||
"claude-opus-4-5": 200000,
|
"claude-opus-4-5": 200000,
|
||||||
"gemini-3-pro": 1048576,
|
"gemini-3-pro": 1048576,
|
||||||
"alpha-glm-4.7": 204800,
|
|
||||||
"gpt-5.1-codex-mini": 400000,
|
"gpt-5.1-codex-mini": 400000,
|
||||||
"gpt-5.1": 400000,
|
"gpt-5.1": 400000,
|
||||||
"glm-4.7": 204800,
|
"glm-4.7": 204800,
|
||||||
@@ -164,7 +161,6 @@ const MODEL_MAX_TOKENS: Record<string, number> = {
|
|||||||
"gpt-5.1-codex": 128000,
|
"gpt-5.1-codex": 128000,
|
||||||
"claude-opus-4-5": 64000,
|
"claude-opus-4-5": 64000,
|
||||||
"gemini-3-pro": 65536,
|
"gemini-3-pro": 65536,
|
||||||
"alpha-glm-4.7": 131072,
|
|
||||||
"gpt-5.1-codex-mini": 128000,
|
"gpt-5.1-codex-mini": 128000,
|
||||||
"gpt-5.1": 128000,
|
"gpt-5.1": 128000,
|
||||||
"glm-4.7": 131072,
|
"glm-4.7": 131072,
|
||||||
@@ -201,7 +197,6 @@ const MODEL_NAMES: Record<string, string> = {
|
|||||||
"gpt-5.1-codex": "GPT-5.1 Codex",
|
"gpt-5.1-codex": "GPT-5.1 Codex",
|
||||||
"claude-opus-4-5": "Claude Opus 4.5",
|
"claude-opus-4-5": "Claude Opus 4.5",
|
||||||
"gemini-3-pro": "Gemini 3 Pro",
|
"gemini-3-pro": "Gemini 3 Pro",
|
||||||
"alpha-glm-4.7": "Alpha GLM-4.7",
|
|
||||||
"gpt-5.1-codex-mini": "GPT-5.1 Codex Mini",
|
"gpt-5.1-codex-mini": "GPT-5.1 Codex Mini",
|
||||||
"gpt-5.1": "GPT-5.1",
|
"gpt-5.1": "GPT-5.1",
|
||||||
"glm-4.7": "GLM-4.7",
|
"glm-4.7": "GLM-4.7",
|
||||||
@@ -229,7 +224,6 @@ export function getOpencodeZenStaticFallbackModels(): ModelDefinitionConfig[] {
|
|||||||
"gpt-5.1-codex",
|
"gpt-5.1-codex",
|
||||||
"claude-opus-4-5",
|
"claude-opus-4-5",
|
||||||
"gemini-3-pro",
|
"gemini-3-pro",
|
||||||
"alpha-glm-4.7",
|
|
||||||
"gpt-5.1-codex-mini",
|
"gpt-5.1-codex-mini",
|
||||||
"gpt-5.1",
|
"gpt-5.1",
|
||||||
"glm-4.7",
|
"glm-4.7",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Command } from "commander";
|
|||||||
import type { CronJob } from "../../cron/types.js";
|
import type { CronJob } from "../../cron/types.js";
|
||||||
import { danger } from "../../globals.js";
|
import { danger } from "../../globals.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
|
import { sanitizeAgentId } from "../../routing/session-key.js";
|
||||||
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
||||||
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||||
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
|
import { parsePositiveIntOrUndefined } from "../program/helpers.js";
|
||||||
@@ -138,7 +139,9 @@ export function registerCronAddCommand(cron: Command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const agentId =
|
const agentId =
|
||||||
typeof opts.agent === "string" && opts.agent.trim() ? opts.agent.trim() : undefined;
|
typeof opts.agent === "string" && opts.agent.trim()
|
||||||
|
? sanitizeAgentId(opts.agent.trim())
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const payload = (() => {
|
const payload = (() => {
|
||||||
const systemEvent = typeof opts.systemEvent === "string" ? opts.systemEvent.trim() : "";
|
const systemEvent = typeof opts.systemEvent === "string" ? opts.systemEvent.trim() : "";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { danger } from "../../globals.js";
|
import { danger } from "../../globals.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
|
import { sanitizeAgentId } from "../../routing/session-key.js";
|
||||||
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||||
import {
|
import {
|
||||||
getCronChannelOptions,
|
getCronChannelOptions,
|
||||||
@@ -90,7 +91,7 @@ export function registerCronEditCommand(cron: Command) {
|
|||||||
throw new Error("Use --agent or --clear-agent, not both");
|
throw new Error("Use --agent or --clear-agent, not both");
|
||||||
}
|
}
|
||||||
if (typeof opts.agent === "string" && opts.agent.trim()) {
|
if (typeof opts.agent === "string" && opts.agent.trim()) {
|
||||||
patch.agentId = opts.agent.trim();
|
patch.agentId = sanitizeAgentId(opts.agent.trim());
|
||||||
}
|
}
|
||||||
if (opts.clearAgent) {
|
if (opts.clearAgent) {
|
||||||
patch.agentId = null;
|
patch.agentId = null;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ async function writePluginFixture(params: {
|
|||||||
dir: string;
|
dir: string;
|
||||||
id: string;
|
id: string;
|
||||||
schema: Record<string, unknown>;
|
schema: Record<string, unknown>;
|
||||||
|
channels?: string[];
|
||||||
}) {
|
}) {
|
||||||
await fs.mkdir(params.dir, { recursive: true });
|
await fs.mkdir(params.dir, { recursive: true });
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
@@ -16,16 +17,16 @@ async function writePluginFixture(params: {
|
|||||||
`export default { id: "${params.id}", register() {} };`,
|
`export default { id: "${params.id}", register() {} };`,
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
const manifest: Record<string, unknown> = {
|
||||||
|
id: params.id,
|
||||||
|
configSchema: params.schema,
|
||||||
|
};
|
||||||
|
if (params.channels) {
|
||||||
|
manifest.channels = params.channels;
|
||||||
|
}
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(params.dir, "clawdbot.plugin.json"),
|
path.join(params.dir, "clawdbot.plugin.json"),
|
||||||
JSON.stringify(
|
JSON.stringify(manifest, null, 2),
|
||||||
{
|
|
||||||
id: params.id,
|
|
||||||
configSchema: params.schema,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -149,4 +150,43 @@ describe("config plugin validation", () => {
|
|||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts plugin heartbeat targets", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot");
|
||||||
|
const pluginDir = path.join(home, "bluebubbles-plugin");
|
||||||
|
await writePluginFixture({
|
||||||
|
dir: pluginDir,
|
||||||
|
id: "bluebubbles-plugin",
|
||||||
|
channels: ["bluebubbles"],
|
||||||
|
schema: { type: "object" },
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.resetModules();
|
||||||
|
const { validateConfigObjectWithPlugins } = await import("./config.js");
|
||||||
|
const res = validateConfigObjectWithPlugins({
|
||||||
|
agents: { defaults: { heartbeat: { target: "bluebubbles" } }, list: [{ id: "pi" }] },
|
||||||
|
plugins: { enabled: false, load: { paths: [pluginDir] } },
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unknown heartbeat targets", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot");
|
||||||
|
vi.resetModules();
|
||||||
|
const { validateConfigObjectWithPlugins } = await import("./config.js");
|
||||||
|
const res = validateConfigObjectWithPlugins({
|
||||||
|
agents: { defaults: { heartbeat: { target: "not-a-channel" } }, list: [{ id: "pi" }] },
|
||||||
|
});
|
||||||
|
expect(res.ok).toBe(false);
|
||||||
|
if (!res.ok) {
|
||||||
|
expect(res.issues).toContainEqual({
|
||||||
|
path: "agents.defaults.heartbeat.target",
|
||||||
|
message: "unknown heartbeat target: not-a-channel",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -84,4 +84,22 @@ describe("config schema", () => {
|
|||||||
const channelProps = channelSchema?.properties as Record<string, unknown> | undefined;
|
const channelProps = channelSchema?.properties as Record<string, unknown> | undefined;
|
||||||
expect(channelProps?.accessToken).toBeTruthy();
|
expect(channelProps?.accessToken).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds heartbeat target hints with dynamic channels", () => {
|
||||||
|
const res = buildConfigSchema({
|
||||||
|
channels: [
|
||||||
|
{
|
||||||
|
id: "bluebubbles",
|
||||||
|
label: "BlueBubbles",
|
||||||
|
configSchema: { type: "object" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultsHint = res.uiHints["agents.defaults.heartbeat.target"];
|
||||||
|
const listHint = res.uiHints["agents.list.*.heartbeat.target"];
|
||||||
|
expect(defaultsHint?.help).toContain("bluebubbles");
|
||||||
|
expect(defaultsHint?.help).toContain("last");
|
||||||
|
expect(listHint?.help).toContain("bluebubbles");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+43
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { CHANNEL_IDS } from "../channels/registry.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { ClawdbotSchema } from "./zod-schema.js";
|
import { ClawdbotSchema } from "./zod-schema.js";
|
||||||
|
|
||||||
@@ -807,6 +808,44 @@ function applyChannelHints(hints: ConfigUiHints, channels: ChannelUiMetadata[]):
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function listHeartbeatTargetChannels(channels: ChannelUiMetadata[]): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const ordered: string[] = [];
|
||||||
|
for (const id of CHANNEL_IDS) {
|
||||||
|
const normalized = id.trim().toLowerCase();
|
||||||
|
if (!normalized || seen.has(normalized)) continue;
|
||||||
|
seen.add(normalized);
|
||||||
|
ordered.push(normalized);
|
||||||
|
}
|
||||||
|
for (const channel of channels) {
|
||||||
|
const normalized = channel.id.trim().toLowerCase();
|
||||||
|
if (!normalized || seen.has(normalized)) continue;
|
||||||
|
seen.add(normalized);
|
||||||
|
ordered.push(normalized);
|
||||||
|
}
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyHeartbeatTargetHints(
|
||||||
|
hints: ConfigUiHints,
|
||||||
|
channels: ChannelUiMetadata[],
|
||||||
|
): ConfigUiHints {
|
||||||
|
const next: ConfigUiHints = { ...hints };
|
||||||
|
const channelList = listHeartbeatTargetChannels(channels);
|
||||||
|
const channelHelp = channelList.length ? ` Known channels: ${channelList.join(", ")}.` : "";
|
||||||
|
const help = `Delivery target ("last", "none", or a channel id).${channelHelp}`;
|
||||||
|
const paths = ["agents.defaults.heartbeat.target", "agents.list.*.heartbeat.target"];
|
||||||
|
for (const path of paths) {
|
||||||
|
const current = next[path] ?? {};
|
||||||
|
next[path] = {
|
||||||
|
...current,
|
||||||
|
help: current.help ?? help,
|
||||||
|
placeholder: current.placeholder ?? "last",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
function applyPluginSchemas(schema: ConfigSchema, plugins: PluginUiMetadata[]): ConfigSchema {
|
function applyPluginSchemas(schema: ConfigSchema, plugins: PluginUiMetadata[]): ConfigSchema {
|
||||||
const next = cloneSchema(schema);
|
const next = cloneSchema(schema);
|
||||||
const root = asSchemaObject(next);
|
const root = asSchemaObject(next);
|
||||||
@@ -908,7 +947,10 @@ export function buildConfigSchema(params?: {
|
|||||||
const channels = params?.channels ?? [];
|
const channels = params?.channels ?? [];
|
||||||
if (plugins.length === 0 && channels.length === 0) return base;
|
if (plugins.length === 0 && channels.length === 0) return base;
|
||||||
const mergedHints = applySensitiveHints(
|
const mergedHints = applySensitiveHints(
|
||||||
applyChannelHints(applyPluginHints(base.uiHints, plugins), channels),
|
applyHeartbeatTargetHints(
|
||||||
|
applyChannelHints(applyPluginHints(base.uiHints, plugins), channels),
|
||||||
|
channels,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const mergedSchema = applyChannelSchemas(applyPluginSchemas(base.schema, plugins), channels);
|
const mergedSchema = applyChannelSchemas(applyPluginSchemas(base.schema, plugins), channels);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
HumanDelayConfig,
|
HumanDelayConfig,
|
||||||
TypingMode,
|
TypingMode,
|
||||||
} from "./types.base.js";
|
} from "./types.base.js";
|
||||||
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
import type {
|
import type {
|
||||||
SandboxBrowserSettings,
|
SandboxBrowserSettings,
|
||||||
SandboxDockerSettings,
|
SandboxDockerSettings,
|
||||||
@@ -177,18 +178,8 @@ export type AgentDefaultsConfig = {
|
|||||||
model?: string;
|
model?: string;
|
||||||
/** Session key for heartbeat runs ("main" or explicit session key). */
|
/** Session key for heartbeat runs ("main" or explicit session key). */
|
||||||
session?: string;
|
session?: string;
|
||||||
/** Delivery target (last|whatsapp|telegram|discord|slack|mattermost|msteams|signal|imessage|none). */
|
/** Delivery target ("last", "none", or a channel id). */
|
||||||
target?:
|
target?: "last" | "none" | ChannelId;
|
||||||
| "last"
|
|
||||||
| "whatsapp"
|
|
||||||
| "telegram"
|
|
||||||
| "discord"
|
|
||||||
| "slack"
|
|
||||||
| "mattermost"
|
|
||||||
| "msteams"
|
|
||||||
| "signal"
|
|
||||||
| "imessage"
|
|
||||||
| "none";
|
|
||||||
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
|
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
|
||||||
to?: string;
|
to?: string;
|
||||||
/** Override the heartbeat prompt body (default: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK."). */
|
/** Override the heartbeat prompt body (default: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK."). */
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||||
import { CHANNEL_IDS } from "../channels/registry.js";
|
import { CHANNEL_IDS, normalizeChatChannelId } from "../channels/registry.js";
|
||||||
import {
|
import {
|
||||||
normalizePluginsConfig,
|
normalizePluginsConfig,
|
||||||
resolveEnableState,
|
resolveEnableState,
|
||||||
@@ -226,6 +226,41 @@ export function validateConfigObjectWithPlugins(raw: unknown):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const heartbeatChannelIds = new Set<string>();
|
||||||
|
for (const channelId of CHANNEL_IDS) {
|
||||||
|
heartbeatChannelIds.add(channelId.toLowerCase());
|
||||||
|
}
|
||||||
|
for (const record of registry.plugins) {
|
||||||
|
for (const channelId of record.channels) {
|
||||||
|
const trimmed = channelId.trim();
|
||||||
|
if (trimmed) heartbeatChannelIds.add(trimmed.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateHeartbeatTarget = (target: string | undefined, path: string) => {
|
||||||
|
if (typeof target !== "string") return;
|
||||||
|
const trimmed = target.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
issues.push({ path, message: "heartbeat target must not be empty" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = trimmed.toLowerCase();
|
||||||
|
if (normalized === "last" || normalized === "none") return;
|
||||||
|
if (normalizeChatChannelId(trimmed)) return;
|
||||||
|
if (heartbeatChannelIds.has(normalized)) return;
|
||||||
|
issues.push({ path, message: `unknown heartbeat target: ${target}` });
|
||||||
|
};
|
||||||
|
|
||||||
|
validateHeartbeatTarget(
|
||||||
|
config.agents?.defaults?.heartbeat?.target,
|
||||||
|
"agents.defaults.heartbeat.target",
|
||||||
|
);
|
||||||
|
if (Array.isArray(config.agents?.list)) {
|
||||||
|
for (const [index, entry] of config.agents.list.entries()) {
|
||||||
|
validateHeartbeatTarget(entry?.heartbeat?.target, `agents.list.${index}.heartbeat.target`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let selectedMemoryPluginId: string | null = null;
|
let selectedMemoryPluginId: string | null = null;
|
||||||
const seenPlugins = new Set<string>();
|
const seenPlugins = new Set<string>();
|
||||||
for (const record of registry.plugins) {
|
for (const record of registry.plugins) {
|
||||||
|
|||||||
@@ -22,19 +22,7 @@ export const HeartbeatSchema = z
|
|||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
session: z.string().optional(),
|
session: z.string().optional(),
|
||||||
includeReasoning: z.boolean().optional(),
|
includeReasoning: z.boolean().optional(),
|
||||||
target: z
|
target: z.string().optional(),
|
||||||
.union([
|
|
||||||
z.literal("last"),
|
|
||||||
z.literal("whatsapp"),
|
|
||||||
z.literal("telegram"),
|
|
||||||
z.literal("discord"),
|
|
||||||
z.literal("slack"),
|
|
||||||
z.literal("msteams"),
|
|
||||||
z.literal("signal"),
|
|
||||||
z.literal("imessage"),
|
|
||||||
z.literal("none"),
|
|
||||||
])
|
|
||||||
.optional(),
|
|
||||||
to: z.string().optional(),
|
to: z.string().optional(),
|
||||||
prompt: z.string().optional(),
|
prompt: z.string().optional(),
|
||||||
ackMaxChars: z.number().int().nonnegative().optional(),
|
ackMaxChars: z.number().int().nonnegative().optional(),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ describe("normalizeCronJobCreate", () => {
|
|||||||
expect("provider" in payload).toBe(false);
|
expect("provider" in payload).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes agentId and drops null", () => {
|
it("trims agentId and drops null", () => {
|
||||||
const normalized = normalizeCronJobCreate({
|
const normalized = normalizeCronJobCreate({
|
||||||
name: "agent-set",
|
name: "agent-set",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { sanitizeAgentId } from "../routing/session-key.js";
|
||||||
import { parseAbsoluteTimeMs } from "./parse.js";
|
import { parseAbsoluteTimeMs } from "./parse.js";
|
||||||
import { migrateLegacyCronPayload } from "./payload-migration.js";
|
import { migrateLegacyCronPayload } from "./payload-migration.js";
|
||||||
import type { CronJobCreate, CronJobPatch } from "./types.js";
|
import type { CronJobCreate, CronJobPatch } from "./types.js";
|
||||||
@@ -75,7 +76,7 @@ export function normalizeCronJobInput(
|
|||||||
next.agentId = null;
|
next.agentId = null;
|
||||||
} else if (typeof agentId === "string") {
|
} else if (typeof agentId === "string") {
|
||||||
const trimmed = agentId.trim();
|
const trimmed = agentId.trim();
|
||||||
if (trimmed) next.agentId = trimmed;
|
if (trimmed) next.agentId = sanitizeAgentId(trimmed);
|
||||||
else delete next.agentId;
|
else delete next.agentId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { inspect } from "node:util";
|
||||||
import { Client } from "@buape/carbon";
|
import { Client } from "@buape/carbon";
|
||||||
import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway";
|
import { GatewayIntents, GatewayPlugin } from "@buape/carbon/gateway";
|
||||||
import { Routes } from "discord-api-types/v10";
|
import { Routes } from "discord-api-types/v10";
|
||||||
@@ -95,7 +96,8 @@ function formatDiscordDeployErrorDetails(err: unknown): string {
|
|||||||
try {
|
try {
|
||||||
bodyText = JSON.stringify(rawBody);
|
bodyText = JSON.stringify(rawBody);
|
||||||
} catch {
|
} catch {
|
||||||
bodyText = String(rawBody);
|
bodyText =
|
||||||
|
typeof rawBody === "string" ? rawBody : inspect(rawBody, { depth: 3, breakLength: 120 });
|
||||||
}
|
}
|
||||||
if (bodyText) {
|
if (bodyText) {
|
||||||
const maxLen = 800;
|
const maxLen = 800;
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { stripEnvelopeFromMessage } from "./chat-sanitize.js";
|
||||||
|
|
||||||
|
describe("stripEnvelopeFromMessage", () => {
|
||||||
|
test("removes message_id hint lines from user messages", () => {
|
||||||
|
const input = {
|
||||||
|
role: "user",
|
||||||
|
content: "[WhatsApp 2026-01-24 13:36] yolo\n[message_id: 7b8b]",
|
||||||
|
};
|
||||||
|
const result = stripEnvelopeFromMessage(input) as { content?: string };
|
||||||
|
expect(result.content).toBe("yolo");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removes message_id hint lines from text content arrays", () => {
|
||||||
|
const input = {
|
||||||
|
role: "user",
|
||||||
|
content: [{ type: "text", text: "hi\n[message_id: abc123]" }],
|
||||||
|
};
|
||||||
|
const result = stripEnvelopeFromMessage(input) as {
|
||||||
|
content?: Array<{ type: string; text?: string }>;
|
||||||
|
};
|
||||||
|
expect(result.content?.[0]?.text).toBe("hi");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not strip inline message_id text that is part of a line", () => {
|
||||||
|
const input = {
|
||||||
|
role: "user",
|
||||||
|
content: "I typed [message_id: 123] on purpose",
|
||||||
|
};
|
||||||
|
const result = stripEnvelopeFromMessage(input) as { content?: string };
|
||||||
|
expect(result.content).toBe("I typed [message_id: 123] on purpose");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not strip assistant messages", () => {
|
||||||
|
const input = {
|
||||||
|
role: "assistant",
|
||||||
|
content: "note\n[message_id: 123]",
|
||||||
|
};
|
||||||
|
const result = stripEnvelopeFromMessage(input) as { content?: string };
|
||||||
|
expect(result.content).toBe("note\n[message_id: 123]");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,6 +14,8 @@ const ENVELOPE_CHANNELS = [
|
|||||||
"BlueBubbles",
|
"BlueBubbles",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const MESSAGE_ID_LINE = /^\s*\[message_id:\s*[^\]]+\]\s*$/i;
|
||||||
|
|
||||||
function looksLikeEnvelopeHeader(header: string): boolean {
|
function looksLikeEnvelopeHeader(header: string): boolean {
|
||||||
if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z\b/.test(header)) return true;
|
if (/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z\b/.test(header)) return true;
|
||||||
if (/\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/.test(header)) return true;
|
if (/\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/.test(header)) return true;
|
||||||
@@ -28,13 +30,20 @@ export function stripEnvelope(text: string): string {
|
|||||||
return text.slice(match[0].length);
|
return text.slice(match[0].length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripMessageIdHints(text: string): string {
|
||||||
|
if (!text.includes("[message_id:")) return text;
|
||||||
|
const lines = text.split(/\r?\n/);
|
||||||
|
const filtered = lines.filter((line) => !MESSAGE_ID_LINE.test(line));
|
||||||
|
return filtered.length === lines.length ? text : filtered.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
function stripEnvelopeFromContent(content: unknown[]): { content: unknown[]; changed: boolean } {
|
function stripEnvelopeFromContent(content: unknown[]): { content: unknown[]; changed: boolean } {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
const next = content.map((item) => {
|
const next = content.map((item) => {
|
||||||
if (!item || typeof item !== "object") return item;
|
if (!item || typeof item !== "object") return item;
|
||||||
const entry = item as Record<string, unknown>;
|
const entry = item as Record<string, unknown>;
|
||||||
if (entry.type !== "text" || typeof entry.text !== "string") return item;
|
if (entry.type !== "text" || typeof entry.text !== "string") return item;
|
||||||
const stripped = stripEnvelope(entry.text);
|
const stripped = stripMessageIdHints(stripEnvelope(entry.text));
|
||||||
if (stripped === entry.text) return item;
|
if (stripped === entry.text) return item;
|
||||||
changed = true;
|
changed = true;
|
||||||
return {
|
return {
|
||||||
@@ -55,7 +64,7 @@ export function stripEnvelopeFromMessage(message: unknown): unknown {
|
|||||||
const next: Record<string, unknown> = { ...entry };
|
const next: Record<string, unknown> = { ...entry };
|
||||||
|
|
||||||
if (typeof entry.content === "string") {
|
if (typeof entry.content === "string") {
|
||||||
const stripped = stripEnvelope(entry.content);
|
const stripped = stripMessageIdHints(stripEnvelope(entry.content));
|
||||||
if (stripped !== entry.content) {
|
if (stripped !== entry.content) {
|
||||||
next.content = stripped;
|
next.content = stripped;
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -67,7 +76,7 @@ export function stripEnvelopeFromMessage(message: unknown): unknown {
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
} else if (typeof entry.text === "string") {
|
} else if (typeof entry.text === "string") {
|
||||||
const stripped = stripEnvelope(entry.text);
|
const stripped = stripMessageIdHints(stripEnvelope(entry.text));
|
||||||
if (stripped !== entry.text) {
|
if (stripped !== entry.text) {
|
||||||
next.text = stripped;
|
next.text = stripped;
|
||||||
changed = true;
|
changed = true;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
DEFAULT_ACCOUNT_ID,
|
DEFAULT_ACCOUNT_ID,
|
||||||
DEFAULT_MAIN_KEY,
|
DEFAULT_MAIN_KEY,
|
||||||
normalizeAgentId,
|
normalizeAgentId,
|
||||||
|
sanitizeAgentId,
|
||||||
} from "./session-key.js";
|
} from "./session-key.js";
|
||||||
|
|
||||||
export type RoutePeerKind = "dm" | "group" | "channel";
|
export type RoutePeerKind = "dm" | "group" | "channel";
|
||||||
@@ -93,13 +94,13 @@ function listAgents(cfg: ClawdbotConfig) {
|
|||||||
|
|
||||||
function pickFirstExistingAgentId(cfg: ClawdbotConfig, agentId: string): string {
|
function pickFirstExistingAgentId(cfg: ClawdbotConfig, agentId: string): string {
|
||||||
const trimmed = (agentId ?? "").trim();
|
const trimmed = (agentId ?? "").trim();
|
||||||
if (!trimmed) return normalizeAgentId(resolveDefaultAgentId(cfg));
|
if (!trimmed) return sanitizeAgentId(resolveDefaultAgentId(cfg));
|
||||||
const normalized = normalizeAgentId(trimmed);
|
const normalized = normalizeAgentId(trimmed);
|
||||||
const agents = listAgents(cfg);
|
const agents = listAgents(cfg);
|
||||||
if (agents.length === 0) return trimmed;
|
if (agents.length === 0) return sanitizeAgentId(trimmed);
|
||||||
const match = agents.find((agent) => normalizeAgentId(agent.id) === normalized);
|
const match = agents.find((agent) => normalizeAgentId(agent.id) === normalized);
|
||||||
if (match?.id?.trim()) return match.id.trim();
|
if (match?.id?.trim()) return sanitizeAgentId(match.id.trim());
|
||||||
return normalizeAgentId(resolveDefaultAgentId(cfg));
|
return sanitizeAgentId(resolveDefaultAgentId(cfg));
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesChannel(
|
function matchesChannel(
|
||||||
|
|||||||
@@ -64,6 +64,19 @@ export function normalizeAgentId(value: string | undefined | null): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sanitizeAgentId(value: string | undefined | null): string {
|
||||||
|
const trimmed = (value ?? "").trim();
|
||||||
|
if (!trimmed) return DEFAULT_AGENT_ID;
|
||||||
|
if (/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(trimmed)) return trimmed;
|
||||||
|
return (
|
||||||
|
trimmed
|
||||||
|
.replace(/[^a-z0-9_-]+/gi, "-")
|
||||||
|
.replace(/^-+/, "")
|
||||||
|
.replace(/-+$/, "")
|
||||||
|
.slice(0, 64) || DEFAULT_AGENT_ID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeAccountId(value: string | undefined | null): string {
|
export function normalizeAccountId(value: string | undefined | null): string {
|
||||||
const trimmed = (value ?? "").trim();
|
const trimmed = (value ?? "").trim();
|
||||||
if (!trimmed) return DEFAULT_ACCOUNT_ID;
|
if (!trimmed) return DEFAULT_ACCOUNT_ID;
|
||||||
|
|||||||
Reference in new issue
Block a user