Skill: evolinkai/slack-assistant (version 1.0.0)

--- name: Slack Assistant description: Slack API integration with smart AI features — send messages, read channels, search conversations, and manage workspaces with Claude-powered summarization and drafting. Powered by evolink.ai --- # Slack Assistant Send messages, read channels, search conversations, and manage your Slack workspace with AI-powered features. Summarize channels, draft replies, and prioritize important messages — all from your terminal. Powered by [Evolink.ai](https://evolink.ai?utm_source=clawhub&utm_medium=skill&utm_campaign=slack) ## When to Use - User says "send a Slack message", "post to #channel" - User wants to read channel history or search messages - User asks "what's happening in #general?" or "any important Slack messages?" - User says "summarize #channel" or "what did I miss in Slack?" - User wants to draft a professional Slack reply using AI - User needs to create channels, invite members, or manage workspace ## Quick Start ### 1. Set up Slack OAuth credentials ```bash # First time only — create Slack app and get credentials bash scripts/slack-auth.sh setup # Authorize your Slack workspace bash scripts/slack-auth.sh login ``` ### 2. Set your EvoLink API key (for AI features) ```bash export EVOLINK_API_KEY="your-key-here" ``` ### 3. Use Slack ```bash # List recent messages in a channel bash scripts/slack.sh list "#general" # Send a message bash scripts/slack.sh send "#general" "Hello team!" # Search messages bash scripts/slack.sh search "project update" ``` ## Capabilities ### Core Operations - **Send messages** — Post to channels or DMs with text, markdown, or blocks - **Read channels** — Fetch message history from any channel - **Search** — Find messages by keyword, user, or date range - **Manage channels** — Create, archive, invite/remove members - **File operations** — Upload and download files ### AI Features (Optional) Requires `EVOLINK_API_KEY`. [Get one free](https://evolink.ai/signup?utm_source=clawhub&utm_medium=skill&utm_campaign=slack) - **ai-summary** — Summarize channel activity with key points - **ai-reply** — Generate professional reply suggestions - **ai-prioritize** — Rank messages by importance ## Commands ### Basic Operations ```bash # List channels bash scripts/slack.sh channels # List recent messages (default: 10) bash scripts/slack.sh list "#channel-name" [limit] # Send message bash scripts/slack.sh send "#channel" "Message text" bash scripts/slack.sh send "@username" "Direct message" # Search messages bash scripts/slack.sh search "keyword" [--channel "#name"] [--from "@user"] # Get message details bash scripts/slack.sh read CHANNEL_ID TIMESTAMP ``` ### AI Operations ```bash # Summarize channel (last 50 messages) bash scripts/slack.sh ai-summary "#channel-name" [limit] # Generate reply suggestion bash scripts/slack.sh ai-reply CHANNEL_ID TIMESTAMP # Prioritize messages by importance bash scripts/slack.sh ai-prioritize "#channel-name" [limit] ``` ### Channel Management ```bash # Create channel bash scripts/slack.sh create-channel "channel-name" [--private] # Invite user to channel bash scripts/slack.sh invite "#channel" "@user" # Archive channel bash scripts/slack.sh archive "#channel" ``` ## Example User: "Summarize what happened in #engineering today" ```bash bash scripts/slack.sh ai-summary "#engineering" 30 ``` Output: ``` 📊 Channel Summary: #engineering (30 messages) Key Topics: • Database migration completed successfully (5 messages) • New API endpoint deployed to staging (3 messages) • Code review requested for PR #234 (2 messages) Action Items: • @alice to review performance metrics • @bob needs approval on deployment plan Sentiment: Positive, productive discussions ``` ## Configuration | Variable | Default | Required | Description | |---|---|---|---| | `SLACK_SKILL_DIR` | `~/.slack-skill` | No | Directory for credentials and cache | | `EVOLINK_API_KEY` | — | Optional (AI) | Your EvoLink API key for AI features. [Get one free](https://evolink.ai/signup?utm_source=clawhub&utm_medium=skill&utm_campaign=slack) | | `EVOLINK_MODEL` | `claude-opus-4-6` | No | Model for AI processing. [EvoLink API](https://docs.evolink.ai/en/api-manual/language-series/claude/claude-messages-api?utm_source=clawhub&utm_medium=skill&utm_campaign=slack) | Required binaries: `python3`, `curl` ## Security **Important: Data Consent for AI Features** AI commands (`ai-summary`, `ai-reply`, `ai-prioritize`) transmit message content, sender names, and channel information to `api.evolink.ai` for processing by Claude. By setting `EVOLINK_API_KEY` and using these commands, you explicitly consent to this transmission. Data is not stored after the response is returned. Core Slack operations never transmit data to any third party. **Network Access** - `api.slack.com` — All Slack API operations - `slack.com` — OAuth authorization flow - `api.evolink.ai` — AI features only (optional) **Persistence & Privilege** This skill stores OAuth tokens in `~/.slack-skill/token.json` (mode 600). No elevated or persistent privileges are requested. ## Links - [GitHub](https://github.com/EvoLinkAI/slack-skill-for-openclaw) - [Slack API Reference](https://api.slack.com/methods) - [EvoLink API](https://docs.evolink.ai/en/api-manual/language-series/claude/claude-messages-api?utm_source=clawhub&utm_medium=skill&utm_campaign=slack) - [Community](https://discord.com/invite/5mGHfA24kn) - [Support](mailto:[REDACTED_SECRET])

path: README.md
content: # Slack Assistant — OpenClaw Skill for Claude Code

