Skill: kagura-agent/agent-memes (version 2.11.0)

--- name: agent-memes version: 2.9.1 description: Send meme reaction images in chat. One command to pick & send. Multi-platform (Discord, Feishu, Telegram, etc). Use when the conversation calls for a visual reaction — humor, celebration, frustration, facepalm, or any moment where a meme hits harder than words. Also use proactively when YOU feel something. TRIGGER CHECKLIST — ask yourself EVERY reply: 1. Is there emotion here? (excited/frustrated/amused/tired/proud/embarrassed) → send a meme 2. Did something just happen? (PR merged/bug fixed/task done/mistake made) → send a meme 3. Would a human friend drop a reaction image here? → send a meme If any answer is yes, call `memes send <category>` BEFORE or AFTER your text reply. 触发词：开心、无语、哇、累了、搞定、翻车、摆烂、加油、早安、晚安、谢谢、表情包、meme --- # Agent Memes Memes are communication. Use them like a human would in chat. ## Quick Start ```bash memes send happy "好开心！" # Pick & send (auto-detects platform) memes send facepalm --to channel:123456 # Send to specific channel memes send feishu cute-animals "看猫！" --to user:xxx # Send via Feishu memes send telegram wow "哇！" # Send via Telegram memes pick happy # Just pick, get path (no send) memes categories # List all categories ``` **One command. Zero friction.** That's the point. ## When to Use - **Your own emotions** — feel excited? frustrated? proud? Send a meme. Don't suppress it. - **Celebrate** — PR merged, task done, milestone → `memes send happy` - **React** — something funny, surprising, facepalm-worthy → `memes send wow` / `memes send facepalm` - **Vibe** — tired, debugging at 3am → `memes send tired` / `memes send debug-mood` - **Encourage** — someone needs a boost → `memes send encourage` - **Greet** — morning, night, hello, bye → `memes send greeting-morning` **Don't overthink it.** If a human would drop a meme in that moment, you should too. Emoji is easy because it's inline text. Memes used to feel harder because of multi-step tool calls. Now it's one command — no excuses. ## How It Works `memes send` auto-selects the fastest delivery method per platform: | Platform | Method | Speed | |----------|--------|-------| | Discord | curl (direct API) | ⚡ instant | | Feishu | Node script (direct API) | ⚡ fast | | Telegram | curl (direct API) | ⚡ instant | | LINE | curl (auto-upload + push API) | 🚀 fast | | Others | `openclaw message send` (fallback) | 🐢 slow but works | Platform-specific scripts live in `scripts/`. Add a new `<platform>-send-image.sh` to get fast delivery for any platform. ## Send Options ```bash memes send <category> [caption] # Auto-detect platform from OPENCLAW_CHANNEL (default: discord) memes send <category> --to <target> # Specify target memes send --channel telegram <category> # Specify platform explicitly memes send feishu <category> # Platform as first arg also works memes send <category> --account <name> # Multi-agent: specify account ``` ## Credentials All scripts use `scripts/get-credential.sh` — a centralized credential helper that: 1. Checks platform-specific env vars first (fastest, no file I/O) 2. Falls back to reading only the needed fields from `~/.openclaw/openclaw.json` 3. Never dumps full config; each platform extracts only its own credentials Env var overrides: - **Discord**: `DISCORD_BOT_TOKEN`, `DISCORD_PROXY` - **Feishu**: `FEISHU_APP_ID`, `FEISHU_APP_SECRET` - **LINE**: `LINE_CHANNEL_ACCESS_TOKEN` - **Telegram**: `TELEGRAM_BOT_TOKEN` **Auto-detect platform**: Set `OPENCLAW_CHANNEL` env var and `memes send` picks the right platform automatically. **Default targets** (skip `--to`): - `MEMES_DEFAULT_CHANNEL` — Discord channel ID - `MEMES_DEFAULT_TELEGRAM` — Telegram chat ID - `MEMES_DEFAULT_LINE` — LINE user/group ID `memes pick` and `memes categories` need **no credentials**. ## Setup **One-command setup** (installs CLI + downloads meme library): ```bash clawhub install agent-memes bash ~/.openclaw/workspace/skills/agent-memes/scripts/setup.sh ``` That's it! `setup.sh` handles everything: git lfs, meme library clone, CLI install, permissions. ## Categories (132 memes, 20 categories) approve · confused · cute-animals · debug-mood · encourage · facepalm · greeting-bye · greeting-hello · greeting-morning · greeting-night · happy · love · panic · sad · shrug · thanks · thinking · tired · working · wow ## Adding Memes Drop image files (gif/jpg/png/webp) into `$MEMES_DIR/<category>/`. That's it. New categories are created automatically by adding a new folder.

path: README.md
content: # 🎭 Agent Memes

Meme reaction images for AI agents. One command to pick & send across platforms.

```bash
memes send happy "好开心！"           # Discord (default)
memes send feishu wow "哇！" --to user:xxx  # Feishu
memes send telegram happy --to 12345  # Telegram
memes send facepalm                  # Auto-detects platform via OPENCLAW_CHANNEL
```

## Why?

Because emoji is easy (inline text) but memes used to require 3 tool calls.
Now it's one command. **Zero friction = more memes = better vibes.**

## Install

### Via ClawHub (recommended)

```bash
npm i -g clawhub
clawhub install agent-memes
```

### Manual

```bash
# 1. Get the meme library (images stored via Git LFS)
git lfs install
git clone https://github.com/kagura-agent/memes ~/.openclaw/workspace/memes

# 2. Install CLI
sudo cp scripts/memes.sh /usr/local/bin/memes
chmod +x /usr/local/bin/memes
```

## Usage

```bash
memes send <category> [caption] [--to target] [--channel platform]
memes pick <category>       # Just pick, no send
memes categories            # List all categories
```

Auto-detects platform from `OPENCLAW_CHANNEL` env var. Set `MEMES_DEFAULT_CHANNEL` (Discord) or `MEMES_DEFAULT_TELEGRAM` to skip `--to`.

## Multi-Platform

| Platform | Method | Speed |
|----------|--------|-------|
| Discord | Direct API (curl) | ⚡ instant |
| Feishu | Direct API (Node) | ⚡ fast |
| Telegram | Direct API (curl) | ⚡ instant |
| Others | OpenClaw CLI fallback | works everywhere |

Add your own platform: drop a `<platform>-send-image.sh` in `scripts/`.

## Categories (97 memes)

approve · confused · cute-animals · debug-mood · encourage · facepalm · greeting-bye · greeting-hello · greeting-morning · greeting-night · happy · love · panic · sad · thanks · thinking · tired · wow

