Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6718032fe1 | ||
|
|
664f059ff3 |
No files matched your search
+1
-4
@@ -6,15 +6,12 @@
|
||||
- Node bridge: harden keepalive + heartbeat handling (TCP keepalive, better disconnects, and keepalive config tests). (#577) — thanks @steipete
|
||||
- Control UI: improve mobile responsiveness. (#558) — thanks @carlulsoe
|
||||
- CLI: add `sandbox list` and `sandbox recreate` commands for managing Docker sandbox containers after image/config updates. (#563) — thanks @pasogott
|
||||
- Docs: add Hetzner Docker VPS guide. (#556) — thanks @Iamadig
|
||||
- Docs: link Hetzner guide from install + platforms docs. (#592) — thanks @steipete
|
||||
- Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc
|
||||
- Slack: honor reply tags + replyToMode while keeping threaded replies in-thread. (#574) — thanks @bolismauro
|
||||
- Discord: avoid category parent overrides for channel allowlists and refactor thread context helpers. (#588) — thanks @steipete
|
||||
- Discord: fix forum thread starters and cache channel lookups for thread context. (#585) — thanks @thewilloftheshadow
|
||||
- Commands: accept /models as an alias for /model.
|
||||
- Commands: add `/usage` as an alias for `/status`. (#492) — thanks @lc0rp
|
||||
- Models/Auth: add MiniMax Anthropic-compatible API onboarding (minimax-api). (#590) — thanks @mneves75
|
||||
- Commands: harden slash command registry and list text-only commands in `/commands`.
|
||||
- Models/Auth: show per-agent auth candidates in `/model status`, and add `clawdbot models auth order {get,set,clear}` (per-agent auth rotation overrides). — thanks @steipete
|
||||
- Debugging: add raw model stream logging flags and document gateway watch mode.
|
||||
@@ -25,9 +22,9 @@
|
||||
- Hooks: default hook agent delivery to true. (#533) — thanks @mcinteerj
|
||||
- Hooks: normalize hook agent providers (aliases + msteams support).
|
||||
- WhatsApp: route queued replies to the original sender instead of the bot's own number. (#534) — thanks @mcinteerj
|
||||
- iMessage: isolate group-ish threads by `chat_id` when `is_group=false` to avoid DM session bleed. (#535) — thanks @mdahmann
|
||||
- Models: add OAuth expiry checks in doctor, expanded `models status` auth output (missing auth + `--check` exit codes). (#538) — thanks @latitudeki5223
|
||||
- Deps: bump Pi to 0.40.0 and drop pi-ai patch (upstream 429 fix). (#543) — thanks @mcinteerj
|
||||
- Agent: skip empty error assistant messages when rebuilding session context to avoid tool-chain corruption. (#561) — thanks @mukhtharcm
|
||||
- Security: per-agent mention patterns and group elevated directives now require explicit mention to avoid cross-agent toggles.
|
||||
- Config: support inline env vars in config (`env.*` / `env.vars`) and document env precedence.
|
||||
- Agent: enable adaptive context pruning by default for tool-result trimming.
|
||||
|
||||
@@ -331,8 +331,7 @@ actor MacNodeBridgeSession {
|
||||
let now = self.clock.now
|
||||
if now > last.advanced(by: timeout) {
|
||||
let age = last.duration(to: now)
|
||||
self.logger.warning(
|
||||
"Node bridge heartbeat timed out; disconnecting (age: \(String(describing: age), privacy: .public)).")
|
||||
self.logger.warning("Node bridge heartbeat timed out; disconnecting (age: \(String(describing: age), privacy: .public)).")
|
||||
await self.disconnect()
|
||||
return
|
||||
}
|
||||
@@ -342,8 +341,7 @@ actor MacNodeBridgeSession {
|
||||
do {
|
||||
try await self.send(BridgePing(type: "ping", id: id))
|
||||
} catch {
|
||||
self.logger.warning(
|
||||
"Node bridge ping send failed; disconnecting (error: \(String(describing: error), privacy: .public)).")
|
||||
self.logger.warning("Node bridge ping send failed; disconnecting (error: \(String(describing: error), privacy: .public)).")
|
||||
await self.disconnect()
|
||||
return
|
||||
}
|
||||
@@ -358,8 +356,7 @@ actor MacNodeBridgeSession {
|
||||
private func handleConnectionState(_ state: NWConnection.State) async {
|
||||
switch state {
|
||||
case let .failed(error):
|
||||
self.logger.warning(
|
||||
"Node bridge connection failed; disconnecting (error: \(String(describing: error), privacy: .public)).")
|
||||
self.logger.warning("Node bridge connection failed; disconnecting (error: \(String(describing: error), privacy: .public)).")
|
||||
await self.disconnect()
|
||||
case .cancelled:
|
||||
self.logger.warning("Node bridge connection cancelled; disconnecting.")
|
||||
|
||||
@@ -679,7 +679,6 @@
|
||||
"platforms/android",
|
||||
"platforms/windows",
|
||||
"platforms/linux",
|
||||
"platforms/hetzner",
|
||||
"platforms/exe-dev"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1464,67 +1464,6 @@ Notes:
|
||||
- Responses API enables clean reasoning/output separation; WhatsApp sees only final text.
|
||||
- Adjust `contextWindow`/`maxTokens` if your LM Studio context length differs.
|
||||
|
||||
### MiniMax API (platform.minimax.io)
|
||||
|
||||
Use MiniMax's Anthropic-compatible API directly without LM Studio:
|
||||
|
||||
```json5
|
||||
{
|
||||
agent: {
|
||||
model: { primary: "minimax/MiniMax-M2.1" },
|
||||
models: {
|
||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||||
"minimax/MiniMax-M2.1": { alias: "Minimax" }
|
||||
}
|
||||
},
|
||||
models: {
|
||||
mode: "merge",
|
||||
providers: {
|
||||
minimax: {
|
||||
baseUrl: "https://api.minimax.io/anthropic",
|
||||
apiKey: "${MINIMAX_API_KEY}",
|
||||
api: "anthropic-messages",
|
||||
models: [
|
||||
{
|
||||
id: "MiniMax-M2.1",
|
||||
name: "MiniMax M2.1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
|
||||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192
|
||||
},
|
||||
{
|
||||
id: "MiniMax-M2.1-lightning",
|
||||
name: "MiniMax M2.1 Lightning",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192
|
||||
},
|
||||
{
|
||||
id: "MiniMax-M2",
|
||||
name: "MiniMax M2",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Set `MINIMAX_API_KEY` environment variable or use `clawdbot onboard --auth-choice minimax-api`
|
||||
- Available models: `MiniMax-M2.1` (default), `MiniMax-M2.1-lightning` (~100 tps), `MiniMax-M2` (reasoning)
|
||||
- Pricing is a placeholder; MiniMax doesn't publish public rates. Override in `models.json` for accurate cost tracking.
|
||||
|
||||
Notes:
|
||||
- Supported APIs: `openai-completions`, `openai-responses`, `anthropic-messages`,
|
||||
`google-generative-ai`
|
||||
|
||||
@@ -51,8 +51,6 @@ It writes config/workspace on the host:
|
||||
- `~/.clawdbot/`
|
||||
- `~/clawd`
|
||||
|
||||
Running on a VPS? See [Hetzner (Docker VPS)](/platforms/hetzner).
|
||||
|
||||
### Manual flow (compose)
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
---
|
||||
summary: "Run Clawdbot Gateway on Hetzner (Docker + VPS) with durable state and baked-in binaries"
|
||||
read_when:
|
||||
- You want a production-grade, always-on Gateway on your own VPS
|
||||
- You want full control over persistence, binaries, and restart behavior
|
||||
- You are running Clawdbot in Docker on Hetzner or a similar provider
|
||||
---
|
||||
|
||||
# Clawdbot on Hetzner (Docker, Production VPS Guide)
|
||||
|
||||
## Goal
|
||||
Run a persistent Clawdbot Gateway on a Hetzner VPS using Docker, with durable state, baked-in binaries, and safe restart behavior.
|
||||
|
||||
The Gateway can be accessed via:
|
||||
- SSH port forwarding from your laptop
|
||||
- Direct port exposure if you manage firewalling and tokens yourself
|
||||
|
||||
This guide assumes Ubuntu or Debian on Hetzner.
|
||||
If you are on another Linux VPS, map packages accordingly.
|
||||
For the generic Docker flow, see [Docker](/install/docker).
|
||||
|
||||
---
|
||||
|
||||
## Quick path (experienced operators)
|
||||
|
||||
1) Provision Hetzner VPS
|
||||
2) Install Docker
|
||||
3) Clone Clawdbot repository
|
||||
4) Create persistent host directories
|
||||
5) Configure `.env` and `docker-compose.yml`
|
||||
6) Bake required binaries into the image
|
||||
7) `docker compose up -d`
|
||||
8) Verify persistence and Gateway access
|
||||
|
||||
---
|
||||
|
||||
## What you need
|
||||
|
||||
- Hetzner VPS with root access
|
||||
- SSH access from your laptop
|
||||
- Docker and Docker Compose
|
||||
- Model auth credentials
|
||||
- Optional provider credentials
|
||||
- WhatsApp QR
|
||||
- Telegram bot token
|
||||
- Gmail OAuth
|
||||
|
||||
---
|
||||
|
||||
## 1) Provision the VPS
|
||||
|
||||
Create an Ubuntu or Debian VPS in Hetzner.
|
||||
|
||||
Connect as root:
|
||||
|
||||
```bash
|
||||
ssh root@YOUR_VPS_IP
|
||||
```
|
||||
|
||||
This guide assumes the VPS is stateful.
|
||||
Do not treat it as disposable infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## 2) Install Docker (on the VPS)
|
||||
|
||||
```bash
|
||||
apt-get update
|
||||
apt-get install -y git curl ca-certificates
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3) Clone the Clawdbot repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/clawdbot/clawdbot.git
|
||||
cd clawdbot
|
||||
```
|
||||
|
||||
This guide assumes you will build a custom image to guarantee binary persistence.
|
||||
|
||||
---
|
||||
|
||||
## 4) Create persistent host directories
|
||||
|
||||
Docker containers are ephemeral.
|
||||
All long-lived state must live on the host.
|
||||
|
||||
```bash
|
||||
mkdir -p /root/.clawdbot
|
||||
mkdir -p /root/clawd
|
||||
|
||||
# Set ownership to the container user (uid 1000):
|
||||
chown -R 1000:1000 /root/.clawdbot
|
||||
chown -R 1000:1000 /root/clawd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5) Configure environment variables
|
||||
|
||||
Create `.env` in the repository root.
|
||||
|
||||
```bash
|
||||
CLAWDBOT_IMAGE=clawdbot:latest
|
||||
CLAWDBOT_GATEWAY_TOKEN=change-me-now
|
||||
CLAWDBOT_GATEWAY_BIND=lan
|
||||
CLAWDBOT_GATEWAY_PORT=18789
|
||||
CLAWDBOT_BRIDGE_PORT=18790
|
||||
|
||||
CLAWDBOT_CONFIG_DIR=/root/.clawdbot
|
||||
CLAWDBOT_WORKSPACE_DIR=/root/clawd
|
||||
|
||||
GOG_KEYRING_PASSWORD=change-me-now
|
||||
XDG_CONFIG_HOME=/home/node/.clawdbot
|
||||
```
|
||||
|
||||
**Do not commit this file.**
|
||||
|
||||
---
|
||||
|
||||
## 6) Docker Compose configuration
|
||||
|
||||
Create or update `docker-compose.yml`.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
clawdbot-gateway:
|
||||
image: ${CLAWDBOT_IMAGE}
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- HOME=/home/node
|
||||
- NODE_ENV=production
|
||||
- TERM=xterm-256color
|
||||
- CLAWDBOT_GATEWAY_BIND=${CLAWDBOT_GATEWAY_BIND}
|
||||
- CLAWDBOT_GATEWAY_PORT=${CLAWDBOT_GATEWAY_PORT}
|
||||
- CLAWDBOT_BRIDGE_PORT=${CLAWDBOT_BRIDGE_PORT}
|
||||
- CLAWDBOT_GATEWAY_TOKEN=${CLAWDBOT_GATEWAY_TOKEN}
|
||||
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
|
||||
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
|
||||
- PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
volumes:
|
||||
- ${CLAWDBOT_CONFIG_DIR}:/home/node/.clawdbot
|
||||
- ${CLAWDBOT_WORKSPACE_DIR}:/home/node/clawd
|
||||
ports:
|
||||
- "${CLAWDBOT_GATEWAY_PORT}:18789"
|
||||
- "${CLAWDBOT_BRIDGE_PORT}:18790"
|
||||
- "18793:18793"
|
||||
command:
|
||||
[
|
||||
"node",
|
||||
"dist/index.js",
|
||||
"gateway-daemon",
|
||||
"--bind",
|
||||
"${CLAWDBOT_GATEWAY_BIND}",
|
||||
"--port",
|
||||
"${CLAWDBOT_GATEWAY_PORT}"
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7) Bake required binaries into the image (critical)
|
||||
|
||||
Installing binaries inside a running container is a trap.
|
||||
Anything installed at runtime will be lost on restart.
|
||||
|
||||
All external binaries required by skills must be installed at image build time.
|
||||
|
||||
The examples below show three common binaries only:
|
||||
- `gog` for Gmail access
|
||||
- `goplaces` for Google Places
|
||||
- `wacli` for WhatsApp
|
||||
|
||||
These are examples, not a complete list.
|
||||
You may install as many binaries as needed using the same pattern.
|
||||
|
||||
If you add new skills later that depend on additional binaries, you must:
|
||||
1. Update the Dockerfile
|
||||
2. Rebuild the image
|
||||
3. Restart the containers
|
||||
|
||||
**Example Dockerfile**
|
||||
|
||||
```dockerfile
|
||||
FROM node:22-bookworm
|
||||
|
||||
RUN apt-get update && apt-get install -y socat && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Example binary 1: Gmail CLI
|
||||
RUN curl -L https://github.com/steipete/gog/releases/latest/download/gog_Linux_x86_64.tar.gz \
|
||||
| tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/gog
|
||||
|
||||
# Example binary 2: Google Places CLI
|
||||
RUN curl -L https://github.com/steipete/goplaces/releases/latest/download/goplaces_Linux_x86_64.tar.gz \
|
||||
| tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/goplaces
|
||||
|
||||
# Example binary 3: WhatsApp CLI
|
||||
RUN curl -L https://github.com/steipete/wacli/releases/latest/download/wacli_Linux_x86_64.tar.gz \
|
||||
| tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/wacli
|
||||
|
||||
# Add more binaries below using the same pattern
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN corepack enable
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm build
|
||||
RUN pnpm ui:install
|
||||
RUN pnpm ui:build
|
||||
|
||||
CMD ["node","dist/index.js"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8) Build and launch
|
||||
|
||||
```bash
|
||||
docker compose build
|
||||
docker compose up -d clawdbot-gateway
|
||||
```
|
||||
|
||||
Verify binaries:
|
||||
|
||||
```bash
|
||||
docker compose exec clawdbot-gateway which gog
|
||||
docker compose exec clawdbot-gateway which goplaces
|
||||
docker compose exec clawdbot-gateway which wacli
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```
|
||||
/usr/local/bin/gog
|
||||
/usr/local/bin/goplaces
|
||||
/usr/local/bin/wacli
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9) Verify Gateway
|
||||
|
||||
```bash
|
||||
docker compose logs -f clawdbot-gateway
|
||||
```
|
||||
|
||||
Success:
|
||||
|
||||
```
|
||||
[gateway] listening on ws://0.0.0.0:18789
|
||||
```
|
||||
|
||||
From your laptop:
|
||||
|
||||
```bash
|
||||
ssh -N -L 18789:127.0.0.1:18789 root@YOUR_VPS_IP
|
||||
```
|
||||
|
||||
Open:
|
||||
|
||||
`http://127.0.0.1:18789/`
|
||||
|
||||
Paste your gateway token.
|
||||
|
||||
---
|
||||
|
||||
## What persists where (source of truth)
|
||||
|
||||
Clawdbot runs in Docker, but Docker is not the source of truth.
|
||||
All long-lived state must survive restarts, rebuilds, and reboots.
|
||||
|
||||
| Component | Location | Persistence mechanism | Notes |
|
||||
|---|---|---|---|
|
||||
| Gateway config | `/home/node/.clawdbot/` | Host volume mount | Includes `clawdbot.json`, tokens |
|
||||
| Model auth profiles | `/home/node/.clawdbot/` | Host volume mount | OAuth tokens, API keys |
|
||||
| Skill configs | `/home/node/.clawdbot/skills/` | Host volume mount | Skill-level state |
|
||||
| Agent workspace | `/home/node/clawd/` | Host volume mount | Code and agent artifacts |
|
||||
| WhatsApp session | `/home/node/.clawdbot/` | Host volume mount | Preserves QR login |
|
||||
| Gmail keyring | `/home/node/.clawdbot/` | Host volume + password | Requires `GOG_KEYRING_PASSWORD` |
|
||||
| External binaries | `/usr/local/bin/` | Docker image | Must be baked at build time |
|
||||
| Node runtime | Container filesystem | Docker image | Rebuilt every image build |
|
||||
| OS packages | Container filesystem | Docker image | Do not install at runtime |
|
||||
| Docker container | Ephemeral | Restartable | Safe to destroy |
|
||||
@@ -20,11 +20,6 @@ Native companion apps for Windows are also planned; the Gateway is recommended v
|
||||
- Windows: [Windows](/platforms/windows)
|
||||
- Linux: [Linux](/platforms/linux)
|
||||
|
||||
## VPS & hosting
|
||||
|
||||
- Hetzner (Docker): [Hetzner](/platforms/hetzner)
|
||||
- exe.dev (VM + HTTPS proxy): [exe.dev](/platforms/exe-dev)
|
||||
|
||||
## Common links
|
||||
|
||||
- Install guide: [Getting Started](/start/getting-started)
|
||||
|
||||
+71
-23
@@ -13,6 +13,7 @@ Status: external CLI integration. Gateway spawns `imsg rpc` (JSON-RPC over stdio
|
||||
- iMessage provider backed by `imsg` on macOS.
|
||||
- Deterministic routing: replies always go back to iMessage.
|
||||
- DMs share the agent's main session; groups are isolated (`imessage:group:<chat_id>`).
|
||||
- If a multi-participant thread arrives with `is_group=false`, you can still isolate it by `chat_id` using `imessage.groups` (see “Group-ish threads” below).
|
||||
|
||||
## Requirements
|
||||
- macOS with Messages signed in.
|
||||
@@ -24,35 +25,61 @@ Status: external CLI integration. Gateway spawns `imsg rpc` (JSON-RPC over stdio
|
||||
1) Ensure Messages is signed in on this Mac.
|
||||
2) Configure iMessage and start the gateway.
|
||||
|
||||
### Remote/SSH variant (optional)
|
||||
If you want iMessage on another Mac, set `imessage.cliPath` to a wrapper that
|
||||
execs `ssh` and runs `imsg rpc` on the remote host. Clawdbot only needs a
|
||||
stdio stream; `imsg` still runs on the remote macOS host.
|
||||
### Dedicated bot macOS user (for isolated identity)
|
||||
If you want the bot to send from a **separate iMessage identity** (and keep your personal Messages clean), use a dedicated Apple ID + a dedicated macOS user.
|
||||
|
||||
Example wrapper (save somewhere in your PATH and `chmod +x`):
|
||||
1) Create a dedicated Apple ID (example: `my-cool-bot@icloud.com`).
|
||||
- Apple may require a phone number for verification / 2FA.
|
||||
2) Create a macOS user (example: `clawdshome`) and sign into it.
|
||||
3) Open Messages in that macOS user and sign into iMessage using the bot Apple ID.
|
||||
4) Enable Remote Login (System Settings → General → Sharing → Remote Login).
|
||||
5) Install `imsg`:
|
||||
- `brew install steipete/tap/imsg`
|
||||
6) Set up SSH so `ssh <bot-macos-user>@localhost true` works without a password.
|
||||
7) Point `imessage.accounts.bot.cliPath` at an SSH wrapper that runs `imsg` as the bot user.
|
||||
|
||||
First-run note: sending/receiving may require GUI approvals (Automation + Full Disk Access) in the *bot macOS user*. If `imsg rpc` looks stuck or exits, log into that user (Screen Sharing helps), run a one-time `imsg chats --limit 1` / `imsg send ...`, approve prompts, then retry.
|
||||
|
||||
Example wrapper (`chmod +x`). Replace `<bot-macos-user>` with your actual macOS username:
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Run an interactive SSH once first to accept host keys:
|
||||
# ssh <bot-macos-user>@localhost true
|
||||
exec /usr/bin/ssh -o BatchMode=yes -o ConnectTimeout=5 -T <bot-macos-user>@localhost \
|
||||
"/usr/local/bin/imsg" "$@"
|
||||
```
|
||||
|
||||
Example config:
|
||||
```json5
|
||||
{
|
||||
imessage: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
bot: {
|
||||
name: "Bot",
|
||||
enabled: true,
|
||||
cliPath: "/path/to/imsg-bot",
|
||||
dbPath: "/Users/<bot-macos-user>/Library/Messages/chat.db"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For single-account setups, use flat options (`imessage.cliPath`, `imessage.dbPath`) instead of the `accounts` map.
|
||||
|
||||
### Remote/SSH variant (optional)
|
||||
If you want iMessage on another Mac, set `imessage.cliPath` to a wrapper that runs `imsg` on the remote macOS host over SSH. Clawdbot only needs stdio.
|
||||
|
||||
Example wrapper:
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
exec ssh -T mac-mini imsg "$@"
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Remote Mac must have Messages signed in and `imsg` installed.
|
||||
- Full Disk Access + Automation prompts happen on the remote Mac.
|
||||
- Use SSH keys (no password prompt) so the gateway can launch `imsg rpc` unattended.
|
||||
|
||||
Example:
|
||||
```json5
|
||||
{
|
||||
imessage: {
|
||||
enabled: true,
|
||||
cliPath: "/usr/local/bin/imessage-remote",
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["+15555550123"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Multi-account support: use `imessage.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
|
||||
Multi-account support: use `imessage.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. Don’t commit `~/.clawdbot/clawdbot.json` (it often contains tokens).
|
||||
|
||||
## Access control (DMs + groups)
|
||||
DMs:
|
||||
@@ -73,6 +100,27 @@ Groups:
|
||||
- `imsg` streams message events; the gateway normalizes them into the shared provider envelope.
|
||||
- Replies always route back to the same chat id or handle.
|
||||
|
||||
## Group-ish threads (`is_group=false`)
|
||||
Some iMessage threads can have multiple participants but still arrive with `is_group=false` depending on how Messages stores the chat identifier.
|
||||
|
||||
If you explicitly configure a `chat_id` under `imessage.groups`, Clawdbot treats that thread as a “group” for:
|
||||
- session isolation (separate `imessage:group:<chat_id>` session key)
|
||||
- group allowlisting / mention gating behavior
|
||||
|
||||
Example:
|
||||
```json5
|
||||
{
|
||||
imessage: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123"],
|
||||
groups: {
|
||||
"42": { "requireMention": false }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
This is useful when you want an isolated personality/model for a specific thread (see [Multi-agent routing](/concepts/multi-agent)). For filesystem isolation, see [Sandboxing](/gateway/sandboxing).
|
||||
|
||||
## Media + limits
|
||||
- Optional attachment ingestion via `imessage.includeAttachments`.
|
||||
- Media cap via `imessage.mediaMaxMb`.
|
||||
|
||||
+1
-4
@@ -170,9 +170,7 @@
|
||||
"@sinclair/typebox": "0.34.47"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"@mariozechner/pi-agent-core": "patches/@mariozechner__pi-agent-core.patch",
|
||||
"@mariozechner/pi-coding-agent": "patches/@mariozechner__pi-coding-agent.patch",
|
||||
"@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch"
|
||||
"@mariozechner/pi-agent-core": "patches/@mariozechner__pi-agent-core.patch"
|
||||
}
|
||||
},
|
||||
"vitest": {
|
||||
@@ -210,7 +208,6 @@
|
||||
"patchedDependencies": {
|
||||
"@mariozechner/pi-agent-core": "patches/@mariozechner__pi-agent-core.patch",
|
||||
"@mariozechner/pi-coding-agent": "patches/@mariozechner__pi-coding-agent.patch",
|
||||
"@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch",
|
||||
"qrcode-terminal": "patches/qrcode-terminal.patch",
|
||||
"playwright-core@1.57.0": "patches/playwright-core@1.57.0.patch"
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js
|
||||
index 0000000..1111111 100644
|
||||
--- a/dist/providers/google-gemini-cli.js
|
||||
+++ b/dist/providers/google-gemini-cli.js
|
||||
@@ -248,6 +248,12 @@ async function* streamGeminiCli(model, context, credentials, options) {
|
||||
break; // Success, exit retry loop
|
||||
}
|
||||
const errorText = await response.text();
|
||||
+ // PATCH: Fail immediately on 429 for Antigravity to let caller rotate accounts.
|
||||
+ // Antigravity rate limits can have very long retry delays (10+ minutes).
|
||||
+ if (isAntigravity && response.status === 429) {
|
||||
+ console.log(`[pi-ai] 429 rate limit - failing fast to rotate account`);
|
||||
+ throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`);
|
||||
+ }
|
||||
// Check if retryable
|
||||
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
|
||||
// Use server-provided delay or exponential backoff
|
||||
@@ -26,23 +26,3 @@ index 0000000..1111111 100644
|
||||
}
|
||||
/** Result from createAgentSession */
|
||||
export interface CreateAgentSessionResult {
|
||||
diff --git a/dist/core/session-manager.js b/dist/core/session-manager.js
|
||||
index b2aba5280d002253b0938b75aedbb9e6e6c4dcf8..67464efff535dbd7a8e6ed825aab2b305ca2aee2 100644
|
||||
--- a/dist/core/session-manager.js
|
||||
+++ b/dist/core/session-manager.js
|
||||
@@ -161,6 +161,15 @@ export function buildSessionContext(entries, leafId, byId) {
|
||||
const messages = [];
|
||||
const appendMessage = (entry) => {
|
||||
if (entry.type === "message") {
|
||||
+ // PATCH: Filter out empty error assistant messages to prevent session corruption
|
||||
+ // When 429/500 errors occur during tool execution, empty error messages get persisted
|
||||
+ // to the session file. These break the tool_use -> tool_result chain for Claude/Gemini.
|
||||
+ const msg = entry.message;
|
||||
+ if (msg.role === "assistant" &&
|
||||
+ msg.stopReason === "error" &&
|
||||
+ (!msg.content || msg.content.length === 0)) {
|
||||
+ return; // Skip empty error messages
|
||||
+ }
|
||||
messages.push(entry.message);
|
||||
}
|
||||
else if (entry.type === "custom_message") {
|
||||
Generated
+6
-12
@@ -11,12 +11,6 @@ patchedDependencies:
|
||||
'@mariozechner/pi-agent-core':
|
||||
hash: 01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4
|
||||
path: patches/@mariozechner__pi-agent-core.patch
|
||||
'@mariozechner/pi-ai':
|
||||
hash: 3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c
|
||||
path: patches/@mariozechner__pi-ai.patch
|
||||
'@mariozechner/pi-coding-agent':
|
||||
hash: 58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e
|
||||
path: patches/@mariozechner__pi-coding-agent.patch
|
||||
|
||||
importers:
|
||||
|
||||
@@ -42,10 +36,10 @@ importers:
|
||||
version: 0.41.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
|
||||
'@mariozechner/pi-ai':
|
||||
specifier: ^0.41.0
|
||||
version: 0.41.0(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5)
|
||||
version: 0.41.0(ws@8.19.0)(zod@4.3.5)
|
||||
'@mariozechner/pi-coding-agent':
|
||||
specifier: ^0.41.0
|
||||
version: 0.41.0(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5)
|
||||
version: 0.41.0(ws@8.19.0)(zod@4.3.5)
|
||||
'@mariozechner/pi-tui':
|
||||
specifier: ^0.41.0
|
||||
version: 0.41.0
|
||||
@@ -3783,7 +3777,7 @@ snapshots:
|
||||
|
||||
'@mariozechner/pi-agent-core@0.41.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)':
|
||||
dependencies:
|
||||
'@mariozechner/pi-ai': 0.41.0(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5)
|
||||
'@mariozechner/pi-ai': 0.41.0(ws@8.19.0)(zod@4.3.5)
|
||||
'@mariozechner/pi-tui': 0.41.0
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
@@ -3793,7 +3787,7 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@mariozechner/pi-ai@0.41.0(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5)':
|
||||
'@mariozechner/pi-ai@0.41.0(ws@8.19.0)(zod@4.3.5)':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
|
||||
'@google/genai': 1.34.0
|
||||
@@ -3813,11 +3807,11 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@mariozechner/pi-coding-agent@0.41.0(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5)':
|
||||
'@mariozechner/pi-coding-agent@0.41.0(ws@8.19.0)(zod@4.3.5)':
|
||||
dependencies:
|
||||
'@mariozechner/clipboard': 0.3.0
|
||||
'@mariozechner/pi-agent-core': 0.41.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
|
||||
'@mariozechner/pi-ai': 0.41.0(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5)
|
||||
'@mariozechner/pi-ai': 0.41.0(ws@8.19.0)(zod@4.3.5)
|
||||
'@mariozechner/pi-tui': 0.41.0
|
||||
chalk: 5.6.2
|
||||
cli-highlight: 2.1.11
|
||||
|
||||
@@ -11,7 +11,6 @@ echo "Running doctor install switch E2E..."
|
||||
docker run --rm -t "$IMAGE_NAME" bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
# Stub systemd/loginctl so doctor + daemon flows work in Docker.
|
||||
export PATH="/tmp/clawdbot-bin:$PATH"
|
||||
mkdir -p /tmp/clawdbot-bin
|
||||
|
||||
@@ -66,7 +65,6 @@ exit 0
|
||||
LOGINCTL
|
||||
chmod +x /tmp/clawdbot-bin/loginctl
|
||||
|
||||
# Install the npm-global variant from the local /app source.
|
||||
npm install -g --prefix /tmp/npm-prefix /app
|
||||
|
||||
npm_bin="/tmp/npm-prefix/bin/clawdbot"
|
||||
@@ -90,8 +88,6 @@ LOGINCTL
|
||||
fi
|
||||
}
|
||||
|
||||
# Each flow: install service with one variant, run doctor from the other,
|
||||
# and verify ExecStart entrypoint switches accordingly.
|
||||
run_flow() {
|
||||
local name="$1"
|
||||
local install_cmd="$2"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
const MINIMAX_KEY = process.env.MINIMAX_API_KEY ?? "";
|
||||
const MINIMAX_BASE_URL =
|
||||
process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/anthropic";
|
||||
process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/v1";
|
||||
const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.1";
|
||||
const LIVE = process.env.MINIMAX_LIVE_TEST === "1" || process.env.LIVE === "1";
|
||||
|
||||
@@ -11,16 +11,15 @@ const describeLive = LIVE && MINIMAX_KEY ? describe : describe.skip;
|
||||
|
||||
describeLive("minimax live", () => {
|
||||
it("returns assistant text", async () => {
|
||||
const model: Model<"anthropic-messages"> = {
|
||||
const model: Model<"openai-completions"> = {
|
||||
id: MINIMAX_MODEL,
|
||||
name: `MiniMax ${MINIMAX_MODEL}`,
|
||||
api: "anthropic-messages",
|
||||
api: "openai-completions",
|
||||
provider: "minimax",
|
||||
baseUrl: MINIMAX_BASE_URL,
|
||||
reasoning: MINIMAX_MODEL === "MiniMax-M2",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
// Pricing: placeholder values (per 1M tokens, multiplied by 1000 for display)
|
||||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192,
|
||||
};
|
||||
|
||||
@@ -101,10 +101,6 @@ export function buildAuthChoiceOptions(params: {
|
||||
// Token flow is currently Anthropic-only; use CLI for advanced providers.
|
||||
options.push({ value: "minimax-cloud", label: "MiniMax M2.1 (minimax.io)" });
|
||||
options.push({ value: "minimax", label: "Minimax M2.1 (LM Studio)" });
|
||||
options.push({
|
||||
value: "minimax-api",
|
||||
label: "MiniMax API (platform.minimax.io)",
|
||||
});
|
||||
if (params.includeSkip) {
|
||||
options.push({ value: "skip", label: "Skip for now" });
|
||||
}
|
||||
|
||||
@@ -36,8 +36,6 @@ import {
|
||||
} from "./google-gemini-model-default.js";
|
||||
import {
|
||||
applyAuthProfileConfig,
|
||||
applyMinimaxApiConfig,
|
||||
applyMinimaxApiProviderConfig,
|
||||
applyMinimaxConfig,
|
||||
applyMinimaxHostedConfig,
|
||||
applyMinimaxHostedProviderConfig,
|
||||
@@ -631,24 +629,6 @@ export async function applyAuthChoice(params: {
|
||||
agentModelOverride = "lmstudio/minimax-m2.1-gs32";
|
||||
await noteAgentModel("lmstudio/minimax-m2.1-gs32");
|
||||
}
|
||||
} else if (params.authChoice === "minimax-api") {
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter MiniMax API key",
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
});
|
||||
await setMinimaxApiKey(String(key).trim(), params.agentDir);
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "minimax:default",
|
||||
provider: "minimax",
|
||||
mode: "api_key",
|
||||
});
|
||||
if (params.setDefaultModel) {
|
||||
nextConfig = applyMinimaxApiConfig(nextConfig);
|
||||
} else {
|
||||
nextConfig = applyMinimaxApiProviderConfig(nextConfig);
|
||||
agentModelOverride = "minimax/MiniMax-M2.1";
|
||||
await noteAgentModel("minimax/MiniMax-M2.1");
|
||||
}
|
||||
}
|
||||
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
|
||||
@@ -7,8 +7,6 @@ import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
applyAuthProfileConfig,
|
||||
applyMinimaxApiConfig,
|
||||
applyMinimaxApiProviderConfig,
|
||||
writeOAuthCredentials,
|
||||
} from "./onboard-auth.js";
|
||||
|
||||
@@ -107,146 +105,3 @@ describe("applyAuthProfileConfig", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyMinimaxApiConfig", () => {
|
||||
it("adds minimax provider with correct settings", () => {
|
||||
const cfg = applyMinimaxApiConfig({});
|
||||
expect(cfg.models?.providers?.minimax).toMatchObject({
|
||||
baseUrl: "https://api.minimax.io/anthropic",
|
||||
api: "anthropic-messages",
|
||||
});
|
||||
});
|
||||
|
||||
it("sets correct primary model", () => {
|
||||
const cfg = applyMinimaxApiConfig({}, "MiniMax-M2.1-lightning");
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe(
|
||||
"minimax/MiniMax-M2.1-lightning",
|
||||
);
|
||||
});
|
||||
|
||||
it("sets reasoning flag for MiniMax-M2 model", () => {
|
||||
const cfg = applyMinimaxApiConfig({}, "MiniMax-M2");
|
||||
expect(cfg.models?.providers?.minimax?.models[0]?.reasoning).toBe(true);
|
||||
});
|
||||
|
||||
it("does not set reasoning for non-M2 models", () => {
|
||||
const cfg = applyMinimaxApiConfig({}, "MiniMax-M2.1");
|
||||
expect(cfg.models?.providers?.minimax?.models[0]?.reasoning).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves existing model fallbacks", () => {
|
||||
const cfg = applyMinimaxApiConfig({
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { fallbacks: ["anthropic/claude-opus-4-5"] },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(cfg.agents?.defaults?.model?.fallbacks).toEqual([
|
||||
"anthropic/claude-opus-4-5",
|
||||
]);
|
||||
});
|
||||
|
||||
it("adds model alias", () => {
|
||||
const cfg = applyMinimaxApiConfig({}, "MiniMax-M2.1");
|
||||
expect(cfg.agents?.defaults?.models?.["minimax/MiniMax-M2.1"]?.alias).toBe(
|
||||
"Minimax",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves existing model params when adding alias", () => {
|
||||
const cfg = applyMinimaxApiConfig(
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"minimax/MiniMax-M2.1": {
|
||||
alias: "MiniMax",
|
||||
params: { custom: "value" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"MiniMax-M2.1",
|
||||
);
|
||||
expect(
|
||||
cfg.agents?.defaults?.models?.["minimax/MiniMax-M2.1"],
|
||||
).toMatchObject({ alias: "Minimax", params: { custom: "value" } });
|
||||
});
|
||||
|
||||
it("replaces existing minimax provider entirely", () => {
|
||||
const cfg = applyMinimaxApiConfig({
|
||||
models: {
|
||||
providers: {
|
||||
minimax: {
|
||||
baseUrl: "https://old.example.com",
|
||||
apiKey: "old-key",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "old-model",
|
||||
name: "Old",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1000,
|
||||
maxTokens: 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(cfg.models?.providers?.minimax?.baseUrl).toBe(
|
||||
"https://api.minimax.io/anthropic",
|
||||
);
|
||||
expect(cfg.models?.providers?.minimax?.api).toBe("anthropic-messages");
|
||||
expect(cfg.models?.providers?.minimax?.models[0]?.id).toBe("MiniMax-M2.1");
|
||||
});
|
||||
|
||||
it("preserves other providers when adding minimax", () => {
|
||||
const cfg = applyMinimaxApiConfig({
|
||||
models: {
|
||||
providers: {
|
||||
anthropic: {
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
apiKey: "anthropic-key",
|
||||
api: "anthropic-messages",
|
||||
models: [
|
||||
{
|
||||
id: "claude-opus-4-5",
|
||||
name: "Claude Opus 4.5",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 15, output: 75, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(cfg.models?.providers?.anthropic).toBeDefined();
|
||||
expect(cfg.models?.providers?.minimax).toBeDefined();
|
||||
});
|
||||
|
||||
it("preserves existing models mode", () => {
|
||||
const cfg = applyMinimaxApiConfig({
|
||||
models: { mode: "replace", providers: {} },
|
||||
});
|
||||
expect(cfg.models?.mode).toBe("replace");
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyMinimaxApiProviderConfig", () => {
|
||||
it("does not overwrite existing primary model", () => {
|
||||
const cfg = applyMinimaxApiProviderConfig({
|
||||
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } },
|
||||
});
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe(
|
||||
"anthropic/claude-opus-4-5",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -263,73 +263,3 @@ export function applyMinimaxHostedConfig(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// MiniMax Anthropic-compatible API (platform.minimax.io/anthropic)
|
||||
export function applyMinimaxApiProviderConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
modelId: string = "MiniMax-M2.1",
|
||||
): ClawdbotConfig {
|
||||
const providers = { ...cfg.models?.providers };
|
||||
providers.minimax = {
|
||||
baseUrl: "https://api.minimax.io/anthropic",
|
||||
apiKey: "", // Resolved via MINIMAX_API_KEY env var or auth profile
|
||||
api: "anthropic-messages",
|
||||
models: [
|
||||
{
|
||||
id: modelId,
|
||||
name: `MiniMax ${modelId}`,
|
||||
reasoning: modelId === "MiniMax-M2",
|
||||
input: ["text"],
|
||||
// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
|
||||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
||||
contextWindow: 200000,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[`minimax/${modelId}`] = {
|
||||
...models[`minimax/${modelId}`],
|
||||
alias: "Minimax",
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: { mode: cfg.models?.mode ?? "merge", providers },
|
||||
};
|
||||
}
|
||||
|
||||
export function applyMinimaxApiConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
modelId: string = "MiniMax-M2.1",
|
||||
): ClawdbotConfig {
|
||||
const next = applyMinimaxApiProviderConfig(cfg, modelId);
|
||||
return {
|
||||
...next,
|
||||
agents: {
|
||||
...next.agents,
|
||||
defaults: {
|
||||
...next.agents?.defaults,
|
||||
model: {
|
||||
...(next.agents?.defaults?.model &&
|
||||
"fallbacks" in (next.agents.defaults.model as Record<string, unknown>)
|
||||
? {
|
||||
fallbacks: (
|
||||
next.agents.defaults.model as { fallbacks?: string[] }
|
||||
).fallbacks,
|
||||
}
|
||||
: undefined),
|
||||
primary: `minimax/${modelId}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import {
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
ensureAuthProfileStore,
|
||||
upsertAuthProfile,
|
||||
} from "../agents/auth-profiles.js";
|
||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { parseDurationMs } from "../cli/parse-duration.js";
|
||||
import {
|
||||
type ClawdbotConfig,
|
||||
CONFIG_PATH_CLAWDBOT,
|
||||
@@ -29,7 +33,6 @@ import { applyGoogleGeminiModelDefault } from "./google-gemini-model-default.js"
|
||||
import { healthCommand } from "./health.js";
|
||||
import {
|
||||
applyAuthProfileConfig,
|
||||
applyMinimaxApiConfig,
|
||||
applyMinimaxConfig,
|
||||
applyMinimaxHostedConfig,
|
||||
setAnthropicApiKey,
|
||||
@@ -174,21 +177,6 @@ export async function runNonInteractiveOnboarding(
|
||||
mode: "api_key",
|
||||
});
|
||||
nextConfig = applyMinimaxHostedConfig(nextConfig);
|
||||
} else if (authChoice === "minimax-api") {
|
||||
const key =
|
||||
opts.minimaxApiKey?.trim() || resolveEnvApiKey("minimax")?.apiKey;
|
||||
if (!key) {
|
||||
runtime.error("Missing --minimax-api-key (or MINIMAX_API_KEY in env).");
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
await setMinimaxApiKey(key);
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "minimax:default",
|
||||
provider: "minimax",
|
||||
mode: "api_key",
|
||||
});
|
||||
nextConfig = applyMinimaxApiConfig(nextConfig);
|
||||
} else if (authChoice === "claude-cli") {
|
||||
const store = ensureAuthProfileStore(undefined, {
|
||||
allowKeychainPrompt: false,
|
||||
@@ -222,18 +210,82 @@ export async function runNonInteractiveOnboarding(
|
||||
nextConfig = applyOpenAICodexModelDefault(nextConfig).next;
|
||||
} else if (authChoice === "minimax") {
|
||||
nextConfig = applyMinimaxConfig(nextConfig);
|
||||
} else if (
|
||||
authChoice === "token" ||
|
||||
authChoice === "oauth" ||
|
||||
authChoice === "openai-codex" ||
|
||||
authChoice === "antigravity"
|
||||
) {
|
||||
} else if (authChoice === "setup-token" || authChoice === "oauth") {
|
||||
if (!process.stdin.isTTY) {
|
||||
runtime.error("`claude setup-token` requires an interactive TTY.");
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = spawnSync("claude", ["setup-token"], { stdio: "inherit" });
|
||||
if (res.error) throw res.error;
|
||||
if (typeof res.status === "number" && res.status !== 0) {
|
||||
runtime.error(`claude setup-token failed (exit ${res.status})`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const store = ensureAuthProfileStore(undefined, {
|
||||
allowKeychainPrompt: true,
|
||||
});
|
||||
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
|
||||
runtime.error(
|
||||
`No Claude CLI credentials found after setup-token. Expected auth profile ${CLAUDE_CLI_PROFILE_ID}.`,
|
||||
);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: CLAUDE_CLI_PROFILE_ID,
|
||||
provider: "anthropic",
|
||||
mode: "token",
|
||||
});
|
||||
} else if (authChoice === "token") {
|
||||
const providerRaw = opts.tokenProvider?.trim();
|
||||
const tokenRaw = opts.token?.trim();
|
||||
if (!providerRaw) {
|
||||
runtime.error(
|
||||
"Missing --token-provider (required for --auth-choice token).",
|
||||
);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (!tokenRaw) {
|
||||
runtime.error("Missing --token (required for --auth-choice token).");
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = normalizeProviderId(providerRaw);
|
||||
const profileId = (
|
||||
opts.tokenProfileId?.trim() || `${provider}:manual`
|
||||
).trim();
|
||||
const expires =
|
||||
opts.tokenExpiresIn?.trim() && opts.tokenExpiresIn.trim().length > 0
|
||||
? Date.now() +
|
||||
parseDurationMs(String(opts.tokenExpiresIn).trim(), {
|
||||
defaultUnit: "d",
|
||||
})
|
||||
: undefined;
|
||||
|
||||
upsertAuthProfile({
|
||||
profileId,
|
||||
credential: {
|
||||
type: "token",
|
||||
provider,
|
||||
token: tokenRaw,
|
||||
...(expires ? { expires } : {}),
|
||||
},
|
||||
});
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId,
|
||||
provider,
|
||||
mode: "token",
|
||||
});
|
||||
} else if (authChoice === "openai-codex" || authChoice === "antigravity") {
|
||||
const label =
|
||||
authChoice === "antigravity"
|
||||
? "Antigravity"
|
||||
: authChoice === "token"
|
||||
? "Token"
|
||||
: "OAuth";
|
||||
authChoice === "antigravity" ? "Antigravity" : "OpenAI Codex OAuth";
|
||||
runtime.error(`${label} requires interactive mode.`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
|
||||
@@ -16,7 +16,6 @@ export type AuthChoice =
|
||||
| "gemini-api-key"
|
||||
| "minimax-cloud"
|
||||
| "minimax"
|
||||
| "minimax-api"
|
||||
| "skip";
|
||||
export type GatewayAuthChoice = "off" | "token" | "password";
|
||||
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
||||
|
||||
@@ -215,6 +215,46 @@ describe("monitorIMessageProvider", () => {
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats configured chat_id as a group session even when is_group is false", async () => {
|
||||
config = {
|
||||
...config,
|
||||
imessage: {
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
groups: { "2": { requireMention: false } },
|
||||
},
|
||||
};
|
||||
|
||||
const run = monitorIMessageProvider();
|
||||
await waitForSubscribe();
|
||||
|
||||
notificationHandler?.({
|
||||
method: "message",
|
||||
params: {
|
||||
message: {
|
||||
id: 14,
|
||||
chat_id: 2,
|
||||
sender: "+15550001111",
|
||||
is_from_me: false,
|
||||
text: "hello",
|
||||
is_group: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await flush();
|
||||
closeResolve?.();
|
||||
await run;
|
||||
|
||||
expect(replyMock).toHaveBeenCalled();
|
||||
const ctx = replyMock.mock.calls[0]?.[0] as {
|
||||
ChatType?: string;
|
||||
SessionKey?: string;
|
||||
};
|
||||
expect(ctx.ChatType).toBe("group");
|
||||
expect(ctx.SessionKey).toBe("agent:main:imessage:group:2");
|
||||
});
|
||||
|
||||
it("prefixes tool and final replies with responsePrefix", async () => {
|
||||
config = {
|
||||
...config,
|
||||
|
||||
+28
-8
@@ -170,10 +170,36 @@ export async function monitorIMessageProvider(
|
||||
const chatId = message.chat_id ?? undefined;
|
||||
const chatGuid = message.chat_guid ?? undefined;
|
||||
const chatIdentifier = message.chat_identifier ?? undefined;
|
||||
const isGroup = Boolean(message.is_group);
|
||||
|
||||
const groupIdCandidate = chatId !== undefined ? String(chatId) : undefined;
|
||||
const groupListPolicy = groupIdCandidate
|
||||
? resolveProviderGroupPolicy({
|
||||
cfg,
|
||||
provider: "imessage",
|
||||
accountId: accountInfo.accountId,
|
||||
groupId: groupIdCandidate,
|
||||
})
|
||||
: {
|
||||
allowlistEnabled: false,
|
||||
allowed: true,
|
||||
groupConfig: undefined,
|
||||
defaultConfig: undefined,
|
||||
};
|
||||
|
||||
// Some iMessage threads can have multiple participants but still report
|
||||
// is_group=false depending on how Messages stores the identifier.
|
||||
// If the owner explicitly configures a chat_id under imessage.groups, treat
|
||||
// that thread as a "group" for permission gating and session isolation.
|
||||
const treatAsGroupByConfig = Boolean(
|
||||
groupIdCandidate &&
|
||||
groupListPolicy.allowlistEnabled &&
|
||||
groupListPolicy.groupConfig,
|
||||
);
|
||||
|
||||
const isGroup = Boolean(message.is_group) || treatAsGroupByConfig;
|
||||
if (isGroup && !chatId) return;
|
||||
|
||||
const groupId = isGroup ? String(chatId) : undefined;
|
||||
const groupId = isGroup ? groupIdCandidate : undefined;
|
||||
const storeAllowFrom = await readProviderAllowFromStore("imessage").catch(
|
||||
() => [],
|
||||
);
|
||||
@@ -214,12 +240,6 @@ export async function monitorIMessageProvider(
|
||||
return;
|
||||
}
|
||||
}
|
||||
const groupListPolicy = resolveProviderGroupPolicy({
|
||||
cfg,
|
||||
provider: "imessage",
|
||||
accountId: accountInfo.accountId,
|
||||
groupId,
|
||||
});
|
||||
if (groupListPolicy.allowlistEnabled && !groupListPolicy.allowed) {
|
||||
logVerbose(
|
||||
`imessage: skipping group message (${groupId ?? "unknown"}) not in allowlist`,
|
||||
|
||||
+1
-12
@@ -6,10 +6,6 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const realOs = await vi.importActual<typeof import("node:os")>("node:os");
|
||||
const HOME = path.join(realOs.tmpdir(), "clawdbot-home-test");
|
||||
const PREV_CLAWDBOT_STATE_DIR = process.env.CLAWDBOT_STATE_DIR;
|
||||
const PREV_CLAWDIS_STATE_DIR = process.env.CLAWDIS_STATE_DIR;
|
||||
delete process.env.CLAWDBOT_STATE_DIR;
|
||||
delete process.env.CLAWDIS_STATE_DIR;
|
||||
|
||||
vi.mock("node:os", () => ({
|
||||
default: { homedir: () => HOME, tmpdir: () => realOs.tmpdir() },
|
||||
@@ -26,18 +22,11 @@ describe("media store", () => {
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(HOME, { recursive: true, force: true });
|
||||
if (PREV_CLAWDBOT_STATE_DIR !== undefined)
|
||||
process.env.CLAWDBOT_STATE_DIR = PREV_CLAWDBOT_STATE_DIR;
|
||||
else delete process.env.CLAWDBOT_STATE_DIR;
|
||||
|
||||
if (PREV_CLAWDIS_STATE_DIR !== undefined)
|
||||
process.env.CLAWDIS_STATE_DIR = PREV_CLAWDIS_STATE_DIR;
|
||||
else delete process.env.CLAWDIS_STATE_DIR;
|
||||
});
|
||||
|
||||
it("creates and returns media directory", async () => {
|
||||
const dir = await store.ensureMediaDir();
|
||||
expect(path.basename(dir)).toBe("media");
|
||||
expect(dir).toContain("clawdbot-home-test");
|
||||
const stat = await fs.stat(dir);
|
||||
expect(stat.isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
+1
-17
@@ -14,16 +14,9 @@ const runtime = {
|
||||
describe("web logout", () => {
|
||||
const origHomedir = os.homedir;
|
||||
let tmpDir: string;
|
||||
let prevStateDir: string | undefined;
|
||||
let prevClawdisStateDir: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
prevStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
prevClawdisStateDir = process.env.CLAWDIS_STATE_DIR;
|
||||
delete process.env.CLAWDBOT_STATE_DIR;
|
||||
delete process.env.CLAWDIS_STATE_DIR;
|
||||
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-logout-"));
|
||||
vi.spyOn(os, "homedir").mockReturnValue(tmpDir);
|
||||
vi.resetModules();
|
||||
@@ -43,14 +36,6 @@ describe("web logout", () => {
|
||||
await fsPromises
|
||||
.rm(tmpDir, { recursive: true, force: true })
|
||||
.catch(() => {});
|
||||
|
||||
if (prevStateDir !== undefined) process.env.CLAWDBOT_STATE_DIR = prevStateDir;
|
||||
else delete process.env.CLAWDBOT_STATE_DIR;
|
||||
|
||||
if (prevClawdisStateDir !== undefined)
|
||||
process.env.CLAWDIS_STATE_DIR = prevClawdisStateDir;
|
||||
else delete process.env.CLAWDIS_STATE_DIR;
|
||||
|
||||
// restore for safety
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
(os.homedir as unknown as typeof origHomedir) = origHomedir;
|
||||
@@ -62,8 +47,7 @@ describe("web logout", () => {
|
||||
async () => {
|
||||
const { logoutWeb, WA_WEB_AUTH_DIR } = await import("./session.js");
|
||||
|
||||
const rel = path.relative(tmpDir, WA_WEB_AUTH_DIR);
|
||||
expect(rel && !rel.startsWith("..") && !path.isAbsolute(rel)).toBe(true);
|
||||
expect(WA_WEB_AUTH_DIR.startsWith(tmpDir)).toBe(true);
|
||||
fs.mkdirSync(WA_WEB_AUTH_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(WA_WEB_AUTH_DIR, "creds.json"), "{}");
|
||||
const result = await logoutWeb({ runtime: runtime as never });
|
||||
|
||||
Reference in new issue
Block a user