chore: add Zoho Cliq docs, VPS scripts, and leaf config schema

This commit is contained in:
Kastrah 2026-01-26 11:34:21 +01:00
parent b35ceca265
commit cf20305894
8 changed files with 815 additions and 9 deletions

192
docs/channels/zoho-cliq.md Normal file
View 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
View 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 - USERs 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
View 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
View 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 "═══════════════════════════════════════════════════════════════════════════"

View File

@ -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 = {

View File

@ -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;

View 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();

View File

@ -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;