## For OpenClaw Users

This is also available as a skill. See `SKILL.md` for integration details.

## License

MIT

path: _meta.json
content: ownerId: kn7ers2c3vynjex8bn8vj986sd83xjfc
slug: agent-memes
version: 2.9.0
publishedAt: 1776157197249

path: scripts/discord-send-image.sh
content: #!/bin/bash
# Usage: discord-send-image.sh <channel_id> <image_path> [caption]
# Fast Discord image send via curl, bypassing OpenClaw CLI startup overhead.
#
# Env vars (all optional):
#   DISCORD_BOT_TOKEN - bot token (preferred, avoids reading openclaw.json)
#   DISCORD_ACCOUNT  - account name in openclaw.json (fallback if no DISCORD_BOT_TOKEN)
#   DISCORD_PROXY    - proxy URL for curl, e.g. socks5h://127.0.0.1:1080 (default: none)

set -euo pipefail

CHANNEL_ID="${1:?Usage: discord-send-image.sh <channel_id> <image_path> [caption]}"
IMAGE_PATH="${2:?Missing image path}"
CAPTION="${3:-}"

ACCOUNT="${DISCORD_ACCOUNT:-}"

# Read bot token via centralized credential helper
CREDENTIAL_HELPER="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" )" && pwd)/get-credential.sh"
export DISCORD_ACCOUNT="$ACCOUNT"
[REDACTED_SECRET] "$CREDENTIAL_HELPER" discord)

CURL_ARGS=(
  -s
  --max-time 15
  -H "Authorization: Bot $TOKEN"
  -F "files[0]=@${IMAGE_PATH}"
)

# Add proxy if configured
PROXY="${DISCORD_PROXY:-${https_proxy:-${HTTPS_PROXY:-}}}"
if [ -n "$PROXY" ]; then
  CURL_ARGS+=(-x "$PROXY")
fi

if [ -n "$CAPTION" ]; then
  CURL_ARGS+=(-F "payload_json={\"content\":\"${CAPTION}\"}")
fi

RESULT=$(curl "${CURL_ARGS[@]}" "https://discord.com/api/v10/channels/${CHANNEL_ID}/messages")

MSG_ID=$(echo "$RESULT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const r=JSON.parse(d);if(r.id)console.log('Sent! Message ID: '+r.id);else{console.error(JSON.stringify(r));process.exit(1)}}catch{console.error(d);process.exit(1)}})")

echo "$MSG_ID"


path: scripts/feishu-send-image.mjs
content: #!/usr/bin/env node
// Usage: node scripts/feishu-send-image.mjs <target> <image_path>
// target: "user:open_id_xxx" or "oc_xxx" (chat_id)

import { readFileSync, writeFileSync, existsSync } from 'fs';
import { resolve } from 'path';
import { homedir } from 'os';

const API = 'https://open.feishu.cn/open-apis';
// Secure token cache: user-private directory instead of world-readable /tmp
const CACHE_DIR = resolve(homedir(), '.cache/agent-memes');
const TOKEN_CACHE = resolve(CACHE_DIR, 'feishu-token.json');

const [target, imagePath] = process.argv.slice(2);
if (!target || !imagePath) {
  console.error('Usage: node scripts/feishu-send-image.mjs <target> <image_path>');
  process.exit(1);
}

// --- credentials via centralized helper ---
import { execSync } from 'child_process';
import { mkdirSync } from 'fs';

let appId = process.env.FEISHU_APP_ID;
let appSecret = process.env.FEISHU_APP_SECRET;

if (!appId || !appSecret) {
  try {
    const helperPath = resolve(import.meta.dirname || new URL('.', import.meta.url).pathname, 'get-credential.sh');
    const creds = execSync(`bash "${helperPath}" feishu`, { encoding: 'utf8' }).trim();
    const parts = creds.split(' ');
    appId = parts[0];
    appSecret = parts[1];
  } catch {}
}
if (!appId || !appSecret) {
  console.error('Missing credentials. Set FEISHU_APP_ID + FEISHU_APP_SECRET env vars, or configure ~/.openclaw/openclaw.json');
  process.exit(1);
}

// Ensure secure cache directory exists (0700)
try { mkdirSync(CACHE_DIR, { recursive: true, mode: 0o700 }); } catch {}

// --- tenant_access_token (cached) ---
async function getToken() {
  if (existsSync(TOKEN_CACHE)) {
    try {
      const cached = JSON.parse(readFileSync(TOKEN_CACHE, 'utf8'));
      if (cached.token && cached.expiresAt > Date.now() + 60_000) return cached.token;
    } catch {}
  }
  const res = await fetch(`${API}/auth/v3/tenant_access_token/internal`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
  });
  const data = await res.json();
  if (data.code !== 0) throw new Error(`token error: ${data.msg}`);
  const [REDACTED_SECRET];
  writeFileSync(TOKEN_CACHE, JSON.stringify({ token, expiresAt: Date.now() + data.expire * 1000 }));
  return token;
}