Slack API integration with smart AI features — send messages, read channels, search conversations, and manage workspaces with Claude-powered summarization and drafting. Powered by [evolink.ai](https://evolink.ai?utm_source=github&utm_medium=skill&utm_campaign=slack)

[What Is This?](#what-is-this) | [Install](#installation) | [Setup](#setup-guide) | [Usage](#usage) | [EvoLink](https://evolink.ai?utm_source=github&utm_medium=skill&utm_campaign=slack)

**Language / 语言:**
[English](README.md) | [简体中文](README.zh-CN.md) | [日本語](README.ja.md) | [한국어](README.ko.md) | [Español](README.es.md) | [Français](README.fr.md) | [Deutsch](README.de.md) | [Türkçe](README.tr.md) | [Русский](README.ru.md)

## What Is This?

An [OpenClaw](https://clawhub.ai) skill that connects Claude Code to your Slack workspace. Just tell Claude what you need:

- "Send a message to #general"
- "What's happening in #engineering?"
- "Summarize the last 50 messages in #product"
- "Draft a reply to that thread"

## Installation

### Option 1: ClawHub (Recommended)
```bash
npx clawhub install slack-assistant
```

### Option 2: npm
```bash
npx evolinkai-slack-assistant
```

### Option 3: Manual
```bash
git clone https://github.com/EvoLinkAI/slack-skill-for-openclaw.git
cp -r slack-skill-for-openclaw /path/to/your/skills/slack-assistant/
```

## Setup Guide

### 1. Create a Slack App

1. Go to [api.slack.com/apps](https://api.slack.com/apps)
2. Click **Create New App** > **From scratch**
3. Name your app and select your workspace
4. Go to **OAuth & Permissions** > **Bot Token Scopes**
5. Add these scopes:
   - `channels:read`, `channels:write`, `channels:history`, `channels:manage`
   - `chat:write`
   - `files:read`, `files:write`
   - `groups:read`, `groups:write`, `groups:history`
   - `im:read`, `im:write`, `im:history`
   - `mpim:read`, `mpim:write`, `mpim:history`
   - `search:read`
   - `users:read`, `users:read.email`
   - `reactions:read`, `reactions:write`
6. Click **Install to Workspace** and authorize
7. Copy the **Bot User OAuth Token** (starts with `xoxb-`)

### 2. Configure Credentials

```bash
# Create config directory
mkdir -p ~/.slack-skill

# Save your bot token
echo '{"bot_token": "xoxb-your-token-here"}' > ~/.slack-skill/credentials.json
chmod 600 ~/.slack-skill/credentials.json
```

### 3. (Optional) Enable AI Features

```bash
export EVOLINK_API_KEY="your-key-here"
```

Get a free API key at [evolink.ai/signup](https://evolink.ai/signup?utm_source=github&utm_medium=skill&utm_campaign=slack)

## Usage

### Core Operations

```bash
# List channels
bash scripts/slack.sh channels

# List recent messages
bash scripts/slack.sh list "#general" 20

# Send a message
bash scripts/slack.sh send "#general" "Hello team!"
bash scripts/slack.sh send "@alice" "Hey, quick question..."

# Search messages
bash scripts/slack.sh search "deployment"

# View a thread
bash scripts/slack.sh read CHANNEL_ID 1234567890.123456

# Reply in a thread
bash scripts/slack.sh reply CHANNEL_ID 1234567890.123456 "Thanks!"
```

### AI Features

```bash
# Summarize channel activity
bash scripts/slack.sh ai-summary "#engineering" 50

# Draft a reply
bash scripts/slack.sh ai-reply CHANNEL_ID 1234567890.123456

# Prioritize messages by importance
bash scripts/slack.sh ai-prioritize "#general" 30
```

### Channel Management

```bash
# Create a channel
bash scripts/slack.sh create-channel "new-project" --private

# Invite a user
bash scripts/slack.sh invite "#new-project" "@alice"

# Upload a file
bash scripts/slack.sh upload "#general" ./report.pdf
```

## Configuration

| Variable | Default | Description |
|---|---|---|
| `SLACK_SKILL_DIR` | `~/.slack-skill` | Config directory |
| `EVOLINK_API_KEY` | — | API key for AI features ([get one](https://evolink.ai/signup?utm_source=github&utm_medium=skill&utm_campaign=slack)) |
| `EVOLINK_MODEL` | `claude-opus-4-6` | AI model ([docs](https://docs.evolink.ai/en/api-manual/language-series/claude/claude-messages-api?utm_source=github&utm_medium=skill&utm_campaign=slack)) |

## Links

- [ClawHub](https://clawhub.ai/EvoLinkAI/slack-assistant)
- [Slack API Reference](https://api.slack.com/methods)
- [EvoLink API](https://docs.evolink.ai/en/api-manual/language-series/claude/claude-messages-api?utm_source=github&utm_medium=skill&utm_campaign=slack)
- [Community](https://discord.com/invite/5mGHfA24kn)
- [Support](mailto:[REDACTED_SECRET])

## License

MIT


path: scripts/slack-auth.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Slack OAuth Authentication Manager
# Usage:
#   bash slack-auth.sh setup          — Configure Slack app credentials
#   bash slack-auth.sh login          — Run OAuth flow and get tokens
#   bash slack-auth.sh status         — Check authentication status
#   bash slack-auth.sh revoke         — Revoke token and remove stored credentials

CONFIG_DIR="${SLACK_SKILL_DIR:-$HOME/.slack-skill}"
CREDENTIALS_FILE="$CONFIG_DIR/credentials.json"
TOKEN_FILE="$CONFIG_DIR/token.json"

# Bot token scopes needed for all features
SCOPES="channels:read,channels:write,channels:history,channels:manage,chat:write,files:read,files:write,groups:read,groups:write,groups:history,im:read,im:write,im:history,mpim:read,mpim:write,mpim:history,search:read,users:read,users:read.email,reactions:read,reactions:write"

# --- Helpers ---
err() { echo "Error: $*" >&2; exit 1; }
info() { echo "→ $*"; }

# Convert Git Bash paths (/c/Users/...) to Windows paths (C:/Users/...) for Python
to_native_path() {
  if [[ "$1" =~ ^/([a-zA-Z])/ ]]; then
    echo "${BASH_REMATCH[1]}:/${1:3}"
  else
    echo "$1"
  fi
}

CREDENTIALS_FILE_NATIVE=$(to_native_path "$CREDENTIALS_FILE")
TOKEN_FILE_NATIVE=$(to_native_path "$TOKEN_FILE")

ensure_config_dir() {
  mkdir -p "$CONFIG_DIR"
  chmod 700 "$CONFIG_DIR"
}

check_credentials() {
  [ -f "$CREDENTIALS_FILE" ] || err "credentials.json not found at $CREDENTIALS_FILE
Create a Slack app and save credentials:
  1. Go to https://api.slack.com/apps
  2. Create New App > From scratch
  3. Go to OAuth & Permissions
  4. Add Bot Token Scopes (see docs)
  5. Install to Workspace
  6. Save client_id, client_secret, and bot_token to $CREDENTIALS_FILE"
}

read_credential_field() {
  python3 -c "
import json
with open('$CREDENTIALS_FILE_NATIVE') as f:
    data = json.load(f)
print(data.get('$1', ''))
" | tr -d '\r'
}

# --- Commands ---
cmd_setup() {
  ensure_config_dir
  info "Slack Skill config directory: $CONFIG_DIR"

  if [ -f "$CREDENTIALS_FILE" ]; then
    info "credentials.json already exists at $CREDENTIALS_FILE"
    local bot_token
    bot_token=$(read_credential_field "bot_token")
    if [ -n "$bot_token" ]; then
      info "Bot [REDACTED_SECRET]"
      info "Run 'slack-auth.sh status' to verify."
    else
      info "No bot_token found. Run 'slack-auth.sh login' for OAuth flow."
    fi
  else
    info "Setting up Slack credentials..."
    info ""
    info "Option A: Direct Bot Token (Recommended for personal use)"
    info "  1. Go to https://api.slack.com/apps"
    info "  2. Create New App > From scratch"
    info "  3. Go to 'OAuth & Permissions'"
    info "  4. Add Bot Token Scopes:"
    info "     channels:read, channels:write, channels:history, channels:manage"
    info "     chat:write, files:read, files:write"
    info "     groups:read, groups:write, groups:history"
    info "     im:read, im:write, im:history"
    info "     mpim:read, mpim:write, mpim:history"
    info "     search:read, users:read, users:read.email"
    info "     reactions:read, reactions:write"
    info "  5. Install App to Workspace"
    info "  6. Copy the 'Bot User OAuth Token' (starts with xoxb-)"
    info "  7. Create $CREDENTIALS_FILE with:"
    info '     {"bot_token": "xoxb-your-token", "client_id": "...", "client_secret": "..."}'
    info ""
    info "Option B: OAuth Flow (For distributable apps)"
    info "  1. Complete steps 1-4 above"
    info "  2. Add Redirect URL: http://localhost:8085"
    info "  3. Save client_id and client_secret to credentials.json:"
    info '     {"client_id": "...", "client_secret": "..."}'
    info "  4. Run 'slack-auth.sh login'"
  fi
}

cmd_login() {
  check_credentials

  # Check if we already have a bot token
  local existing_token
  existing_token=$(read_credential_field "bot_token")
  if [ -n "$existing_token" ]; then
    info "Bot token already present in credentials.json."
    info "Testing connection..."
    local test_result
    test_result=$(curl -s -H "Authorization: Bearer $existing_token" "https://slack.com/api/auth.test")
    local ok
    ok=$(echo "$test_result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ok',''))" | tr -d '\r')
    if [ "$ok" = "True" ]; then
      # Save to token.json for consistency
      python3 -c "
import json, time
data = {'bot_token': '$existing_token', 'created_at': int(time.time())}
with open('$TOKEN_FILE_NATIVE', 'w') as f:
    json.dump(data, f, indent=2)
"
      chmod 600 "$TOKEN_FILE"
      local team user
      team=$(echo "$test_result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('team',''))" | tr -d '\r')
      user=$(echo "$test_result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('user',''))" | tr -d '\r')
      info "Connected! Workspace: $team, Bot: $user"
      return
    else
      info "Stored bot token is invalid. Starting OAuth flow..."
    fi
  fi

  # OAuth flow
  local client_id client_secret
  client_id=$(read_credential_field "client_id")
  client_secret=$(read_credential_field "client_secret")

  [ -z "$client_id" ] && err "client_id not found in credentials.json"
  [ -z "$client_secret" ] && err "client_secret not found in credentials.json"

  REDIRECT_PORT=8085
  REDIRECT_URI="http://localhost:$REDIRECT_PORT"

  local auth_url="https://slack.com/oauth/v2/authorize?client_id=${client_id}&scope=${SCOPES}&redirect_uri=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$REDIRECT_URI'))" | tr -d '\r')"

  info "Open this URL in your browser to authorize:"
  echo ""
  echo "$auth_url"
  echo ""
  info "Waiting for authorization callback on port $REDIRECT_PORT..."

  # One-shot HTTP server to capture OAuth callback
  local auth_code
  auth_code=$(python3 -c "
import http.server, urllib.parse, sys

class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        query = urllib.parse.urlparse(self.path).query
        params = urllib.parse.parse_qs(query)
        code = params.get('code', [None])[0]
        error = params.get('error', [None])[0]

        if code:
            self.send_response(200)
            self.send_header('Content-Type', 'text/html')
            self.end_headers()
            self.wfile.write(b'<html><body><h2>Slack authorization successful!</h2><p>You can close this window now.</p></body></html>')
            print(code)
        elif error:
            self.send_response(400)
            self.send_header('Content-Type', 'text/html')
            self.end_headers()
            self.wfile.write(f'<html><body><h2>Authorization failed: {error}</h2></body></html>'.encode())
            print('ERROR:' + error, file=sys.stderr)
        sys.stdout.flush()

    def log_message(self, format, *args):
        pass

server = http.server.HTTPServer(('localhost', $REDIRECT_PORT), Handler)
server.handle_request()
server.server_close()
" | tr -d '\r')

  [ -z "$auth_code" ] && err "No authorization code received"

  info "Exchanging code for token..."

  local response
  response=$(curl -s -X POST "https://slack.com/api/oauth.v2.access" \
    -d "code=$auth_code" \
    -d "client_id=$client_id" \
    -d "client_secret=$client_secret" \
    -d "redirect_uri=$REDIRECT_URI")

  local ok
  ok=$(echo "$response" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ok', False))" | tr -d '\r')

  if [ "$ok" != "True" ]; then
    err "Token exchange failed: $response"
  fi

  python3 -c "
import json, time
response = json.loads('''$response''')
token_data = {
    'bot_token': response.get('access_token', ''),
    'team_id': response.get('team', {}).get('id', ''),
    'team_name': response.get('team', {}).get('name', ''),
    'bot_user_id': response.get('bot_user_id', ''),
    'scope': response.get('scope', ''),
    'created_at': int(time.time())
}
with open('$TOKEN_FILE_NATIVE', 'w') as f:
    json.dump(token_data, f, indent=2)
"

  chmod 600 "$TOKEN_FILE"
  info "Authentication successful! Token saved to $TOKEN_FILE"
}

cmd_status() {
  info "Config directory: $CONFIG_DIR"

  if [ -f "$CREDENTIALS_FILE" ]; then
    local bot_token
    bot_token=$(read_credential_field "bot_token")
    if [ -n "$bot_token" ]; then
      info "Credentials: OK (bot_token present)"
    else
      local client_id
      client_id=$(read_credential_field "client_id")
      info "Credentials: OK (client_id: ${client_id:0:15}...)"
    fi
  else
    info "Credentials: NOT FOUND"
    return
  fi

  # Test the actual token
  local token=""
  if [ -f "$TOKEN_FILE" ]; then
    [REDACTED_SECRET] -c "import json; f=open('$TOKEN_FILE_NATIVE'); print(json.load(f).get('bot_token',''))" | tr -d '\r')
  fi
  [ -z "$token" ] && [REDACTED_SECRET] "bot_token")

  if [ -n "$token" ]; then
    local result
    result=$(curl -s -H "Authorization: Bearer $token" "https://slack.com/api/auth.test")
    local ok
    ok=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ok',''))" | tr -d '\r')
    if [ "$ok" = "True" ]; then
      local team user
      team=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('team',''))" | tr -d '\r')
      user=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('user',''))" | tr -d '\r')
      info "Token: VALID (Workspace: $team, Bot: $user)"
    else
      local error_msg
      error_msg=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('error','unknown'))" | tr -d '\r')
      info "[REDACTED_SECRET] ($error_msg)"
    fi
  else
    info "Token: NOT FOUND (run slack-auth.sh login)"
  fi
}

cmd_revoke() {
  local token=""
  if [ -f "$TOKEN_FILE" ]; then
    [REDACTED_SECRET] -c "import json; f=open('$TOKEN_FILE_NATIVE'); print(json.load(f).get('bot_token',''))" | tr -d '\r')
  fi
  [ -z "$token" ] && [REDACTED_SECRET] "bot_token" 2>/dev/null || echo "")

  if [ -n "$token" ]; then
    curl -s -X POST "https://slack.com/api/auth.revoke" \
      -H "Authorization: Bearer $token" >/dev/null 2>&1 || true
    info "Token revoked from Slack."
  fi

  if [ -f "$TOKEN_FILE" ]; then
    rm -f "$TOKEN_FILE"
    info "Local token file removed."
  else
    info "No token file found. Nothing to revoke."
  fi
}

# --- Main ---
COMMAND="${1:-help}"

case "$COMMAND" in
  setup)   cmd_setup ;;
  login)   cmd_login ;;
  status)  cmd_status ;;
  revoke)  cmd_revoke ;;
  help|*)
    echo "Slack OAuth Authentication Manager"
    echo ""
    echo "Usage: bash slack-auth.sh <command>"
    echo ""
    echo "Commands:"
    echo "  setup    — Configure Slack app credentials"
    echo "  login    — Authenticate with bot token or OAuth flow"
    echo "  status   — Check authentication status"
    echo "  revoke   — Revoke token and clean up"
    ;;
esac


path: scripts/slack.sh
content: #!/usr/bin/env bash
set -euo pipefail

# Slack Assistant — Send messages, read channels, search, and manage Slack with AI features
# Usage: bash slack.sh <command> [options]
#
# Commands:
#   channels                           — List channels
#   list <#channel> [limit]            — List recent messages in a channel
#   send <#channel|@user> <message>    — Send a message
#   reply <channel_id> <ts> <message>  — Reply in a thread
#   search <query>                     — Search messages
#   read <channel_id> <ts>             — Read a specific message and its thread
#   users                              — List workspace users
#   create-channel <name> [--private]  — Create a channel
#   invite <#channel> <@user>          — Invite user to channel
#   archive <#channel>                 — Archive a channel
#   upload <#channel> <file_path>      — Upload a file
#   react <channel_id> <ts> <emoji>    — Add reaction
#   profile                            — View bot profile
#   ai-summary <#channel> [limit]      — AI-summarize channel activity
#   ai-reply <channel_id> <ts>         — AI-draft a reply
#   ai-prioritize <#channel> [limit]   — AI-rank messages by importance

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONFIG_DIR="${SLACK_SKILL_DIR:-$HOME/.slack-skill}"
TOKEN_FILE="$CONFIG_DIR/token.json"
CREDENTIALS_FILE="$CONFIG_DIR/credentials.json"

SLACK_API="https://slack.com/api"
EVOLINK_API="https://api.evolink.ai/v1/messages"

# --- Helpers ---
err() { echo "Error: $*" >&2; exit 1; }

# Convert Git Bash paths (/c/Users/...) to native paths (C:/Users/...) for Python
to_native_path() {
  if [[ "$1" =~ ^/([a-zA-Z])/ ]]; then
    echo "${BASH_REMATCH[1]}:/${1:3}"
  else
    echo "$1"
  fi
}

TOKEN_FILE_NATIVE=$(to_native_path "$TOKEN_FILE")
CREDENTIALS_FILE_NATIVE=$(to_native_path "$CREDENTIALS_FILE")

get_bot_token() {
  local token=""

  # Try token.json first
  if [ -f "$TOKEN_FILE" ]; then
    [REDACTED_SECRET] -c "import json; f=open('$TOKEN_FILE_NATIVE'); print(json.load(f).get('bot_token',''))" | tr -d '\r')
  fi

  # Fall back to credentials.json
  if [ -z "$token" ] && [ -f "$CREDENTIALS_FILE" ]; then
    [REDACTED_SECRET] -c "import json; f=open('$CREDENTIALS_FILE_NATIVE'); print(json.load(f).get('bot_token',''))" | tr -d '\r')
  fi

  [ -z "$token" ] && err "Not authenticated. Run: bash scripts/slack-auth.sh login"
  echo "$token"
}

