diff --git a/docs/channels/zoho-cliq.md b/docs/channels/zoho-cliq.md new file mode 100644 index 000000000..6d8bb1911 --- /dev/null +++ b/docs/channels/zoho-cliq.md @@ -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 ` +- 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:` - Send to a specific chat by ID +- `channel:` - Send to a channel by its unique name +- `user:` - Send DM to a user by email +- `user:` - 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. diff --git a/scripts/sync-skills-to-vps.sh b/scripts/sync-skills-to-vps.sh new file mode 100755 index 000000000..c791c8494 --- /dev/null +++ b/scripts/sync-skills-to-vps.sh @@ -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" diff --git a/scripts/sync-upstream.sh b/scripts/sync-upstream.sh new file mode 100755 index 000000000..1ee7c7277 --- /dev/null +++ b/scripts/sync-upstream.sh @@ -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 "$@" diff --git a/scripts/vps-setup.sh b/scripts/vps-setup.sh new file mode 100755 index 000000000..fd92931bd --- /dev/null +++ b/scripts/vps-setup.sh @@ -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 "═══════════════════════════════════════════════════════════════════════════" diff --git a/src/agents/tools/web-search.ts b/src/agents/tools/web-search.ts index 431200162..dcec7f609 100644 --- a/src/agents/tools/web-search.ts +++ b/src/agents/tools/web-search.ts @@ -59,8 +59,8 @@ const WebSearchSchema = Type.Object({ type WebSearchConfig = NonNullable["web"] extends infer Web ? Web extends { search?: infer Search } - ? Search - : undefined + ? Search + : undefined : undefined; type BraveSearchResult = { diff --git a/src/channels/plugins/zoho-cliq.ts b/src/channels/plugins/zoho-cliq.ts index 062b367ac..dd9d53cb4 100644 --- a/src/channels/plugins/zoho-cliq.ts +++ b/src/channels/plugins/zoho-cliq.ts @@ -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 = { 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 = { 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; diff --git a/src/config/zod-schema.leaf.ts b/src/config/zod-schema.leaf.ts new file mode 100644 index 000000000..5fb23c797 --- /dev/null +++ b/src/config/zod-schema.leaf.ts @@ -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(); diff --git a/src/tui/components/custom-editor.ts b/src/tui/components/custom-editor.ts index 565f02147..3f20f126f 100644 --- a/src/tui/components/custom-editor.ts +++ b/src/tui/components/custom-editor.ts @@ -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;