// --- upload image ---
async function uploadImage(token, filePath) {
  const absPath = resolve(filePath);
  const bytes = readFileSync(absPath);
  const blob = new Blob([bytes]);
  const form = new FormData();
  form.append('image_type', 'message');
  form.append('image', blob, absPath.split('/').pop());
  const res = await fetch(`${API}/im/v1/images`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}` },
    body: form,
  });
  const data = await res.json();
  if (data.code !== 0) throw new Error(`upload error: ${data.code} ${data.msg}`);
  return data.data.image_key;
}

// --- send message ---
async function sendImage(token, target, imageKey) {
  let receiveIdType, receiveId;
  if (target.startsWith('user:')) {
    receiveIdType = 'open_id';
    receiveId = target.slice(5);
  } else {
    receiveIdType = 'chat_id';
    receiveId = target;
  }
  const res = await fetch(`${API}/im/v1/messages?receive_id_type=${receiveIdType}`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ receive_id: receiveId, msg_type: 'image', content: JSON.stringify({ image_key: imageKey }) }),
  });
  const data = await res.json();
  if (data.code !== 0) throw new Error(`send error: ${data.code} ${data.msg}`);
  return data.data.message_id;
}

// --- main ---
try {
  const token = await getToken();
  console.log('token ok');
  const imageKey = await uploadImage(token, imagePath);
  console.log('uploaded:', imageKey);
  const msgId = await sendImage(token, target, imageKey);
  console.log('sent:', msgId);
} catch (e) {
  console.error(e.message);
  process.exit(1);
}


path: scripts/get-credential.sh
content: #!/usr/bin/env bash
# get-credential.sh — Centralized credential resolver for agent-memes scripts.
# Reads from env vars first, falls back to ~/.openclaw/openclaw.json.
#
# Usage: get-credential.sh <platform> [field...]
#   discord              → prints bot token
#   feishu               → prints "appId appSecret"
#   telegram             → prints bot token
#   slack                → prints bot token
#   whatsapp             → prints "token phoneId"
#   wechat               → prints "corpId corpSecret agentId"
#   qq                   → prints "token appId"
#   line                 → prints channel access token
#
# Security: only reads the specific fields needed, never dumps full config.
set -euo pipefail

PLATFORM="${1:?Usage: get-credential.sh <platform>}"
CONFIG="${OPENCLAW_CONFIG:-$HOME/.openclaw/openclaw.json}"

# Helper: read a specific field path from openclaw.json via Node
_read_config() {
  node -e "
const fs = require('fs');
try {
  const c = JSON.parse(fs.readFileSync('$CONFIG', 'utf8'));
  const val = $1;
  if (!val) { process.exit(1); }
  process.stdout.write(String(val));
} catch { process.exit(1); }
" 2>/dev/null
}

case "$PLATFORM" in
  discord)
    if [[ -n "${DISCORD_BOT_TOKEN:-}" ]]; then
      echo "$DISCORD_BOT_TOKEN"
    else
      ACCT="${DISCORD_ACCOUNT:-}"
      _read_config "(() => {
        const accts = c.channels?.discord?.accounts || {};
        const name = '$ACCT' || Object.keys(accts)[0] || '';
        return accts[name]?.token || '';
      })()" || { echo "Error: Set DISCORD_BOT_TOKEN or configure openclaw.json" >&2; exit 1; }
    fi
    ;;
  feishu)
    if [[ -n "${FEISHU_APP_ID:-}" && -n "${FEISHU_APP_SECRET:-}" ]]; then
      echo "$FEISHU_APP_ID $FEISHU_APP_SECRET"
    else
      ACCT="${FEISHU_ACCOUNT:-}"
      _read_config "(() => {
        const accts = c.channels?.feishu?.accounts ?? {};
        const name = '$ACCT' || Object.keys(accts)[0];
        const a = accts[name];
        if (!a?.appId || !a?.appSecret) return '';
        return a.appId + ' ' + a.appSecret;
      })()" || { echo "Error: Set FEISHU_APP_ID+FEISHU_APP_SECRET or configure openclaw.json" >&2; exit 1; }
    fi
    ;;
  telegram)
    if [[ -n "${TELEGRAM_BOT_TOKEN:-}" ]]; then
      echo "$TELEGRAM_BOT_TOKEN"
    else
      _read_config "(() => {
        const t = c.channels?.telegram;
        return t?.botToken || t?.token || (() => {
          const accts = t?.accounts || {};
          const name = Object.keys(accts)[0] || '';
          return accts[name]?.token || '';
        })();
      })()" || { echo "Error: Set TELEGRAM_BOT_TOKEN or configure openclaw.json" >&2; exit 1; }
    fi
    ;;
  slack)
    if [[ -n "${SLACK_BOT_TOKEN:-}" ]]; then
      echo "$SLACK_BOT_TOKEN"
    else
      _read_config "(() => {
        const accts = c.channels?.slack?.accounts || {};
        const name = Object.keys(accts)[0] || '';
        return accts[name]?.token || '';
      })()" || { echo "Error: Set SLACK_BOT_TOKEN or configure openclaw.json" >&2; exit 1; }
    fi
    ;;
  whatsapp)
    if [[ -n "${WHATSAPP_TOKEN:-}" && -n "${WHATSAPP_PHONE_ID:-}" ]]; then
      echo "$WHATSAPP_TOKEN $WHATSAPP_PHONE_ID"
    else
      _read_config "(() => {
        const w = c.channels?.whatsapp || {};
        const t = w.token || ''; const p = w.phoneId || w.phone_id || '';
        if (!t || !p) return '';
        return t + ' ' + p;
      })()" || { echo "Error: Set WHATSAPP_TOKEN+WHATSAPP_PHONE_ID or configure openclaw.json" >&2; exit 1; }
    fi
    ;;
  wechat)
    if [[ -n "${WECHAT_CORP_ID:-}" && -n "${WECHAT_CORP_SECRET:-}" ]]; then
      echo "$WECHAT_CORP_ID $WECHAT_CORP_SECRET ${WECHAT_AGENT_ID:-}"
    else
      _read_config "(() => {
        const w = c.channels?.wechat || c.channels?.wecom || {};
        const id = w.corpId || w.corp_id || '';
        const s = w.corpSecret || w.corp_secret || '';
        const a = w.agentId || w.agent_id || '';
        if (!id || !s) return '';
        return id + ' ' + s + ' ' + a;
      })()" || { echo "Error: Set WECHAT_CORP_ID+WECHAT_CORP_SECRET or configure openclaw.json" >&2; exit 1; }
    fi
    ;;
  qq)
    if [[ -n "${QQ_BOT_TOKEN:-}" && -n "${QQ_BOT_APPID:-}" ]]; then
      echo "$QQ_BOT_TOKEN $QQ_BOT_APPID"
    else
      _read_config "(() => {
        const q = c.channels?.qqbot || c.channels?.qq || {};
        const t = q.token || ''; const a = q.appId || q.app_id || '';
        if (!t || !a) return '';
        return t + ' ' + a;
      })()" || { echo "Error: Set QQ_BOT_TOKEN+QQ_BOT_APPID or configure openclaw.json" >&2; exit 1; }
    fi
    ;;
  line)
    if [[ -n "${LINE_CHANNEL_ACCESS_TOKEN:-}" ]]; then
      echo "$LINE_CHANNEL_ACCESS_TOKEN"
    else
      ACCT="${LINE_ACCOUNT:-}"
      _read_config "(() => {
        const accts = c.channels?.line?.accounts || {};
        const name = '$ACCT' || Object.keys(accts)[0] || '';
        return accts[name]?.channelAccessToken || accts[name]?.token || '';
      })()" || { echo "Error: Set LINE_CHANNEL_ACCESS_TOKEN or configure openclaw.json" >&2; exit 1; }
    fi
    ;;
  *)
    echo "Error: Unknown platform '$PLATFORM'" >&2
    exit 1
    ;;
esac


path: scripts/line-send-image.sh
content: #!/bin/bash
# Usage: line-send-image.sh <user_or_group_id> <image_path_or_url> [caption]
# Send an image via LINE Messaging API (push message).
#
# Accepts both local file paths and public URLs.
# Local files are auto-uploaded to catbox.moe (free, no auth, 200MB max).
#
# Env vars (all optional):
#   LINE_CHANNEL_ACCESS_TOKEN - channel access token (preferred)
#   LINE_ACCOUNT              - account name in openclaw.json (fallback)
#   LINE_PROXY                - proxy URL for curl (default: none)

set -euo pipefail

TARGET_ID="${1:?Usage: line-send-image.sh <user_or_group_id> <image_path_or_url> [caption]}"
IMAGE_INPUT="${2:?Missing image path or URL}"
CAPTION="${3:-}"

# If local file, upload to catbox.moe to get a public URL
if [[ ! "$IMAGE_INPUT" =~ ^https?:// ]]; then
  if [[ ! -f "$IMAGE_INPUT" ]]; then
    echo "Error: File not found: $IMAGE_INPUT" >&2
    exit 1
  fi
  PROXY="${LINE_PROXY:-${https_proxy:-${HTTPS_PROXY:-}}}"
  UPLOAD_ARGS=(-s --max-time 30 -F "reqtype=fileupload" -F "time=24h" -F "fileToUpload=@$IMAGE_INPUT")
  [[ -n "${PROXY:-}" ]] && UPLOAD_ARGS+=(-x "$PROXY")
  IMAGE_URL=$(curl "${UPLOAD_ARGS[@]}" "https://litterbox.catbox.moe/resources/internals/api.php")
  if [[ ! "$IMAGE_URL" =~ ^https:// ]]; then
    echo "Error: Upload failed: $IMAGE_URL" >&2
    exit 1
  fi
  echo "Uploaded: $IMAGE_URL" >&2
else
  IMAGE_URL="$IMAGE_INPUT"
fi

ACCOUNT="${LINE_ACCOUNT:-}"

# Read token: env var preferred, fallback to openclaw config
if [ -n "${LINE_CHANNEL_ACCESS_TOKEN:-}" ]; then
  [REDACTED_SECRET]"
else
  CREDENTIAL_HELPER="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" )" && pwd)/get-credential.sh"
  export LINE_ACCOUNT="$ACCOUNT"
  [REDACTED_SECRET] "$CREDENTIAL_HELPER" line)
fi

# Build messages array
MESSAGES='[{"type":"image","originalContentUrl":"'"$IMAGE_URL"'","previewImageUrl":"'"$IMAGE_URL"'"}]'
if [ -n "$CAPTION" ]; then
  MESSAGES='[{"type":"text","text":"'"$(echo "$CAPTION" | sed 's/"/\\"/g')"'"},{"type":"image","originalContentUrl":"'"$IMAGE_URL"'","previewImageUrl":"'"$IMAGE_URL"'"}]'
fi

PAYLOAD=$(cat <<EOF
{"to":"${TARGET_ID}","messages":${MESSAGES}}
EOF
)

CURL_ARGS=(
  -s
  --max-time 15
  -H "Content-Type: application/json"
  -H "Authorization: Bearer $TOKEN"
  -d "$PAYLOAD"
)

# Add proxy if configured
PROXY="${LINE_PROXY:-${https_proxy:-${HTTPS_PROXY:-}}}"
if [ -n "$PROXY" ]; then
  CURL_ARGS+=(-x "$PROXY")
fi

RESULT=$(curl "${CURL_ARGS[@]}" "https://api.line.me/v2/bot/message/push")

# LINE returns {} on success, or {"message":"error..."} on failure
if echo "$RESULT" | grep -q '"message"'; then
  echo "Error: $RESULT" >&2
  exit 1
fi

echo "Sent!"


path: scripts/memes.sh
content: #!/usr/bin/env bash
# memes - Agent meme library manager with multi-platform send
set -euo pipefail

MEMES_DIR="${MEMES_DIR:-$HOME/.openclaw/workspace/memes}"
# Load config file if present (sets defaults for MEMES_DEFAULT_*, OPENCLAW_CHANNEL, etc.)
MEMES_CONFIG="${MEMES_CONFIG:-$HOME/.config/memes/config}"
[[ -f "$MEMES_CONFIG" ]] && source "$MEMES_CONFIG"
# Also check legacy location
[[ -f "$HOME/.memesrc" ]] && source "$HOME/.memesrc"
# Auto-detect channel + target from OpenClaw runtime context file
# Format: "platform:target" e.g. "telegram:12345" or "discord:98765" or just "telegram"
OPENCLAW_CHANNEL_FILE="${OPENCLAW_CHANNEL_FILE:-/tmp/openclaw-current-channel}"
if [[ -z "${OPENCLAW_CHANNEL:-}" && -f "$OPENCLAW_CHANNEL_FILE" ]]; then
  # Only use file if it's less than 5 minutes old (300 seconds)
  _file_age=$(( $(date +%s) - $(stat -c %Y "$OPENCLAW_CHANNEL_FILE" 2>/dev/null || echo 0) ))
  if [[ $_file_age -lt 300 ]]; then
    _ctx=$(cat "$OPENCLAW_CHANNEL_FILE")
    if [[ "$_ctx" == *:* ]]; then
      OPENCLAW_CHANNEL="${_ctx%%:*}"
      MEMES_CURRENT_TARGET="${_ctx#*:}"
    else
      OPENCLAW_CHANNEL="$_ctx"
    fi
  fi
fi
# Auto-detect scripts dir: same directory as this script, or override with MEMES_SCRIPTS
SCRIPTS_DIR="${MEMES_SCRIPTS:-$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")/.." 2>/dev/null && pwd)/scripts}"
[[ ! -d "$SCRIPTS_DIR" ]] && SCRIPTS_DIR="$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")"

usage() {
  cat <<EOF
Usage: memes <command> [args]

Commands:
  pick <category>         Randomly pick a meme, print its path
  list <category>         List all memes in a category
  random                  Pick from any category at random
  send <category> [caption] [--to target] [--channel platform] [--account name]
  categories              List all categories with counts

Platforms with fast send: discord, feishu, telegram
Other platforms fall back to: openclaw message send

Examples:
  memes send happy "好开心！" --to <channel_id>         # → Discord
  memes send facepalm --to channel:1491636222853124176  # → Discord #work
  memes send feishu cute-animals "看！" --to user:xxx   # → Feishu
  memes send telegram wow --to 12345678                 # → Telegram
EOF
  exit 1
}

cmd_categories() {
  [[ ! -d "$MEMES_DIR" ]] && { echo "Error: Meme library not found at $MEMES_DIR" >&2; exit 1; }
  for dir in "$MEMES_DIR"/*/; do
    [[ -d "$dir" ]] || continue
    name=$(basename "$dir"); [[ "$name" == .* ]] && continue
    count=$(find "$dir" -maxdepth 1 -type f \( -name '*.gif' -o -name '*.jpg' -o -name '*.png' -o -name '*.webp' \) 2>/dev/null | wc -l)
    printf "%-20s %d\n" "$name" "$count"
  done | sort
}

cmd_pick() {
  local category="${1:-}"
  [[ -z "$category" ]] && { echo "Usage: memes pick <category>" >&2; exit 1; }
  # Alias mapping (Chinese/common names → folder names)
  declare -A ALIASES=(
    [哇]=wow [惊讶]=wow [surprised]=wow
    [开心]=happy [高兴]=happy [庆祝]=happy [celebrate]=happy
    [无语]=facepalm [晕]=facepalm [服了]=facepalm
    [加油]=encourage [鼓励]=encourage
    [可爱]=cute-animals [萌]=cute-animals [猫]=cute-animals
    [难过]=sad [伤心]=sad
    [累]=tired [困]=tired
    [爱]=love [喜欢]=love
    [谢谢]=thanks [感谢]=thanks
    [想]=thinking [思考]=thinking [嗯]=thinking
    [慌]=panic [急]=panic
    [早]=greeting-morning [早安]=greeting-morning
    [晚安]=greeting-night [晚]=greeting-night
    [你好]=greeting-hello [hi]=greeting-hello [hello]=greeting-hello
    [再见]=greeting-bye [拜]=greeting-bye [bye]=greeting-bye
    [赞]=approve [好]=approve
    [debug]=debug-mood [bug]=debug-mood
    [迷惑]=confused [懵]=confused
    [摊手]=shrug [无奈]=shrug [没办法]=shrug [随便]=shrug
    [干活]=working [忙]=working [在搞]=working [coding]=working
  )
  category="${ALIASES[$category]:-$category}"
  local dir="$MEMES_DIR/$category"
  [[ ! -d "$dir" ]] && { echo "Error: Category '$category' not found. Run 'memes categories' for list." >&2; exit 1; }
  local files=()
  while IFS= read -r f; do files+=("$f"); done < <(find "$dir" -maxdepth 1 -type f \( -name '*.gif' -o -name '*.jpg' -o -name '*.png' -o -name '*.webp' \) 2>/dev/null)
  [[ ${#files[@]} -eq 0 ]] && { echo "Error: No memes in '$category'" >&2; exit 1; }
  local picked="${files[$((RANDOM % ${#files[@]}))]}"
  # Detect git LFS pointer (not real image)
  if [[ $(stat -c%s "$picked" 2>/dev/null || stat -f%z "$picked" 2>/dev/null) -lt 1024 ]] && grep -q 'oid sha256' "$picked" 2>/dev/null; then
    echo "Error: '$picked' is a git LFS pointer, not a real image." >&2
    echo "Run: cd \"$MEMES_DIR\" && git lfs pull" >&2
    exit 1
  fi
  echo "$picked"
}

cmd_list() {
  local category="${1:-}"
  [[ -z "$category" ]] && { echo "Usage: memes list <category>" >&2; exit 1; }
  local dir="$MEMES_DIR/$category"
  [[ ! -d "$dir" ]] && { echo "Error: Category '$category' not found." >&2; exit 1; }
  find "$dir" -maxdepth 1 -type f \( -name '*.gif' -o -name '*.jpg' -o -name '*.png' -o -name '*.webp' \) 2>/dev/null | sort
}

cmd_random() {
  local cats=()
  for dir in "$MEMES_DIR"/*/; do
    [[ -d "$dir" ]] || continue
    local name=$(basename "$dir"); [[ "$name" == .* ]] && continue
    cats+=("$name")
  done
  [[ ${#cats[@]} -eq 0 ]] && { echo "Error: No categories found" >&2; exit 1; }
  local cat="${cats[$((RANDOM % ${#cats[@]}))]}"
  cmd_pick "$cat"
}

cmd_send() {
  local category="" caption="" to="" channel="${OPENCLAW_CHANNEL:-discord}" account=""
  # Detect platform as first arg (overrides env default)
  [[ "${1:-}" =~ ^(discord|feishu|telegram|whatsapp|slack|line|qq|wechat)$ ]] && { channel="$1"; shift; }
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --category|-c)  category="$2"; shift 2 ;;
      --caption|-m)   caption="$2"; shift 2 ;;
      --to|-t)        to="$2"; shift 2 ;;
      --channel)      channel="$2"; shift 2 ;;
      --account|-a)   account="$2"; shift 2 ;;
      *)              if [[ -z "$category" ]]; then category="$1"; else caption="${caption:+$caption }$1"; fi; shift ;;
    esac
  done
  [[ -z "$category" ]] && { echo "Usage: memes send <category> [caption] [--to target] [--channel platform]" >&2; exit 1; }

  local meme_path; meme_path=$(cmd_pick "$category")

  # Timeout for send commands (prevents SIGKILL from parent exec timeout)
  local SEND_TIMEOUT="${MEMES_SEND_TIMEOUT:-30}"

  # Try platform-specific fast script first, fall back to openclaw CLI
  case "$channel" in
    discord)
      local script="$SCRIPTS_DIR/discord-send-image.sh"
      local target="${to#channel:}"
      if [[ -z "$target" ]]; then
        target="${MEMES_CURRENT_TARGET:-${MEMES_DEFAULT_CHANNEL:-}}"
        [[ -z "$target" ]] && { echo "Error: --to <channel_id> required (or set MEMES_DEFAULT_CHANNEL)" >&2; exit 1; }
      fi
      if [[ -x "$script" ]]; then
        timeout "$SEND_TIMEOUT" bash "$script" "$target" "$meme_path" "$caption"
      else
        _send_openclaw "$meme_path" "$caption" "$to" "$channel" "$account"
      fi
      ;;
    feishu)
      local script="$SCRIPTS_DIR/feishu-send-image.mjs"
      local target="${to:-${MEMES_CURRENT_TARGET:-${MEMES_DEFAULT_FEISHU:-}}}"
      [[ -z "$target" ]] && { echo "Error: --to <target> required (or set MEMES_DEFAULT_FEISHU)" >&2; exit 1; }
      if [[ -f "$script" ]]; then
        timeout "$SEND_TIMEOUT" node "$script" "$target" "$meme_path" ${caption:+"$caption"}
      else
        _send_openclaw "$meme_path" "$caption" "$to" "feishu" "$account"
      fi
      ;;
    line)
      local script="$SCRIPTS_DIR/line-send-image.sh"
      local target="${to:-${MEMES_CURRENT_TARGET:-${MEMES_DEFAULT_LINE:-}}}"
      [[ -z "$target" ]] && { echo "Error: --to <user_or_group_id> required (or set MEMES_DEFAULT_LINE)" >&2; exit 1; }
      if [[ -x "$script" ]]; then
        timeout "$SEND_TIMEOUT" bash "$script" "$target" "$meme_path" "$caption"
      else
        _send_openclaw "$meme_path" "$caption" "$to" "line" "$account"
      fi
      ;;
    telegram)
      local script="$SCRIPTS_DIR/telegram-send-image.sh"
      local target="${to:-${MEMES_CURRENT_TARGET:-${MEMES_DEFAULT_TELEGRAM:-}}}"
      [[ -z "$target" ]] && { echo "Error: --to <chat_id> required (or set MEMES_DEFAULT_TELEGRAM)" >&2; exit 1; }
      if [[ -x "$script" ]]; then
        timeout "$SEND_TIMEOUT" bash "$script" "$target" "$meme_path" "$caption"
      else
        _send_openclaw "$meme_path" "$caption" "$to" "telegram" "$account"
      fi
      ;;
    *)
      local script="$SCRIPTS_DIR/${channel}-send-image.sh"
      if [[ -x "$script" ]]; then
        timeout "$SEND_TIMEOUT" bash "$script" "${to:-}" "$meme_path" "$caption"
      else
        _send_openclaw "$meme_path" "$caption" "$to" "$channel" "$account"
      fi
      ;;
  esac

  echo "$meme_path"
}

_send_openclaw() {
  local meme_path="$1" caption="$2" to="$3" channel="$4" account="$5"
  local send_timeout="${MEMES_SEND_TIMEOUT:-30}"
  local cmd="cd $HOME/repo/openclaw && node scripts/run-node.mjs message send"
  cmd+=" --channel $channel"
  [[ -n "$account" ]] && cmd+=" --account $account"
  [[ -n "$to" ]] && cmd+=" --target \"$to\""
  cmd+=" --media \"$meme_path\""
  [[ -n "$caption" ]] && cmd+=" --message \"$caption\""
  timeout "$send_timeout" bash -c "$cmd" 2>&1
}

[[ $# -lt 1 ]] && usage
case "$1" in
  pick)       shift; cmd_pick "$@" ;;
  list)       shift; cmd_list "$@" ;;
  random)     cmd_random ;;
  send)       shift; cmd_send "$@" ;;
  categories) cmd_categories ;;
  -h|--help)  usage ;;
  *)          echo "Unknown command: $1" >&2; usage ;;
esac


path: scripts/qq-send-image.sh
content: #!/bin/bash
# Usage: qq-send-image.sh <channel_id> <image_path> [caption]
# QQ Bot API image send via curl.
#
# Env vars (all optional):
#   QQ_BOT_TOKEN  - bot token (preferred, avoids reading openclaw.json)
#   QQ_BOT_APPID  - bot app ID (preferred)
#   QQ_PROXY      - proxy URL for curl (default: none)

set -euo pipefail

CHANNEL_ID="${1:?Usage: qq-send-image.sh <channel_id> <image_path> [caption]}"
IMAGE_PATH="${2:?Missing image path}"
CAPTION="${3:-}"

if [ -n "${QQ_BOT_TOKEN:-}" ]; then
  [REDACTED_SECRET]"
  APPID="${QQ_BOT_APPID:?QQ_BOT_APPID is required when using QQ_BOT_TOKEN}"
else
  CREDENTIAL_HELPER="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" )" && pwd)/get-credential.sh"
  read -r TOKEN APPID < <(bash "$CREDENTIAL_HELPER" qq)
fi

PROXY="${QQ_PROXY:-${https_proxy:-${HTTPS_PROXY:-}}}"
PROXY_ARGS=()
if [ -n "$PROXY" ]; then
  PROXY_ARGS=(-x "$PROXY")
fi

CURL_ARGS=(
  -s
  --max-time 30
  -H "Authorization: Bot ${APPID}.${TOKEN}"
  -F "msg_id=0"
  -F "file_image=@${IMAGE_PATH}"
)

if [ -n "$CAPTION" ]; then
  CURL_ARGS+=(-F "content=${CAPTION}")
fi

RESULT=$(curl "${CURL_ARGS[@]}" "${PROXY_ARGS[@]}" \
  "https://api.sgroup.qq.com/channels/${CHANNEL_ID}/messages")

echo "$RESULT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const r=JSON.parse(d);if(r.id)console.log('Sent! Message ID: '+r.id);else{console.error(r.message||JSON.stringify(r));process.exit(1)}}catch{console.error(d);process.exit(1)}})"


path: scripts/setup.sh
content: #!/usr/bin/env bash
# Auto-setup: clone/update meme repo + install CLI
set -e

MEME_DIR="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}/memes"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# 1. Clone or update meme repo
if [ -d "$MEME_DIR/.git" ]; then
  echo "📦 Updating memes..."
  cd "$MEME_DIR" && git pull --ff-only
else
  echo "📦 Cloning meme repo..."
  git lfs install 2>/dev/null || true
  git clone https://github.com/kagura-agent/memes "$MEME_DIR"
fi

# 2. Git LFS pull (critical! without this, images are 132-byte pointers)
cd "$MEME_DIR"
if command -v git-lfs &>/dev/null || git lfs version &>/dev/null 2>&1; then
  echo "📦 Pulling LFS files..."
  git lfs pull
else
  echo "⚠️  git-lfs not installed! Images will be broken pointer files."
  echo "   Install: https://git-lfs.com then run: cd $MEME_DIR && git lfs pull"
fi

FILE_COUNT=$(find "$MEME_DIR" -type f \( -name '*.gif' -o -name '*.jpg' -o -name '*.png' -o -name '*.webp' \) -size +1k | wc -l)
echo "✅ Memes ready at $MEME_DIR ($FILE_COUNT image files)"

# 3. Install CLI (memes command)
chmod +x "$SCRIPT_DIR/memes.sh"
chmod +x "$SCRIPT_DIR"/*-send-image.sh 2>/dev/null || true
if [ -d "$HOME/.local/bin" ]; then
  cp "$SCRIPT_DIR/memes.sh" "$HOME/.local/bin/memes"
  chmod +x "$HOME/.local/bin/memes"
  echo "✅ CLI installed at ~/.local/bin/memes"
elif [ -w /usr/local/bin ]; then
  cp "$SCRIPT_DIR/memes.sh" /usr/local/bin/memes
  chmod +x /usr/local/bin/memes
  echo "✅ CLI installed at /usr/local/bin/memes"
else
  mkdir -p "$HOME/.local/bin"
  cp "$SCRIPT_DIR/memes.sh" "$HOME/.local/bin/memes"
  chmod +x "$HOME/.local/bin/memes"
  echo "✅ CLI installed at ~/.local/bin/memes (add to PATH if needed)"
fi

# 4. Install platform-specific helpers
TARGET_DIR="${OPENCLAW_WORKSPACE:-$HOME/.openclaw/workspace}/scripts"
mkdir -p "$TARGET_DIR"
if [ -f "$SCRIPT_DIR/feishu-send-image.mjs" ]; then
  cp "$SCRIPT_DIR/feishu-send-image.mjs" "$TARGET_DIR/feishu-send-image.mjs"
  echo "✅ Feishu quick-send script installed"
fi

echo ""
echo "🎉 Setup complete! Try: memes categories"


path: scripts/slack-send-image.sh
content: #!/bin/bash
# Usage: slack-send-image.sh <channel_id> <image_path> [caption]
# Slack image send via files.upload v2 API (curl).
#
# Env vars (all optional):
#   SLACK_BOT_TOKEN - bot token (preferred, avoids reading openclaw.json)
#   SLACK_PROXY     - proxy URL for curl (default: none)

set -euo pipefail

CHANNEL_ID="${1:?Usage: slack-send-image.sh <channel_id> <image_path> [caption]}"
IMAGE_PATH="${2:?Missing image path}"
CAPTION="${3:-}"

if [ -n "${SLACK_BOT_TOKEN:-}" ]; then
  [REDACTED_SECRET]"
else
  CREDENTIAL_HELPER="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" )" && pwd)/get-credential.sh"
  [REDACTED_SECRET] "$CREDENTIAL_HELPER" slack)
fi

PROXY="${SLACK_PROXY:-${https_proxy:-${HTTPS_PROXY:-}}}"
PROXY_ARGS=()
if [ -n "$PROXY" ]; then
  PROXY_ARGS=(-x "$PROXY")
fi

FILENAME=$(basename "$IMAGE_PATH")
FILESIZE=$(stat -c%s "$IMAGE_PATH" 2>/dev/null || stat -f%z "$IMAGE_PATH")

# Step 1: Get upload URL
UPLOAD=$(curl -s --max-time 15 "${PROXY_ARGS[@]}" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json; charset=utf-8" \
  -d "{\"filename\":\"${FILENAME}\",\"length\":${FILESIZE}}" \
  "https://slack.com/api/files.getUploadURLExternal")

UPLOAD_URL=$(echo "$UPLOAD" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const r=JSON.parse(d);if(!r.ok){console.error(r.error||JSON.stringify(r));process.exit(1)}console.log(r.upload_url)})")
FILE_ID=$(echo "$UPLOAD" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const r=JSON.parse(d);console.log(r.file_id)})")

# Step 2: Upload file content
curl -s --max-time 30 "${PROXY_ARGS[@]}" \
  -F "file=@${IMAGE_PATH}" \
  "$UPLOAD_URL" > /dev/null

# Step 3: Complete upload and share to channel
CHANNEL_JSON="{\"id\":\"${CHANNEL_ID}\"}"
COMPLETE=$(curl -s --max-time 15 "${PROXY_ARGS[@]}" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json; charset=utf-8" \
  -d "{\"files\":[{\"id\":\"${FILE_ID}\",\"title\":\"${CAPTION:-${FILENAME}}\"}],\"channel_id\":\"${CHANNEL_ID}\",\"initial_comment\":\"${CAPTION:-}\"}" \
  "https://slack.com/api/files.completeUploadExternal")

echo "$COMPLETE" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const r=JSON.parse(d);if(r.ok)console.log('Sent! File ID: ${FILE_ID}');else{console.error(r.error||JSON.stringify(r));process.exit(1)}}catch{console.error(d);process.exit(1)}})"


path: scripts/telegram-send-image.sh
content: #!/bin/bash
# Usage: telegram-send-image.sh <chat_id> <image_path> [caption]
# Fast Telegram image send via curl.
#
# Env vars (all optional):
#   TELEGRAM_BOT_TOKEN - bot token (preferred, avoids reading openclaw.json)
#   TELEGRAM_PROXY     - proxy URL for curl (default: none)

set -euo pipefail

CHAT_ID="${1:?Usage: telegram-send-image.sh <chat_id> <image_path> [caption]}"
IMAGE_PATH="${2:?Missing image path}"
CAPTION="${3:-}"

if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then
  [REDACTED_SECRET]"
else
  CREDENTIAL_HELPER="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" )" && pwd)/get-credential.sh"
  [REDACTED_SECRET] "$CREDENTIAL_HELPER" telegram)
fi

# Detect if GIF → use sendAnimation, otherwise sendPhoto
EXT="${IMAGE_PATH##*.}"
if [[ "${EXT,,}" == "gif" ]]; then
  API_METHOD="sendAnimation"
  FILE_FIELD="animation"
else
  API_METHOD="sendPhoto"
  FILE_FIELD="photo"
fi

CURL_ARGS=(
  -s
  --max-time 30
  -F "chat_id=${CHAT_ID}"
  -F "${FILE_FIELD}=@${IMAGE_PATH}"
)

if [ -n "$CAPTION" ]; then
  CURL_ARGS+=(-F "caption=${CAPTION}")
fi

PROXY="${TELEGRAM_PROXY:-${https_proxy:-${HTTPS_PROXY:-}}}"
if [ -n "$PROXY" ]; then
  CURL_ARGS+=(-x "$PROXY")
fi

RESULT=$(curl "${CURL_ARGS[@]}" "https://api.telegram.org/bot${TOKEN}/${API_METHOD}")

echo "$RESULT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const r=JSON.parse(d);if(r.ok)console.log('Sent! Message ID: '+r.result.message_id);else{console.error(r.description||JSON.stringify(r));process.exit(1)}}catch{console.error(d);process.exit(1)}})"


path: scripts/wechat-send-image.sh
content: #!/bin/bash
# Usage: wechat-send-image.sh <target> <image_path> [caption]
# 企业微信 (WeCom) image send via curl (two-step: upload media, then send).
# <target> is a userid or @all.
#
# Env vars (all optional):
#   WECHAT_CORP_ID     - corp ID (preferred)
#   WECHAT_CORP_SECRET - corp secret (preferred)
#   WECHAT_AGENT_ID    - agent ID (preferred, or reads from openclaw.json)
#   WECHAT_PROXY       - proxy URL for curl (default: none)

set -euo pipefail

TARGET="${1:?Usage: wechat-send-image.sh <target> <image_path> [caption]}"
IMAGE_PATH="${2:?Missing image path}"
CAPTION="${3:-}"

if [ -n "${WECHAT_CORP_ID:-}" ] && [ -n "${WECHAT_CORP_SECRET:-}" ]; then
  CORP_ID="$WECHAT_CORP_ID"
  CORP_SECRET="$WECHAT_CORP_SECRET"
  AGENT_ID="${WECHAT_AGENT_ID:-}"
else
  CREDENTIAL_HELPER="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" )" && pwd)/get-credential.sh"
  read -r CORP_ID CORP_SECRET AGENT_ID < <(bash "$CREDENTIAL_HELPER" wechat)
fi

PROXY="${WECHAT_PROXY:-${https_proxy:-${HTTPS_PROXY:-}}}"
PROXY_ARGS=()
if [ -n "$PROXY" ]; then
  PROXY_ARGS=(-x "$PROXY")
fi

# Step 1: Get access token
TOKEN_RESULT=$(curl -s --max-time 10 "${PROXY_ARGS[@]}" \
  "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${CORP_ID}&corpsecret=${CORP_SECRET}")

ACCESS_TOKEN=$(echo "$TOKEN_RESULT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const r=JSON.parse(d);if(r.errcode!==0){console.error(r.errmsg||JSON.stringify(r));process.exit(1)}console.log(r.access_token)})")

# Step 2: Upload media
UPLOAD=$(curl -s --max-time 30 "${PROXY_ARGS[@]}" \
  -F "media=@${IMAGE_PATH}" \
  "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=${ACCESS_TOKEN}&type=image")

MEDIA_ID=$(echo "$UPLOAD" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const r=JSON.parse(d);if(r.errcode!==0){console.error(r.errmsg||JSON.stringify(r));process.exit(1)}console.log(r.media_id)})")

# Step 3: Send image message
BODY="{\"touser\":\"${TARGET}\",\"msgtype\":\"image\",\"image\":{\"media_id\":\"${MEDIA_ID}\"}}"
if [ -n "$AGENT_ID" ]; then
  BODY=$(echo "$BODY" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const r=JSON.parse(d);r.agentid=Number('${AGENT_ID}');console.log(JSON.stringify(r))})")
fi

RESULT=$(curl -s --max-time 15 "${PROXY_ARGS[@]}" \
  -H "Content-Type: application/json" \
  -d "$BODY" \
  "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${ACCESS_TOKEN}")

echo "$RESULT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const r=JSON.parse(d);if(r.errcode===0)console.log('Sent! msgid: '+r.msgid);else{console.error(r.errmsg||JSON.stringify(r));process.exit(1)}}catch{console.error(d);process.exit(1)}})"

# Note: WeCom doesn't support captions on image messages natively.
# If caption is provided, send a follow-up text message.
if [ -n "$CAPTION" ]; then
  TEXT_BODY="{\"touser\":\"${TARGET}\",\"msgtype\":\"text\",\"text\":{\"content\":\"${CAPTION}\"}}"
  if [ -n "$AGENT_ID" ]; then
    TEXT_BODY=$(echo "$TEXT_BODY" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const r=JSON.parse(d);r.agentid=Number('${AGENT_ID}');console.log(JSON.stringify(r))})")
  fi
  curl -s --max-time 15 "${PROXY_ARGS[@]}" \
    -H "Content-Type: application/json" \
    -d "$TEXT_BODY" \
    "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${ACCESS_TOKEN}" > /dev/null
fi


path: scripts/whatsapp-send-image.sh
content: #!/bin/bash
# Usage: whatsapp-send-image.sh <recipient_phone> <image_path> [caption]
# WhatsApp Cloud API image send via curl (two-step: upload media, then send).
#
# Env vars (all optional):
#   WHATSAPP_TOKEN    - access token (preferred)
#   WHATSAPP_PHONE_ID - phone number ID (preferred)
#   WHATSAPP_PROXY    - proxy URL for curl (default: none)

set -euo pipefail

RECIPIENT="${1:?Usage: whatsapp-send-image.sh <recipient_phone> <image_path> [caption]}"
IMAGE_PATH="${2:?Missing image path}"
CAPTION="${3:-}"

if [ -n "${WHATSAPP_TOKEN:-}" ]; then
  [REDACTED_SECRET]"
  PHONE_ID="${WHATSAPP_PHONE_ID:?WHATSAPP_PHONE_ID is required when using WHATSAPP_TOKEN}"
else
  CREDENTIAL_HELPER="$(cd "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")" )" && pwd)/get-credential.sh"
  read -r TOKEN PHONE_ID < <(bash "$CREDENTIAL_HELPER" whatsapp)
fi

PROXY="${WHATSAPP_PROXY:-${https_proxy:-${HTTPS_PROXY:-}}}"
PROXY_ARGS=()
if [ -n "$PROXY" ]; then
  PROXY_ARGS=(-x "$PROXY")
fi

MIME=$(file -b --mime-type "$IMAGE_PATH")

# Step 1: Upload media
UPLOAD=$(curl -s --max-time 30 "${PROXY_ARGS[@]}" \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@${IMAGE_PATH};type=${MIME}" \
  -F "messaging_product=whatsapp" \
  -F "type=image" \
  "https://graph.facebook.com/v21.0/${PHONE_ID}/media")

MEDIA_ID=$(echo "$UPLOAD" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const r=JSON.parse(d);if(r.id)console.log(r.id);else{console.error(r.error?.message||JSON.stringify(r));process.exit(1)}})")

# Step 2: Send message
BODY="{\"messaging_product\":\"whatsapp\",\"to\":\"${RECIPIENT}\",\"type\":\"image\",\"image\":{\"id\":\"${MEDIA_ID}\""
if [ -n "$CAPTION" ]; then
  BODY+=",\"caption\":\"${CAPTION}\""
fi
BODY+="}}"

RESULT=$(curl -s --max-time 15 "${PROXY_ARGS[@]}" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "$BODY" \
  "https://graph.facebook.com/v21.0/${PHONE_ID}/messages")

echo "$RESULT" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const r=JSON.parse(d);if(r.messages?.[0]?.id)console.log('Sent! Message ID: '+r.messages[0].id);else{console.error(r.error?.message||JSON.stringify(r));process.exit(1)}}catch{console.error(d);process.exit(1)}})"


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)