slack_get() {
  local method="$1"
  shift
  local token
  [REDACTED_SECRET]) || exit 1
  curl -s -H "Authorization: Bearer $token" "$SLACK_API/$method" "$@"
}

slack_post() {
  local method="$1"
  local data="$2"
  local token
  [REDACTED_SECRET]) || exit 1
  curl -s -X POST \
    -H "Authorization: Bearer $token" \
    -H "Content-Type: application/json" \
    -d "$data" \
    "$SLACK_API/$method"
}

slack_post_form() {
  local method="$1"
  shift
  local token
  [REDACTED_SECRET]) || exit 1
  curl -s -X POST \
    -H "Authorization: Bearer $token" \
    "$SLACK_API/$method" "$@"
}

# Resolve #channel-name to channel ID
resolve_channel() {
  local input="$1"
  input="${input#\#}"  # strip leading #

  # If it looks like an ID already (starts with C, G, or D), use directly
  if [[ "$input" =~ ^[CGD][A-Z0-9]+$ ]]; then
    echo "$input"
    return
  fi

  # Look up by name
  local result
  result=$(slack_get "conversations.list" -G -d "limit=999" -d "types=public_channel,private_channel")
  local channel_id
  channel_id=$(echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
for ch in data.get('channels', []):
    if ch['name'] == '$input':
        print(ch['id'])
        sys.exit(0)
print('')
" | tr -d '\r')

  [ -z "$channel_id" ] && err "Channel '#$input' not found"
  echo "$channel_id"
}

# Resolve @username to user ID
resolve_user() {
  local input="$1"
  input="${input#@}"  # strip leading @

  # If it looks like a user ID already
  if [[ "$input" =~ ^[UW][A-Z0-9]+$ ]]; then
    echo "$input"
    return
  fi

  local result
  result=$(slack_get "users.list" -G -d "limit=999")
  local user_id
  user_id=$(echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
for u in data.get('members', []):
    if u.get('name') == '$input' or u.get('real_name','').lower() == '$input'.lower():
        print(u['id'])
        sys.exit(0)
print('')
" | tr -d '\r')

  [ -z "$user_id" ] && err "User '@$input' not found"
  echo "$user_id"
}

# Open a DM channel with a user
open_dm() {
  local user_id="$1"
  local result
  result=$(slack_post "conversations.open" "{\"users\": \"$user_id\"}")
  echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if data.get('ok'):
    print(data['channel']['id'])
else:
    print('')
" | tr -d '\r'
}

evolink_ai() {
  local prompt="$1"
  local content="$2"

  local [REDACTED_SECRET] EVOLINK_API_KEY for AI features. Get one at https://evolink.ai/signup}"
  local model="${EVOLINK_MODEL:-claude-opus-4-6}"

  local tmpfile
  tmpfile=$(mktemp)
  trap "rm -f '$tmpfile'" EXIT

  python3 -c "
import json
data = {
    'model': '$model',
    'max_tokens': 4096,
    'messages': [
        {
            'role': 'user',
            'content': '''$prompt

$content'''
        }
    ]
}
with open('$tmpfile', 'w') as f:
    json.dump(data, f)
"

  local response
  response=$(curl -s -X POST "$EVOLINK_API" \
    -H "Authorization: Bearer $api_key" \
    -H "Content-Type: application/json" \
    -d "@$tmpfile")

  echo "$response" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if 'content' in data:
    for block in data['content']:
        if block.get('type') == 'text':
            print(block['text'])
elif 'error' in data:
    print(f\"AI Error: {data['error'].get('message', str(data['error']))}\", file=sys.stderr)
else:
    print(json.dumps(data, indent=2))
"
}

# --- Commands ---

cmd_channels() {
  local result
  result=$(slack_get "conversations.list" -G -d "limit=200" -d "types=public_channel,private_channel")

  echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if not data.get('ok'):
    print(f\"Error: {data.get('error', 'unknown')}\")
    sys.exit(1)
channels = data.get('channels', [])
print(f'Found {len(channels)} channels:')
print()
for ch in sorted(channels, key=lambda x: x['name']):
    private = '🔒' if ch.get('is_private') else '  '
    members = ch.get('num_members', 0)
    purpose = ch.get('purpose', {}).get('value', '')[:40]
    print(f\"  {private} #{ch['name']:<30} {ch['id']:<12} ({members} members)  {purpose}\")
"
}

cmd_list() {
  local channel_input="${1:?Usage: slack.sh list <#channel> [limit]}"
  local limit="${2:-10}"

  local channel_id
  channel_id=$(resolve_channel "$channel_input") || exit 1

  local result
  result=$(slack_get "conversations.history" -G -d "channel=$channel_id" -d "limit=$limit")

  echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if not data.get('ok'):
    print(f\"Error: {data.get('error', 'unknown')}\")
    sys.exit(1)
messages = data.get('messages', [])
if not messages:
    print('No messages found.')
    sys.exit(0)
print(f'Last {len(messages)} messages:')
print()
for msg in reversed(messages):
    user = msg.get('user', msg.get('bot_id', 'unknown'))
    text = msg.get('text', '')[:100]
    ts = msg.get('ts', '')
    thread = ' [thread]' if msg.get('thread_ts') and msg.get('reply_count', 0) > 0 else ''
    replies = f' ({msg[\"reply_count\"]} replies)' if msg.get('reply_count', 0) > 0 else ''
    print(f\"  [{ts}] <{user}>{thread}{replies}\")
    print(f\"    {text}\")
    print()
"
}

cmd_send() {
  local target="${1:?Usage: slack.sh send <#channel|@user> <message>}"
  local message="${2:?Missing message text}"

  local channel_id

  if [[ "$target" == @* ]]; then
    # DM to user
    local user_id
    user_id=$(resolve_user "$target") || exit 1
    channel_id=$(open_dm "$user_id") || exit 1
    [ -z "$channel_id" ] && err "Could not open DM with $target"
  else
    channel_id=$(resolve_channel "$target") || exit 1
  fi

  local result
  result=$(slack_post "chat.postMessage" "{\"channel\": \"$channel_id\", \"text\": \"$message\"}")

  local ok
  ok=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ok',''))" | tr -d '\r')

  if [ "$ok" = "True" ]; then
    local ts
    ts=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ts',''))" | tr -d '\r')
    echo "Message sent to $target (ts: $ts)"
  else
    local error
    error=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('error','unknown'))" | tr -d '\r')
    err "Failed to send message: $error"
  fi
}

cmd_reply() {
  local channel_id="${1:?Usage: slack.sh reply <channel_id> <thread_ts> <message>}"
  local thread_ts="${2:?Missing thread timestamp}"
  local message="${3:?Missing reply message}"

  local result
  result=$(slack_post "chat.postMessage" "{\"channel\": \"$channel_id\", \"text\": \"$message\", \"thread_ts\": \"$thread_ts\"}")

  local ok
  ok=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ok',''))" | tr -d '\r')

  if [ "$ok" = "True" ]; then
    echo "Reply sent in thread $thread_ts"
  else
    local error
    error=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('error','unknown'))" | tr -d '\r')
    err "Failed to send reply: $error"
  fi
}

cmd_search() {
  local query="${1:?Usage: slack.sh search <query>}"

  local token
  [REDACTED_SECRET]) || exit 1

  # search.messages requires user token in some cases, try with bot token first
  local result
  result=$(curl -s -H "Authorization: Bearer $token" "$SLACK_API/search.messages" -G \
    --data-urlencode "query=$query" \
    -d "count=20" \
    -d "sort=timestamp" \
    -d "sort_dir=desc")

  echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if not data.get('ok'):
    print(f\"Error: {data.get('error', 'unknown')}\")
    print('Note: search.messages may require a User Token instead of Bot Token.')
    sys.exit(1)
matches = data.get('messages', {}).get('matches', [])
total = data.get('messages', {}).get('total', 0)
print(f'Found {total} results (showing {len(matches)}):')
print()
for msg in matches:
    channel_name = msg.get('channel', {}).get('name', 'unknown')
    user = msg.get('username', msg.get('user', 'unknown'))
    text = msg.get('text', '')[:100]
    ts = msg.get('ts', '')
    print(f\"  #{channel_name} | <{user}> [{ts}]\")
    print(f\"    {text}\")
    print()
"
}

cmd_read() {
  local channel_id="${1:?Usage: slack.sh read <channel_id> <message_ts>}"
  local message_ts="${2:?Missing message timestamp}"

  # Get the message and its thread
  local result
  result=$(slack_get "conversations.replies" -G -d "channel=$channel_id" -d "ts=$message_ts" -d "limit=50")

  echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if not data.get('ok'):
    print(f\"Error: {data.get('error', 'unknown')}\")
    sys.exit(1)
messages = data.get('messages', [])
if not messages:
    print('Message not found.')
    sys.exit(0)
print(f'Thread ({len(messages)} messages):')
print('=' * 60)
for msg in messages:
    user = msg.get('user', msg.get('bot_id', 'unknown'))
    text = msg.get('text', '')
    ts = msg.get('ts', '')
    print(f\"From: {user}\")
    print(f\"Time: {ts}\")
    print('---')
    print(text[:3000])
    print('-' * 60)
"
}

cmd_users() {
  local result
  result=$(slack_get "users.list" -G -d "limit=200")

  echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if not data.get('ok'):
    print(f\"Error: {data.get('error', 'unknown')}\")
    sys.exit(1)
members = [m for m in data.get('members', []) if not m.get('deleted') and not m.get('is_bot') and m.get('id') != 'USLACKBOT']
print(f'Found {len(members)} active users:')
print()
for m in sorted(members, key=lambda x: x.get('real_name', '')):
    status = m.get('profile', {}).get('status_emoji', '')
    print(f\"  @{m.get('name',''):<20} {m.get('real_name',''):<25} {m['id']:<12} {status}\")
"
}

cmd_create_channel() {
  local name="${1:?Usage: slack.sh create-channel <name> [--private]}"
  local is_private="false"

  if [ "${2:-}" = "--private" ]; then
    is_private="true"
  fi

  local result
  result=$(slack_post "conversations.create" "{\"name\": \"$name\", \"is_private\": $is_private}")

  local ok
  ok=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ok',''))" | tr -d '\r')

  if [ "$ok" = "True" ]; then
    local channel_id
    channel_id=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin)['channel']['id'])" | tr -d '\r')
    echo "Channel #$name created ($channel_id)"
  else
    local error
    error=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('error','unknown'))" | tr -d '\r')
    err "Failed to create channel: $error"
  fi
}

cmd_invite() {
  local channel_input="${1:?Usage: slack.sh invite <#channel> <@user>}"
  local user_input="${2:?Missing user}"

  local channel_id user_id
  channel_id=$(resolve_channel "$channel_input") || exit 1
  user_id=$(resolve_user "$user_input") || exit 1

  local result
  result=$(slack_post "conversations.invite" "{\"channel\": \"$channel_id\", \"users\": \"$user_id\"}")

  local ok
  ok=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ok',''))" | tr -d '\r')

  if [ "$ok" = "True" ]; then
    echo "Invited $user_input to $channel_input"
  else
    local error
    error=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('error','unknown'))" | tr -d '\r')
    err "Failed to invite: $error"
  fi
}

cmd_archive() {
  local channel_input="${1:?Usage: slack.sh archive <#channel>}"

  local channel_id
  channel_id=$(resolve_channel "$channel_input") || exit 1

  local result
  result=$(slack_post "conversations.archive" "{\"channel\": \"$channel_id\"}")

  local ok
  ok=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ok',''))" | tr -d '\r')

  if [ "$ok" = "True" ]; then
    echo "Archived $channel_input"
  else
    local error
    error=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('error','unknown'))" | tr -d '\r')
    err "Failed to archive: $error"
  fi
}

cmd_upload() {
  local channel_input="${1:?Usage: slack.sh upload <#channel> <file_path>}"
  local file_path="${2:?Missing file path}"

  [ -f "$file_path" ] || err "File not found: $file_path"

  local channel_id
  channel_id=$(resolve_channel "$channel_input") || exit 1

  local filename
  filename=$(basename "$file_path")

  local result
  result=$(slack_post_form "files.upload" \
    -F "channels=$channel_id" \
    -F "file=@$file_path" \
    -F "filename=$filename")

  local ok
  ok=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ok',''))" | tr -d '\r')

  if [ "$ok" = "True" ]; then
    echo "Uploaded $filename to $channel_input"
  else
    local error
    error=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('error','unknown'))" | tr -d '\r')
    err "Failed to upload: $error"
  fi
}

cmd_react() {
  local channel_id="${1:?Usage: slack.sh react <channel_id> <timestamp> <emoji>}"
  local timestamp="${2:?Missing message timestamp}"
  local emoji="${3:?Missing emoji name (without colons)}"

  local result
  result=$(slack_post "reactions.add" "{\"channel\": \"$channel_id\", \"timestamp\": \"$timestamp\", \"name\": \"$emoji\"}")

  local ok
  ok=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ok',''))" | tr -d '\r')

  if [ "$ok" = "True" ]; then
    echo "Added :$emoji: reaction"
  else
    local error
    error=$(echo "$result" | python3 -c "import json,sys; print(json.load(sys.stdin).get('error','unknown'))" | tr -d '\r')
    err "Failed to add reaction: $error"
  fi
}

cmd_profile() {
  local token
  [REDACTED_SECRET]) || exit 1
  local result
  result=$(curl -s -H "Authorization: Bearer $token" "$SLACK_API/auth.test")

  echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if not data.get('ok'):
    print(f\"Error: {data.get('error', 'unknown')}\")
    sys.exit(1)
print(f\"User:      {data.get('user', 'N/A')}\")
print(f\"User ID:   {data.get('user_id', 'N/A')}\")
print(f\"Team:      {data.get('team', 'N/A')}\")
print(f\"Team ID:   {data.get('team_id', 'N/A')}\")
print(f\"URL:       {data.get('url', 'N/A')}\")
"
}

# --- AI Commands ---

cmd_ai_summary() {
  local channel_input="${1:?Usage: slack.sh ai-summary <#channel> [limit]}"
  local limit="${2:-50}"

  local channel_id
  channel_id=$(resolve_channel "$channel_input") || exit 1

  echo "Fetching messages from $channel_input..."

  local result
  result=$(slack_get "conversations.history" -G -d "channel=$channel_id" -d "limit=$limit")

  local msg_content
  msg_content=$(echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if not data.get('ok'):
    print(f\"Error: {data.get('error', 'unknown')}\", file=sys.stderr)
    sys.exit(1)
messages = data.get('messages', [])
if not messages:
    print('No messages found.')
    sys.exit(0)
lines = []
for msg in reversed(messages):
    user = msg.get('user', msg.get('bot_id', 'unknown'))
    text = msg.get('text', '')[:500]
    ts = msg.get('ts', '')
    replies = msg.get('reply_count', 0)
    thread = f' [{replies} replies]' if replies > 0 else ''
    lines.append(f'<{user}> [{ts}]{thread}: {text}')
print('\n'.join(lines))
" | tr -d '\r')

  [ -z "$msg_content" ] && { echo "No messages to summarize."; return; }

  echo "Generating AI summary..."
  evolink_ai "You are a Slack channel analyst. Summarize the following channel conversation concisely. Include:
1. Key topics discussed (with participant names)
2. Decisions made
3. Action items (who needs to do what)
4. Overall sentiment/tone

Format with clear sections and bullet points." "$msg_content"
}

cmd_ai_reply() {
  local channel_id="${1:?Usage: slack.sh ai-reply <channel_id> <message_ts>}"
  local message_ts="${2:?Missing message timestamp}"

  echo "Reading thread..."
  local result
  result=$(slack_get "conversations.replies" -G -d "channel=$channel_id" -d "ts=$message_ts" -d "limit=20")

  local thread_content
  thread_content=$(echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if not data.get('ok'):
    print(f\"Error: {data.get('error', 'unknown')}\", file=sys.stderr)
    sys.exit(1)
messages = data.get('messages', [])
lines = []
for msg in messages:
    user = msg.get('user', msg.get('bot_id', 'unknown'))
    text = msg.get('text', '')[:1000]
    lines.append(f'<{user}>: {text}')
print('\n'.join(lines))
" | tr -d '\r')

  [ -z "$thread_content" ] && { echo "No messages found."; return; }

  echo "Generating AI reply draft..."
  evolink_ai "You are a professional Slack communicator. Draft a helpful, concise reply to the following Slack thread. Match the tone of the conversation (casual if casual, formal if formal). Write only the reply text, no metadata." "$thread_content"
}

cmd_ai_prioritize() {
  local channel_input="${1:?Usage: slack.sh ai-prioritize <#channel> [limit]}"
  local limit="${2:-30}"

  local channel_id
  channel_id=$(resolve_channel "$channel_input") || exit 1

  echo "Fetching messages from $channel_input..."

  local result
  result=$(slack_get "conversations.history" -G -d "channel=$channel_id" -d "limit=$limit")

  local msg_content
  msg_content=$(echo "$result" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if not data.get('ok'):
    print(f\"Error: {data.get('error', 'unknown')}\", file=sys.stderr)
    sys.exit(1)
messages = data.get('messages', [])
lines = []
for i, msg in enumerate(reversed(messages), 1):
    user = msg.get('user', msg.get('bot_id', 'unknown'))
    text = msg.get('text', '')[:300]
    ts = msg.get('ts', '')
    replies = msg.get('reply_count', 0)
    reactions = sum(r.get('count', 0) for r in msg.get('reactions', []))
    lines.append(f'{i}. <{user}> [{ts}] (replies:{replies}, reactions:{reactions}): {text}')
print('\n'.join(lines))
" | tr -d '\r')

  [ -z "$msg_content" ] && { echo "No messages found."; return; }

  echo "Analyzing priority..."
  evolink_ai "You are a Slack message triage assistant. Rank the following messages by importance and urgency. Group them into:
- URGENT: Needs immediate response
- IMPORTANT: Should respond today
- NORMAL: Can wait
- FYI: Informational only

For each message, explain why in one sentence. Consider reply counts and reaction counts as engagement signals." "$msg_content"
}

# --- Main ---
COMMAND="${1:-help}"
shift || true

case "$COMMAND" in
  channels)        cmd_channels ;;
  list)            cmd_list "$@" ;;
  send)            cmd_send "$@" ;;
  reply)           cmd_reply "$@" ;;
  search)          cmd_search "$@" ;;
  read)            cmd_read "$@" ;;
  users)           cmd_users ;;
  create-channel)  cmd_create_channel "$@" ;;
  invite)          cmd_invite "$@" ;;
  archive)         cmd_archive "$@" ;;
  upload)          cmd_upload "$@" ;;
  react)           cmd_react "$@" ;;
  profile)         cmd_profile ;;
  ai-summary)      cmd_ai_summary "$@" ;;
  ai-reply)        cmd_ai_reply "$@" ;;
  ai-prioritize)   cmd_ai_prioritize "$@" ;;
  help|*)
    echo "Slack Assistant — Send, read, search, and manage Slack with AI"
    echo ""
    echo "Usage: bash slack.sh <command> [options]"
    echo ""
    echo "Core Commands:"
    echo "  channels                          List all channels"
    echo "  list <#channel> [limit]           List recent messages"
    echo "  send <#channel|@user> <message>   Send a message"
    echo "  reply <chan_id> <ts> <message>     Reply in a thread"
    echo "  search <query>                    Search messages"
    echo "  read <channel_id> <ts>            Read message/thread"
    echo "  users                             List workspace users"
    echo "  create-channel <name> [--private] Create a channel"
    echo "  invite <#channel> <@user>         Invite user to channel"
    echo "  archive <#channel>                Archive a channel"
    echo "  upload <#channel> <file_path>     Upload a file"
    echo "  react <chan_id> <ts> <emoji>       Add emoji reaction"
    echo "  profile                           View bot profile"
    echo ""
    echo "AI Commands (requires EVOLINK_API_KEY):"
    echo "  ai-summary <#channel> [limit]     Summarize channel activity"
    echo "  ai-reply <chan_id> <ts>            Draft a reply with AI"
    echo "  ai-prioritize <#channel> [limit]  Rank messages by importance"
    ;;
esac


path: _meta.json
content: {
  "name": "Slack Assistant",
  "version": "1.0.0",
  "description": "Slack API integration with smart AI features — send messages, read channels, search conversations, and manage workspaces with Claude-powered summarization and drafting. Powered by evolink.ai",
  "author": "EvoLinkAI",
  "license": "MIT",
  "requiredFiles": ["~/.slack-skill/credentials.json"],
  "requiredEnvVars": [],
  "optionalEnvVars": ["EVOLINK_API_KEY", "EVOLINK_MODEL", "SLACK_SKILL_DIR"],
  "requiredBinaries": ["python3", "curl"],
  "externalServices": [
    "slack.com",
    "api.slack.com",
    "api.evolink.ai"
  ],
  "dataTransmission": {
    "api.evolink.ai": "When AI features (ai-summary, ai-reply, ai-prioritize) are used, message content, sender, and channel information are sent to api.evolink.ai for processing. No data is stored by EvoLink after the response is returned. AI features are optional and require explicit opt-in via EVOLINK_API_KEY."
  },
  "requiredDependencies": ["python3", "curl"],
  "optionalDependencies": []
}


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