chore: add Zoho Cliq docs, VPS scripts, and leaf config schema
This commit is contained in:
parent
b35ceca265
commit
cf20305894
192
docs/channels/zoho-cliq.md
Normal file
192
docs/channels/zoho-cliq.md
Normal file
@ -0,0 +1,192 @@
|
||||
---
|
||||
summary: "Zoho Cliq setup via OAuth API"
|
||||
read_when:
|
||||
- Setting up Zoho Cliq
|
||||
- Debugging Zoho Cliq routing
|
||||
---
|
||||
|
||||
# Zoho Cliq (plugin)
|
||||
|
||||
Status: supported via plugin (OAuth API). Direct messages, group chats, and channels are supported.
|
||||
Zoho Cliq is a team communication platform from Zoho; see the official site at
|
||||
[zoho.com/cliq](https://www.zoho.com/cliq/) for product details.
|
||||
|
||||
## Plugin required
|
||||
Zoho Cliq ships as a plugin and is not bundled with the core install.
|
||||
|
||||
Install via CLI (npm registry):
|
||||
```bash
|
||||
clawdbot plugins install @clawdbot/zoho-cliq
|
||||
```
|
||||
|
||||
Local checkout (when running from a git repo):
|
||||
```bash
|
||||
clawdbot plugins install ./extensions/zoho-cliq
|
||||
```
|
||||
|
||||
If you choose Zoho Cliq during configure/onboarding and a git checkout is detected,
|
||||
Clawdbot will offer the local install path automatically.
|
||||
|
||||
Details: [Plugins](/plugin)
|
||||
|
||||
## Quick setup
|
||||
|
||||
### 1. Create OAuth Application
|
||||
1. Go to [Zoho API Console](https://accounts.zoho.com/developerconsole)
|
||||
2. Click **Add Client** → **Server-based Applications**
|
||||
3. Fill in:
|
||||
- **Client Name**: Clawdbot
|
||||
- **Homepage URL**: https://clawd.bot (or your domain)
|
||||
- **Authorized Redirect URI**: `https://accounts.zoho.com` (for manual token generation)
|
||||
4. Copy the **Client ID** and **Client Secret**
|
||||
|
||||
### 2. Generate Refresh Token
|
||||
1. Open this URL in your browser (replace `YOUR_CLIENT_ID`):
|
||||
```
|
||||
https://accounts.zoho.com/oauth/v2/auth?scope=ZohoCliq.Messages.ALL,ZohoCliq.Chats.ALL,ZohoCliq.Users.READ&client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=https://accounts.zoho.com&access_type=offline
|
||||
```
|
||||
2. Authorize the application
|
||||
3. Copy the `code` parameter from the redirect URL
|
||||
4. Exchange for tokens via curl:
|
||||
```bash
|
||||
curl -X POST "https://accounts.zoho.com/oauth/v2/token" \
|
||||
-d "grant_type=authorization_code" \
|
||||
-d "client_id=YOUR_CLIENT_ID" \
|
||||
-d "client_secret=YOUR_CLIENT_SECRET" \
|
||||
-d "code=YOUR_AUTH_CODE" \
|
||||
-d "redirect_uri=https://accounts.zoho.com"
|
||||
```
|
||||
5. Copy the **refresh_token** from the response
|
||||
|
||||
### 3. Configure Clawdbot
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
"zoho-cliq": {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
main: {
|
||||
clientId: "1000.XXXXX",
|
||||
clientSecret: "your-client-secret",
|
||||
refreshToken: "1000.xxxxx.xxxxx",
|
||||
dc: "US"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Start the gateway
|
||||
```bash
|
||||
clawdbot gateway run
|
||||
```
|
||||
|
||||
## Data centers
|
||||
Zoho operates in multiple data centers. Set `dc` to match your Zoho account region:
|
||||
|
||||
| Region | dc | API Domain |
|
||||
|--------|------|------------|
|
||||
| United States | `US` | cliq.zoho.com |
|
||||
| Europe | `EU` | cliq.zoho.eu |
|
||||
| India | `IN` | cliq.zoho.in |
|
||||
| Australia | `AU` | cliq.zoho.com.au |
|
||||
| Japan | `JP` | cliq.zoho.jp |
|
||||
| Canada | `CA` | cliq.zohocloud.ca |
|
||||
| Saudi Arabia | `SA` | cliq.zoho.sa |
|
||||
|
||||
Default is `US` if not specified.
|
||||
|
||||
## Environment variables (default account)
|
||||
Set these on the gateway host if you prefer env vars:
|
||||
|
||||
- `ZOHO_CLIQ_CLIENT_ID=...`
|
||||
- `ZOHO_CLIQ_CLIENT_SECRET=...`
|
||||
- `ZOHO_CLIQ_REFRESH_TOKEN=...`
|
||||
- `ZOHO_CLIQ_DC=US`
|
||||
|
||||
Env vars apply only to the **default** account. Other accounts must use config values.
|
||||
|
||||
## OAuth scopes required
|
||||
The following scopes are needed for full functionality:
|
||||
|
||||
- `ZohoCliq.Messages.ALL` - Read and send messages
|
||||
- `ZohoCliq.Chats.ALL` - Access chat conversations
|
||||
- `ZohoCliq.Users.READ` - Read user information
|
||||
|
||||
## Access control (DMs)
|
||||
- Default: `channels.zoho-cliq.dmPolicy = "pairing"` (unknown senders get a pairing code).
|
||||
- Approve via:
|
||||
- `clawdbot pairing list zoho-cliq`
|
||||
- `clawdbot pairing approve zoho-cliq <CODE>`
|
||||
- Public DMs: `channels.zoho-cliq.dmPolicy="open"` plus `channels.zoho-cliq.allowFrom=["*"]`.
|
||||
- Allowlist: `channels.zoho-cliq.dmPolicy="allowlist"` with specific user emails in `allowFrom`.
|
||||
|
||||
## Targets for outbound delivery
|
||||
Use these target formats with `clawdbot message send` or cron/webhooks:
|
||||
|
||||
- `chat:<chat_id>` - Send to a specific chat by ID
|
||||
- `channel:<unique_name>` - Send to a channel by its unique name
|
||||
- `user:<email>` - Send DM to a user by email
|
||||
- `user:<zuid>` - Send DM to a user by Zoho User ID
|
||||
- `@email@example.com` - Send DM (shorthand)
|
||||
|
||||
Examples:
|
||||
```bash
|
||||
clawdbot message send --channel zoho-cliq --to "user:john@example.com" --text "Hello!"
|
||||
clawdbot message send --channel zoho-cliq --to "channel:engineering" --text "Update deployed"
|
||||
```
|
||||
|
||||
## Multi-account
|
||||
Zoho Cliq supports multiple accounts under `channels.zoho-cliq.accounts`:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
"zoho-cliq": {
|
||||
accounts: {
|
||||
default: {
|
||||
name: "Primary",
|
||||
clientId: "...",
|
||||
clientSecret: "...",
|
||||
refreshToken: "...",
|
||||
dc: "US"
|
||||
},
|
||||
eu_team: {
|
||||
name: "EU Team",
|
||||
clientId: "...",
|
||||
clientSecret: "...",
|
||||
refreshToken: "...",
|
||||
dc: "EU"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Message polling
|
||||
Zoho Cliq uses polling to check for new messages (default: every 3 seconds). This is due to API limitations - Zoho Cliq's webhook support requires bot registration in their platform.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Auth errors
|
||||
- **"invalid_code"**: Authorization code expired (valid for ~1 minute). Generate a new one.
|
||||
- **"invalid_client"**: Check client ID and secret match your OAuth app.
|
||||
- **Token refresh failed**: Ensure refresh token is valid and hasn't been revoked.
|
||||
|
||||
### No messages received
|
||||
- Polling runs every 3 seconds; there may be a slight delay.
|
||||
- Check `clawdbot channels status --probe` to verify connection.
|
||||
- Ensure the OAuth app has the required scopes.
|
||||
|
||||
### Wrong data center
|
||||
- If you get 404 errors, your `dc` setting may not match your Zoho account region.
|
||||
- Check your Zoho account URL (e.g., accounts.zoho.eu = EU datacenter).
|
||||
|
||||
### Rate limits
|
||||
Zoho Cliq has API rate limits:
|
||||
- Messages: 50 requests/min per user
|
||||
- Chats: 30 requests/min per user
|
||||
|
||||
If you hit limits, reduce polling frequency or message volume.
|
||||
19
scripts/sync-skills-to-vps.sh
Executable file
19
scripts/sync-skills-to-vps.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Sync local skills to VPS managed skills directory.
|
||||
# Update SSH_TARGET or paths if your VPS changes.
|
||||
SSH_TARGET="root@100.94.23.85"
|
||||
LOCAL_SKILLS_DIR="/Users/user/Downloads/Documents - USER’s MacBook Air/Code/clawdia/skills"
|
||||
REMOTE_SKILLS_DIR="/root/.clawdbot/skills"
|
||||
LOG_FILE="$HOME/Library/Logs/clawdbot-sync-skills.log"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Sync start" >> "$LOG_FILE"
|
||||
|
||||
/usr/bin/rsync -az --delete \
|
||||
"${LOCAL_SKILLS_DIR}/" \
|
||||
"${SSH_TARGET}:${REMOTE_SKILLS_DIR}/" >> "$LOG_FILE" 2>&1
|
||||
|
||||
echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] Sync complete" >> "$LOG_FILE"
|
||||
206
scripts/sync-upstream.sh
Executable file
206
scripts/sync-upstream.sh
Executable file
@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Sync with upstream clawdbot while preserving local extensions
|
||||
#
|
||||
# Strategy:
|
||||
# 1. Stash local extensions
|
||||
# 2. Pull/rebase upstream
|
||||
# 3. Restore local extensions
|
||||
# 4. Handle conflicts if any
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$REPO_DIR"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[+]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||
error() { echo -e "${RED}[x]${NC} $1"; exit 1; }
|
||||
info() { echo -e "${CYAN}[i]${NC} $1"; }
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# Configuration
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
UPSTREAM_REMOTE="upstream"
|
||||
UPSTREAM_URL="https://github.com/clawdbot/clawdbot.git"
|
||||
MAIN_BRANCH="main"
|
||||
|
||||
# Local-only paths (won't conflict with upstream)
|
||||
LOCAL_EXTENSIONS=(
|
||||
"extensions/zoho-cliq"
|
||||
# Add more local extensions here
|
||||
)
|
||||
|
||||
# Backup directory
|
||||
BACKUP_DIR="$REPO_DIR/.local-backup"
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# Setup upstream remote
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
setup_upstream() {
|
||||
if ! git remote get-url "$UPSTREAM_REMOTE" &>/dev/null; then
|
||||
log "Adding upstream remote: $UPSTREAM_URL"
|
||||
git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL"
|
||||
else
|
||||
info "Upstream remote already configured"
|
||||
fi
|
||||
}
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# Backup local extensions
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
backup_local() {
|
||||
log "Backing up local extensions..."
|
||||
rm -rf "$BACKUP_DIR"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
for ext in "${LOCAL_EXTENSIONS[@]}"; do
|
||||
if [[ -d "$REPO_DIR/$ext" ]]; then
|
||||
local dest="$BACKUP_DIR/$ext"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp -r "$REPO_DIR/$ext" "$dest"
|
||||
info " Backed up: $ext"
|
||||
fi
|
||||
done
|
||||
|
||||
# Also backup any uncommitted changes
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
git stash push -m "sync-upstream-$(date +%Y%m%d-%H%M%S)"
|
||||
info " Stashed uncommitted changes"
|
||||
fi
|
||||
}
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# Restore local extensions
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
restore_local() {
|
||||
log "Restoring local extensions..."
|
||||
|
||||
for ext in "${LOCAL_EXTENSIONS[@]}"; do
|
||||
local src="$BACKUP_DIR/$ext"
|
||||
local dest="$REPO_DIR/$ext"
|
||||
if [[ -d "$src" ]]; then
|
||||
# Remove upstream version if exists (unlikely for local-only)
|
||||
rm -rf "$dest"
|
||||
cp -r "$src" "$dest"
|
||||
info " Restored: $ext"
|
||||
fi
|
||||
done
|
||||
|
||||
# Restore stashed changes if any
|
||||
if git stash list | grep -q "sync-upstream-"; then
|
||||
log "Restoring stashed changes..."
|
||||
git stash pop || warn "Stash pop had conflicts - resolve manually"
|
||||
fi
|
||||
}
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# Sync with upstream
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
sync_upstream() {
|
||||
log "Fetching upstream..."
|
||||
git fetch "$UPSTREAM_REMOTE"
|
||||
|
||||
local LOCAL_HEAD=$(git rev-parse HEAD)
|
||||
local UPSTREAM_HEAD=$(git rev-parse "$UPSTREAM_REMOTE/$MAIN_BRANCH")
|
||||
|
||||
if [[ "$LOCAL_HEAD" == "$UPSTREAM_HEAD" ]]; then
|
||||
info "Already up to date with upstream"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Count commits behind/ahead
|
||||
local BEHIND=$(git rev-list --count HEAD.."$UPSTREAM_REMOTE/$MAIN_BRANCH")
|
||||
local AHEAD=$(git rev-list --count "$UPSTREAM_REMOTE/$MAIN_BRANCH"..HEAD)
|
||||
|
||||
info "Local is $BEHIND commits behind, $AHEAD commits ahead of upstream"
|
||||
|
||||
# Merge strategy: merge upstream into local (preserves local commits)
|
||||
log "Merging upstream/$MAIN_BRANCH..."
|
||||
|
||||
if git merge "$UPSTREAM_REMOTE/$MAIN_BRANCH" -m "Merge upstream $MAIN_BRANCH"; then
|
||||
log "Merge successful!"
|
||||
else
|
||||
error "Merge conflicts detected. Resolve manually then run: git merge --continue"
|
||||
fi
|
||||
}
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# Check for local modifications to upstream files
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
check_conflicts() {
|
||||
log "Checking for potential conflicts..."
|
||||
|
||||
# Files modified locally that also exist upstream
|
||||
local MODIFIED=$(git diff --name-only "$UPSTREAM_REMOTE/$MAIN_BRANCH"...HEAD 2>/dev/null || true)
|
||||
|
||||
if [[ -n "$MODIFIED" ]]; then
|
||||
info "Files modified locally (may conflict on next sync):"
|
||||
echo "$MODIFIED" | head -20
|
||||
local COUNT=$(echo "$MODIFIED" | wc -l | tr -d ' ')
|
||||
if [[ $COUNT -gt 20 ]]; then
|
||||
info " ... and $((COUNT - 20)) more"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# Main
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
main() {
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo " Clawdbot Upstream Sync"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
# Ensure we're on the right branch
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
if [[ "$CURRENT_BRANCH" != "$MAIN_BRANCH" ]]; then
|
||||
warn "Not on $MAIN_BRANCH branch (on: $CURRENT_BRANCH)"
|
||||
read -p "Switch to $MAIN_BRANCH? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
git checkout "$MAIN_BRANCH"
|
||||
else
|
||||
error "Aborted"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure working directory is clean (except for local extensions)
|
||||
if ! git diff --quiet -- . ":(exclude)extensions/"; then
|
||||
warn "You have uncommitted changes outside extensions/"
|
||||
read -p "Continue anyway? [y/N] " -n 1 -r
|
||||
echo
|
||||
[[ $REPLY =~ ^[Yy]$ ]] || error "Aborted"
|
||||
fi
|
||||
|
||||
setup_upstream
|
||||
backup_local
|
||||
sync_upstream
|
||||
restore_local
|
||||
check_conflicts
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo -e "${GREEN}Sync complete!${NC}"
|
||||
echo "═══════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "Local extensions preserved:"
|
||||
for ext in "${LOCAL_EXTENSIONS[@]}"; do
|
||||
[[ -d "$REPO_DIR/$ext" ]] && echo " - $ext"
|
||||
done
|
||||
echo ""
|
||||
echo "Next: Review changes with 'git log --oneline -10'"
|
||||
}
|
||||
|
||||
# Run
|
||||
main "$@"
|
||||
241
scripts/vps-setup.sh
Executable file
241
scripts/vps-setup.sh
Executable file
@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Clawdbot VPS Setup Script
|
||||
# For Hetzner or any Debian/Ubuntu VPS
|
||||
#
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/.../vps-setup.sh | bash
|
||||
# or: bash vps-setup.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${GREEN}[+]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
|
||||
error() { echo -e "${RED}[x]${NC} $1"; exit 1; }
|
||||
|
||||
# Check root
|
||||
[[ $EUID -eq 0 ]] || error "Run as root: sudo bash $0"
|
||||
|
||||
# Detect OS
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
else
|
||||
error "Cannot detect OS"
|
||||
fi
|
||||
|
||||
log "Detected OS: $OS"
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# 1. System packages
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
log "Installing system dependencies..."
|
||||
|
||||
if [[ "$OS" == "ubuntu" || "$OS" == "debian" ]]; then
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl wget git build-essential
|
||||
elif [[ "$OS" == "fedora" || "$OS" == "centos" || "$OS" == "rhel" ]]; then
|
||||
dnf install -y curl wget git gcc gcc-c++ make
|
||||
else
|
||||
warn "Unknown OS, assuming packages are installed"
|
||||
fi
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# 2. Node.js (via NodeSource)
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
if ! command -v node &>/dev/null || [[ $(node -v | cut -d. -f1 | tr -d 'v') -lt 22 ]]; then
|
||||
log "Installing Node.js 22..."
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt-get install -y nodejs
|
||||
else
|
||||
log "Node.js $(node -v) already installed"
|
||||
fi
|
||||
|
||||
# Parse arguments
|
||||
TAILSCALE_AUTH_KEY=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--tailscale-key)
|
||||
TAILSCALE_AUTH_KEY="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# 3. Tailscale
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
if ! command -v tailscale &>/dev/null; then
|
||||
log "Installing Tailscale..."
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
else
|
||||
log "Tailscale already installed"
|
||||
fi
|
||||
|
||||
# Authenticate Tailscale if key provided
|
||||
if [[ -n "$TAILSCALE_AUTH_KEY" ]]; then
|
||||
log "Authenticating Tailscale..."
|
||||
tailscale up --authkey="$TAILSCALE_AUTH_KEY" --ssh --reset
|
||||
elif ! tailscale status &>/dev/null; then
|
||||
warn "Tailscale not connected. Run: tailscale up --ssh"
|
||||
warn "Get auth key from: https://login.tailscale.com/admin/settings/keys"
|
||||
fi
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# 4. Create clawdbot user
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
CLAWDBOT_USER="clawdbot"
|
||||
CLAWDBOT_HOME="/home/$CLAWDBOT_USER"
|
||||
|
||||
if ! id "$CLAWDBOT_USER" &>/dev/null; then
|
||||
log "Creating user: $CLAWDBOT_USER"
|
||||
useradd -m -s /bin/bash "$CLAWDBOT_USER"
|
||||
else
|
||||
log "User $CLAWDBOT_USER already exists"
|
||||
fi
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# 5. Install Clawdbot
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
log "Installing Clawdbot..."
|
||||
npm install -g clawdbot@latest
|
||||
|
||||
# Verify installation
|
||||
CLAWDBOT_VERSION=$(clawdbot --version 2>/dev/null || echo "unknown")
|
||||
log "Clawdbot version: $CLAWDBOT_VERSION"
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# 6. Create directories
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
log "Creating directories..."
|
||||
mkdir -p "$CLAWDBOT_HOME/.clawdbot"
|
||||
mkdir -p "$CLAWDBOT_HOME/clawd"
|
||||
chown -R "$CLAWDBOT_USER:$CLAWDBOT_USER" "$CLAWDBOT_HOME/.clawdbot"
|
||||
chown -R "$CLAWDBOT_USER:$CLAWDBOT_USER" "$CLAWDBOT_HOME/clawd"
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# 7. Create systemd service
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
log "Creating systemd service..."
|
||||
|
||||
cat > /etc/systemd/system/clawdbot-gateway.service << 'EOF'
|
||||
[Unit]
|
||||
Description=Clawdbot Gateway
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=clawdbot
|
||||
Group=clawdbot
|
||||
WorkingDirectory=/home/clawdbot/clawd
|
||||
ExecStart=/usr/bin/clawdbot gateway run --bind loopback --port 18789
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Environment
|
||||
Environment=NODE_ENV=production
|
||||
Environment=HOME=/home/clawdbot
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths=/home/clawdbot/.clawdbot /home/clawdbot/clawd
|
||||
PrivateTmp=true
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=clawdbot-gateway
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
log "Systemd service created: clawdbot-gateway"
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# 8. Create config template
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
CONFIG_FILE="$CLAWDBOT_HOME/.clawdbot/clawdbot.json"
|
||||
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||
log "Creating config template..."
|
||||
cat > "$CONFIG_FILE" << 'EOF'
|
||||
{
|
||||
"gateway": {
|
||||
"mode": "local",
|
||||
"port": 18789,
|
||||
"bind": "loopback",
|
||||
"auth": {
|
||||
"mode": "token",
|
||||
"token": "REPLACE_WITH_SECURE_TOKEN"
|
||||
}
|
||||
},
|
||||
"channels": {},
|
||||
"plugins": {
|
||||
"entries": {}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
chown "$CLAWDBOT_USER:$CLAWDBOT_USER" "$CONFIG_FILE"
|
||||
warn "Edit config: nano $CONFIG_FILE"
|
||||
warn "Generate token: openssl rand -hex 32"
|
||||
fi
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# 9. Firewall (UFW)
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
if command -v ufw &>/dev/null; then
|
||||
log "Configuring firewall..."
|
||||
ufw allow ssh
|
||||
ufw allow in on tailscale0
|
||||
ufw --force enable
|
||||
log "Firewall: SSH + Tailscale allowed"
|
||||
else
|
||||
warn "UFW not installed - configure firewall manually"
|
||||
fi
|
||||
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
# Summary
|
||||
#───────────────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════════"
|
||||
echo -e "${GREEN}Setup Complete!${NC}"
|
||||
echo "═══════════════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo ""
|
||||
echo " 1. Connect Tailscale (if not already):"
|
||||
echo " tailscale up --ssh --authkey=tskey-auth-xxx"
|
||||
echo ""
|
||||
echo " 2. Generate gateway token:"
|
||||
echo " TOKEN=\$(openssl rand -hex 32)"
|
||||
echo " echo \"Token: \$TOKEN\""
|
||||
echo ""
|
||||
echo " 3. Edit config:"
|
||||
echo " nano $CONFIG_FILE"
|
||||
echo " # Set the token and add channels"
|
||||
echo ""
|
||||
echo " 4. Start gateway:"
|
||||
echo " systemctl enable --now clawdbot-gateway"
|
||||
echo ""
|
||||
echo " 5. Check status:"
|
||||
echo " systemctl status clawdbot-gateway"
|
||||
echo " journalctl -u clawdbot-gateway -f"
|
||||
echo ""
|
||||
echo " 6. Access from Mac (via Tailscale):"
|
||||
echo " ssh clawdbot@$(hostname)"
|
||||
echo " # Or via Tailscale name: ssh clawdbot@vps-tailscale-name"
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════════════════════════"
|
||||
@ -59,8 +59,8 @@ const WebSearchSchema = Type.Object({
|
||||
|
||||
type WebSearchConfig = NonNullable<ClawdbotConfig["tools"]>["web"] extends infer Web
|
||||
? Web extends { search?: infer Search }
|
||||
? Search
|
||||
: undefined
|
||||
? Search
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
type BraveSearchResult = {
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
// Zoho Cliq Channel Plugin - Core Stub
|
||||
// Full implementation is in extensions/zoho-cliq
|
||||
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
} from "../../routing/session-key.js";
|
||||
import type { ChannelMeta } from "./types.js";
|
||||
import type { ChannelPlugin } from "./types.js";
|
||||
|
||||
@ -63,10 +66,7 @@ export const zohoCliqPlugin: ChannelPlugin<ResolvedZohoCliqAccount> = {
|
||||
const resolvedAccountId = normalizeAccountId(accountId);
|
||||
const channelCfg = (cfg as any).channels?.["zoho-cliq"];
|
||||
const accounts = channelCfg?.accounts;
|
||||
const accountCfg =
|
||||
accounts?.[resolvedAccountId] ??
|
||||
(resolvedAccountId === DEFAULT_ACCOUNT_ID ? channelCfg : undefined) ??
|
||||
{};
|
||||
const accountCfg = accounts?.[resolvedAccountId] ?? (resolvedAccountId === DEFAULT_ACCOUNT_ID ? channelCfg : undefined) ?? {};
|
||||
|
||||
const clientId = accountCfg.clientId ?? process.env.ZOHO_CLIQ_CLIENT_ID ?? "";
|
||||
const clientSecret = accountCfg.clientSecret ?? process.env.ZOHO_CLIQ_CLIENT_SECRET ?? "";
|
||||
@ -89,7 +89,9 @@ export const zohoCliqPlugin: ChannelPlugin<ResolvedZohoCliqAccount> = {
|
||||
defaultAccountId: (cfg) => {
|
||||
const accounts = (cfg as any).channels?.["zoho-cliq"]?.accounts;
|
||||
if (!accounts) return DEFAULT_ACCOUNT_ID;
|
||||
const enabled = Object.entries(accounts).filter(([_, a]) => (a as any).enabled ?? true);
|
||||
const enabled = Object.entries(accounts).filter(
|
||||
([_, a]) => (a as any).enabled ?? true,
|
||||
);
|
||||
if (enabled.length === 1) return enabled[0][0];
|
||||
if (Object.hasOwn(accounts, DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
|
||||
140
src/config/zod-schema.leaf.ts
Normal file
140
src/config/zod-schema.leaf.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { z } from "zod";
|
||||
import { isSafeExecutableValue } from "../infra/exec-safety.js";
|
||||
|
||||
export const HexColorSchema = z.string().regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)");
|
||||
|
||||
export const ExecutableTokenSchema = z
|
||||
.string()
|
||||
.refine(isSafeExecutableValue, "expected safe executable name or path");
|
||||
|
||||
export const GroupPolicySchema = z.enum(["open", "disabled", "allowlist"]);
|
||||
|
||||
export const DmPolicySchema = z.enum(["pairing", "allowlist", "open", "disabled"]);
|
||||
|
||||
export const ReplyToModeSchema = z.union([z.literal("off"), z.literal("first"), z.literal("all")]);
|
||||
|
||||
export const MSTeamsReplyStyleSchema = z.enum(["thread", "top-level"]);
|
||||
|
||||
export const MarkdownTableModeSchema = z.enum(["off", "bullets", "code"]);
|
||||
|
||||
export const MarkdownConfigSchema = z
|
||||
.object({
|
||||
tables: MarkdownTableModeSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const BlockStreamingCoalesceSchema = z
|
||||
.object({
|
||||
minChars: z.number().int().positive().optional(),
|
||||
maxChars: z.number().int().positive().optional(),
|
||||
idleMs: z.number().int().nonnegative().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const BlockStreamingChunkSchema = z
|
||||
.object({
|
||||
minChars: z.number().int().positive().optional(),
|
||||
maxChars: z.number().int().positive().optional(),
|
||||
breakPreference: z
|
||||
.union([z.literal("paragraph"), z.literal("newline"), z.literal("sentence")])
|
||||
.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const RetryConfigSchema = z
|
||||
.object({
|
||||
attempts: z.number().int().min(1).optional(),
|
||||
minDelayMs: z.number().int().min(0).optional(),
|
||||
maxDelayMs: z.number().int().min(0).optional(),
|
||||
jitter: z.number().min(0).max(1).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ToolPolicySchema = z
|
||||
.object({
|
||||
allow: z.array(z.string()).optional(),
|
||||
deny: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const TtsProviderSchema = z.enum(["elevenlabs", "openai"]);
|
||||
export const TtsModeSchema = z.enum(["final", "all"]);
|
||||
export const TtsConfigSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
mode: TtsModeSchema.optional(),
|
||||
provider: TtsProviderSchema.optional(),
|
||||
summaryModel: z.string().optional(),
|
||||
modelOverrides: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
allowText: z.boolean().optional(),
|
||||
allowProvider: z.boolean().optional(),
|
||||
allowVoice: z.boolean().optional(),
|
||||
allowModelId: z.boolean().optional(),
|
||||
allowVoiceSettings: z.boolean().optional(),
|
||||
allowNormalization: z.boolean().optional(),
|
||||
allowSeed: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
elevenlabs: z
|
||||
.object({
|
||||
apiKey: z.string().optional(),
|
||||
baseUrl: z.string().optional(),
|
||||
voiceId: z.string().optional(),
|
||||
modelId: z.string().optional(),
|
||||
seed: z.number().int().min(0).max(4294967295).optional(),
|
||||
applyTextNormalization: z.enum(["auto", "on", "off"]).optional(),
|
||||
languageCode: z.string().optional(),
|
||||
voiceSettings: z
|
||||
.object({
|
||||
stability: z.number().min(0).max(1).optional(),
|
||||
similarityBoost: z.number().min(0).max(1).optional(),
|
||||
style: z.number().min(0).max(1).optional(),
|
||||
useSpeakerBoost: z.boolean().optional(),
|
||||
speed: z.number().min(0.5).max(2).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
openai: z
|
||||
.object({
|
||||
apiKey: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
voice: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
prefsPath: z.string().optional(),
|
||||
maxTextLength: z.number().int().min(1).optional(),
|
||||
timeoutMs: z.number().int().min(1000).max(120000).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const ValyuConfigSchema = z
|
||||
.object({
|
||||
apiKey: z.string().optional(),
|
||||
baseUrl: z.string().optional(),
|
||||
maxResults: z.number().int().positive().optional(),
|
||||
searchType: z.union([z.literal("all"), z.literal("web"), z.literal("proprietary")]).optional(),
|
||||
deepSearch: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
export const NativeCommandsSettingSchema = z.union([z.boolean(), z.literal("auto")]);
|
||||
|
||||
export const ProviderCommandsSchema = z
|
||||
.object({
|
||||
native: NativeCommandsSettingSchema.optional(),
|
||||
nativeSkills: NativeCommandsSettingSchema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
@ -1,4 +1,10 @@
|
||||
import { Editor, type EditorTheme, type TUI, Key, matchesKey } from "@mariozechner/pi-tui";
|
||||
import {
|
||||
Editor,
|
||||
type EditorTheme,
|
||||
type TUI,
|
||||
Key,
|
||||
matchesKey,
|
||||
} from "@mariozechner/pi-tui";
|
||||
|
||||
export class CustomEditor extends Editor {
|
||||
onEscape?: () => void;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user