Commit df11be2d9247

Vincent Demeester <vincent@sbr.pm>
2026-07-02 23:19:39
refactor: remove slack-archive tooling and service
Dropped the slack-archive package, its NixOS service and timer on kyushu, the slack agent skill, and slackdump from kyushu and okinawa since the archiving workflow is no longer used.
1 parent 43c95c4
Changed files (8)
dots
agents
skills
pkgs
systems
tools
dots/agents/skills/slack/tools/slack-read.sh
@@ -1,645 +0,0 @@
-#!/usr/bin/env bash
-# shellcheck disable=SC2046,SC2064,SC2086,SC2015
-# slack-read.sh: Read-only Slack CLI
-# Supports two backends:
-#   - curl: for Enterprise Grid workspaces (Red Hat) using passage-stored tokens
-#   - slackdump: for regular workspaces (Tekton) using slackdump's auth
-set -euo pipefail
-
-# --- Configuration ---
-SLACKDUMP="${SLACKDUMP_BIN:-slackdump}"
-WORKSPACE=""
-BACKEND=""  # auto-detect: "curl" or "slackdump"
-CACHE_DIR="${SLACK_READ_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/slack-read}"
-USER_CACHE_MAX_AGE=86400 # 24 hours
-
-# Known workspaces and their backends
-declare -A WORKSPACE_BACKENDS=(
-  [redhat]=curl
-  [tekton]=slackdump
-)
-# Mapping from our workspace name to slackdump's workspace name
-declare -A SLACKDUMP_WORKSPACE_NAMES=(
-  [tekton]=default
-)
-# Default workspace
-DEFAULT_WORKSPACE="redhat"
-
-# Parse global flags before subcommand
-while [[ $# -gt 0 ]]; do
-  case "$1" in
-    --workspace|-w) WORKSPACE="$2"; shift 2 ;;
-    *) break ;;
-  esac
-done
-
-# Set workspace and backend
-WORKSPACE="${WORKSPACE:-$DEFAULT_WORKSPACE}"
-BACKEND="${WORKSPACE_BACKENDS[$WORKSPACE]:-slackdump}"
-
-# Workspace-specific cache
-CACHE_DIR="$CACHE_DIR/$WORKSPACE"
-USER_CACHE="$CACHE_DIR/users.json"
-mkdir -p "$CACHE_DIR"
-
-# --- Helpers ---
-die() { echo "ERROR: $*" >&2; exit 1; }
-
-# --- Curl Backend (Enterprise Grid) ---
-
-# Get token and cookie from passage
-curl_get_token() { passage show "slack/$WORKSPACE/token" 2>/dev/null || die "No token in passage at slack/$WORKSPACE/token"; }
-curl_get_cookie() { passage show "slack/$WORKSPACE/cookie" 2>/dev/null || die "No cookie in passage at slack/$WORKSPACE/cookie"; }
-
-# Make authenticated Slack API call
-slack_api() {
-  local method="$1"; shift
-  local token cookie response
-  token=$(curl_get_token)
-  cookie=$(curl_get_cookie)
-
-  response=$(curl -s -X POST "https://slack.com/api/$method" \
-    -H "Authorization: Bearer $token" \
-    -b "d=$cookie" \
-    "$@")
-
-  local ok
-  ok=$(echo "$response" | jq -r '.ok' 2>/dev/null)
-  if [[ "$ok" != "true" ]]; then
-    local err
-    err=$(echo "$response" | jq -r '.error // "unknown"' 2>/dev/null)
-    die "Slack API $method failed: $err"
-  fi
-  echo "$response"
-}
-
-# --- User Cache ---
-
-ensure_user_cache() {
-  local rebuild=false
-  if [[ ! -f "$USER_CACHE" ]]; then
-    rebuild=true
-  else
-    local age=$(( $(date +%s) - $(stat -c %Y "$USER_CACHE" 2>/dev/null || echo 0) ))
-    [[ $age -gt $USER_CACHE_MAX_AGE ]] && rebuild=true
-  fi
-
-  if [[ "$rebuild" == "true" ]]; then
-    echo "Refreshing user cache..." >&2
-    if [[ "$BACKEND" == "curl" ]]; then
-      curl_build_user_cache
-    else
-      slackdump_build_user_cache
-    fi
-  fi
-}
-
-curl_build_user_cache() {
-  # Enterprise Grid blocks users.list, so start with empty cache
-  # and populate incrementally from search results
-  if [[ ! -f "$USER_CACHE" ]]; then
-    echo '{}' > "$USER_CACHE"
-  fi
-}
-
-# Add user to cache from search results (incremental)
-cache_user_from_result() {
-  local user_id="$1" username="$2"
-  if [[ -f "$USER_CACHE" ]] && [[ -n "$user_id" ]] && [[ -n "$username" ]] && [[ "$username" != "null" ]]; then
-    local existing
-    existing=$(jq -r --arg id "$user_id" '.[$id] // ""' "$USER_CACHE" 2>/dev/null)
-    if [[ -z "$existing" ]]; then
-      local tmp
-      tmp=$(jq --arg id "$user_id" --arg name "$username" '. + {($id): $name}' "$USER_CACHE")
-      echo "$tmp" > "$USER_CACHE"
-    fi
-  fi
-}
-
-slackdump_build_user_cache() {
-  local tmpdir
-  tmpdir=$(mktemp -d /tmp/slack-read-users-XXXXXX)
-  (cd "$tmpdir" && "$SLACKDUMP" list users -format json -q -y $(ws_flags) 2>/dev/null)
-  local json_file
-  json_file=$(find "$tmpdir" -name 'users-*.json' -type f 2>/dev/null | head -1)
-  if [[ -n "$json_file" ]] && [[ -s "$json_file" ]]; then
-    jq 'map({
-      (.id): (
-        if (.profile.display_name // "") != "" then .profile.display_name
-        elif (.profile.real_name // "") != "" then .profile.real_name
-        elif (.name // "") != "" then .name
-        else .id end
-      )
-    }) | add // {}' "$json_file" > "$USER_CACHE"
-  else
-    echo '{}' > "$USER_CACHE"
-  fi
-  rm -rf "$tmpdir"
-}
-
-resolve_user() {
-  local user_id="$1"
-  if [[ -f "$USER_CACHE" ]]; then
-    local name
-    name=$(jq -r --arg id "$user_id" '.[$id] // empty' "$USER_CACHE" 2>/dev/null)
-    [[ -n "$name" ]] && { echo "$name"; return; }
-  fi
-  echo "$user_id"
-}
-
-# --- Text Formatting ---
-
-clean_text() {
-  local text="$1"
-  # Resolve <@UID|name> mentions
-  text=$(echo "$text" | sed -E 's/<@(U[A-Z0-9]+)\|([^>]+)>/@\2/g')
-  # Resolve <@UID> mentions
-  while [[ "$text" =~ \<@(U[A-Z0-9]+)\> ]]; do
-    local uid="${BASH_REMATCH[1]}"
-    local uname
-    uname=$(resolve_user "$uid")
-    text="${text//<@$uid>/@$uname}"
-  done
-  # <URL|label> -> label (URL)
-  text=$(echo "$text" | sed -E 's/<(https?:[^|>]+)\|([^>]+)>/\2 (\1)/g')
-  # <URL> -> URL
-  text=$(echo "$text" | sed -E 's/<(https?:[^>]+)>/\1/g')
-  # <#C123|name> -> #name
-  text=$(echo "$text" | sed -E 's/<#[A-Z0-9]+\|([^>]+)>/#\1/g')
-  echo "$text"
-}
-
-format_ts() {
-  local ts="$1"
-  local unix_ts="${ts%%.*}"
-  date -d "@$unix_ts" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$ts"
-}
-
-format_message() {
-  local msg="$1"
-  local indent="${2:-}"
-  local user_id ts text reply_count
-  user_id=$(echo "$msg" | jq -r '.user // "unknown"')
-  ts=$(echo "$msg" | jq -r '.ts')
-  text=$(echo "$msg" | jq -r '.text // ""')
-  reply_count=$(echo "$msg" | jq -r '.reply_count // 0')
-
-  local username
-  username=$(resolve_user "$user_id")
-  local time_str
-  time_str=$(format_ts "$ts")
-  local clean
-  clean=$(clean_text "$text")
-
-  local reply_indicator=""
-  [[ "$reply_count" -gt 0 ]] && reply_indicator=" [${reply_count} replies]"
-
-  echo "${indent}${username} [${time_str}]:${reply_indicator} ${clean}"
-}
-
-# --- Slackdump helpers ---
-
-ws_flags() {
-  local sd_name="${SLACKDUMP_WORKSPACE_NAMES[$WORKSPACE]:-$WORKSPACE}"
-  [[ -n "$sd_name" ]] && echo "-workspace" "$sd_name"
-}
-
-check_slackdump() {
-  command -v "$SLACKDUMP" &>/dev/null || die "slackdump not found. Install: nix profile install nixpkgs#slackdump"
-}
-
-# --- Commands ---
-
-cmd_help() {
-  cat <<'EOF'
-SlackRead.sh: Read-only Slack CLI
-
-Usage: SlackRead.sh [--workspace NAME] <command> [options]
-
-Global options:
-  --workspace, -w NAME    Target workspace (default: redhat)
-                          Known: redhat (curl), tekton (slackdump)
-
-Commands:
-  auth                    Check authentication status
-  channels [options]      List channels
-  history <channel> [options]  Read channel messages
-  thread <channel> <ts|URL>    Read a thread
-  search <query>          Search messages
-  users [query]           List or search users
-
-History options:
-  --since YYYY-MM-DD      Messages since date (default: 7 days ago)
-  --limit N               Max messages (default: 50, curl only)
-
-Examples:
-  SlackRead.sh auth
-  SlackRead.sh channels --member-only
-  SlackRead.sh history team-ocp-pipeline
-  SlackRead.sh history team-ocp-pipeline --since 2026-03-25
-  SlackRead.sh -w tekton history pipeline-dev
-  SlackRead.sh thread https://redhat.enterprise.slack.com/archives/C123/p456
-  SlackRead.sh search "release 1.22"
-  SlackRead.sh users vincent
-EOF
-}
-
-cmd_auth() {
-  if [[ "$BACKEND" == "curl" ]]; then
-    echo "Workspace: $WORKSPACE (curl backend)"
-    echo "Token source: passage slack/$WORKSPACE/token"
-    echo "Cookie source: passage slack/$WORKSPACE/cookie"
-    echo "---"
-    local result
-    result=$(slack_api "auth.test")
-    echo "$result" | jq -r '"Status: OK\nUser: \(.user)\nTeam: \(.team)\nEnterprise: \(.enterprise_id // "n/a")"'
-  else
-    check_slackdump
-    echo "Workspace: $WORKSPACE (slackdump backend)"
-    "$SLACKDUMP" workspace list -a 2>/dev/null || die "No workspace configured"
-  fi
-}
-
-cmd_channels() {
-  local member_only=false
-  local include_all=false
-  while [[ $# -gt 0 ]]; do
-    case "$1" in
-      --member-only|-m) member_only=true; shift ;;
-      --all|-a) include_all=true; shift ;;
-      *) shift ;;
-    esac
-  done
-
-  if [[ "$BACKEND" == "curl" ]]; then
-    # Enterprise Grid blocks conversations.list
-    # Suggest using search to discover channels instead
-    echo "Note: Enterprise Grid restricts channel listing."
-    echo "Use 'SlackRead.sh search \"in:#channel-name\"' to find channels."
-    echo "Or provide the channel ID directly to history/thread commands."
-    echo ""
-    echo "Common channels (from search history):"
-    # Show any channels we've seen in search results from cache
-    if [[ -f "$CACHE_DIR/known_channels.json" ]]; then
-      jq -r 'to_entries[] | "\(.key)\t#\(.value)"' "$CACHE_DIR/known_channels.json" 2>/dev/null | sort -t$'\t' -k2 | column -t -s$'\t'
-    else
-      echo "(none cached yet — run a search first)"
-    fi
-  else
-    check_slackdump
-    local flags="-no-json -format text -chan-types public_channel,private_channel"
-    [[ "$member_only" == "true" ]] && flags="$flags -member-only"
-    [[ "$include_all" == "true" ]] && flags="$flags -chan-types public_channel,private_channel,mpim,im"
-    "$SLACKDUMP" list channels $flags $(ws_flags) -y 2>/dev/null | grep -v '^\[' | grep -v '^$'
-  fi
-}
-
-cmd_history() {
-  local channel="${1:?Usage: SlackRead.sh history <channel> [--since DATE] [--limit N]}"
-  shift
-  local since="" limit=50
-  since=$(date -d "7 days ago" "+%Y-%m-%d" 2>/dev/null || echo "")
-
-  while [[ $# -gt 0 ]]; do
-    case "$1" in
-      --since|-s) since="$2"; shift 2 ;;
-      --limit|-l) limit="$2"; shift 2 ;;
-      *) shift ;;
-    esac
-  done
-
-  ensure_user_cache
-
-  if [[ "$BACKEND" == "curl" ]]; then
-    curl_history "$channel" "$since" "$limit"
-  else
-    slackdump_history "$channel" "$since"
-  fi
-}
-
-curl_resolve_channel() {
-  local channel="$1"
-  # If already a channel ID, return as-is
-  [[ "$channel" =~ ^[CDG][A-Z0-9]+$ ]] && { echo "$channel"; return; }
-
-  # Use search to discover channel ID from name
-  local response
-  response=$(slack_api "search.messages" \
-    -d "query=in:#${channel}" \
-    -d "count=1")
-  local chan_id
-  chan_id=$(echo "$response" | jq -r '.messages.matches[0].channel.id // ""')
-  if [[ -n "$chan_id" ]]; then
-    echo "$chan_id"
-  else
-    die "Channel '#${channel}' not found. Try using the channel ID directly."
-  fi
-}
-
-curl_history() {
-  local channel="$1" since="$2" limit="$3"
-  local channel_name="$channel"
-
-  # Resolve channel name to ID
-  local channel_id
-  channel_id=$(curl_resolve_channel "$channel")
-
-  # Convert since date to unix timestamp for oldest param
-  local oldest=""
-  if [[ -n "$since" ]]; then
-    oldest=$(date -d "$since" "+%s" 2>/dev/null || echo "")
-  fi
-
-  # Fetch via conversations.history (works on Enterprise Grid with channel ID)
-  local args=(-d "channel=$channel_id" -d "limit=$limit")
-  [[ -n "$oldest" ]] && args+=(-d "oldest=$oldest")
-
-  local response
-  response=$(slack_api "conversations.history" "${args[@]}")
-
-  local msg_count
-  msg_count=$(echo "$response" | jq '.messages | length')
-  echo "#${channel_name} — ${msg_count} messages"
-  echo "---"
-
-  # Messages come newest-first, reverse for chronological order
-  echo "$response" | jq -c '.messages | reverse | .[]' 2>/dev/null | while IFS= read -r msg; do
-    local user_id username ts text reply_count
-    user_id=$(echo "$msg" | jq -r '.user // "unknown"')
-    username=$(echo "$msg" | jq -r '.user // "unknown"')
-    ts=$(echo "$msg" | jq -r '.ts')
-    text=$(echo "$msg" | jq -r '.text // ""')
-    reply_count=$(echo "$msg" | jq -r '.reply_count // 0')
-
-    # Resolve user - try cache, fallback to user_id
-    local display_name
-    display_name=$(resolve_user "$user_id")
-
-    local time_str
-    time_str=$(format_ts "$ts")
-    local clean
-    clean=$(clean_text "$text")
-
-    local reply_indicator=""
-    [[ "$reply_count" -gt 0 ]] && reply_indicator=" [${reply_count} replies]"
-
-    echo "${display_name} [${time_str}]:${reply_indicator} ${clean}"
-  done
-}
-
-slackdump_history() {
-  local channel="$1" since="$2"
-  check_slackdump
-
-  # Resolve channel name to ID if needed
-  if [[ ! "$channel" =~ ^[CDG] ]]; then
-    local resolved
-    resolved=$("$SLACKDUMP" list channels -no-json -format text -b $(ws_flags) -y 2>/dev/null \
-      | grep -i "#${channel}" | head -1 | awk '{print $1}')
-    [[ -n "$resolved" ]] && channel="$resolved" || die "Channel '${channel}' not found."
-  fi
-
-  local tmpdir
-  tmpdir=$(mktemp -d /tmp/slack-read-XXXXXX)
-  trap "rm -rf '$tmpdir'" EXIT
-
-  local dump_args=(-o "$tmpdir/dump.zip" -y)
-  [[ -n "$since" ]] && dump_args+=(-time-from "${since}T00:00:00")
-
-  "$SLACKDUMP" dump $(ws_flags) "${dump_args[@]}" "$channel" 2>/dev/null
-
-  if [[ -f "$tmpdir/dump.zip" ]]; then
-    local json_file
-    json_file=$(unzip -l "$tmpdir/dump.zip" 2>/dev/null | grep '\.json$' | awk '{print $NF}' | head -1)
-    if [[ -n "$json_file" ]]; then
-      unzip -p "$tmpdir/dump.zip" "$json_file" > "$tmpdir/messages.json"
-      local channel_name msg_count
-      channel_name=$(jq -r '.name // .channel_id // "unknown"' "$tmpdir/messages.json")
-      msg_count=$(jq '.messages | length' "$tmpdir/messages.json")
-
-      if [[ "$msg_count" -eq 0 ]]; then
-        echo "No messages found in #${channel_name}"
-        return
-      fi
-
-      echo "#${channel_name} — ${msg_count} messages"
-      echo "---"
-
-      jq -c '.messages[]' "$tmpdir/messages.json" | while IFS= read -r msg; do
-        format_message "$msg"
-        # Thread replies inline
-        local replies
-        replies=$(echo "$msg" | jq -c '.slackdump_thread_replies // [] | .[1:]')
-        if [[ "$replies" != "[]" ]]; then
-          echo "$replies" | jq -c '.[]' | while IFS= read -r reply; do
-            format_message "$reply" "  ↳ "
-          done
-        fi
-      done
-    else
-      echo "No messages found."
-    fi
-  else
-    die "Failed to dump channel $channel"
-  fi
-}
-
-cmd_thread() {
-  local input="${1:?Usage: SlackRead.sh thread <channel> <ts> OR SlackRead.sh thread <URL>}"
-  shift
-  local channel="" thread_ts=""
-
-  # Parse Slack URL
-  if [[ "$input" =~ ^https:// ]]; then
-    channel=$(echo "$input" | sed -E 's|.*/archives/([^/]+)/.*|\1|')
-    local raw_ts
-    raw_ts=$(echo "$input" | sed -E 's|.*/p([0-9]+)$|\1|')
-    thread_ts="${raw_ts:0:10}.${raw_ts:10}"
-  else
-    channel="$input"
-    thread_ts="${1:?Usage: SlackRead.sh thread <channel> <ts>}"
-    shift || true
-  fi
-
-  ensure_user_cache
-
-  if [[ "$BACKEND" == "curl" ]]; then
-    local response
-    response=$(slack_api "conversations.replies" \
-      -d "channel=$channel" \
-      -d "ts=$thread_ts" \
-      -d "limit=100")
-
-    local msg_count
-    msg_count=$(echo "$response" | jq '.messages | length')
-    echo "Thread — ${msg_count} messages"
-    echo "---"
-
-    echo "$response" | jq -c '.messages[]' | while IFS= read -r msg; do
-      format_message "$msg"
-    done
-  else
-    check_slackdump
-    local tmpdir
-    tmpdir=$(mktemp -d /tmp/slack-read-XXXXXX)
-    trap "rm -rf '$tmpdir'" EXIT
-
-    "$SLACKDUMP" dump $(ws_flags) -o "$tmpdir/dump.zip" -y "${channel}:${thread_ts}" 2>/dev/null
-
-    if [[ -f "$tmpdir/dump.zip" ]]; then
-      local json_file
-      json_file=$(unzip -l "$tmpdir/dump.zip" 2>/dev/null | grep '\.json$' | awk '{print $NF}' | head -1)
-      if [[ -n "$json_file" ]]; then
-        unzip -p "$tmpdir/dump.zip" "$json_file" > "$tmpdir/thread.json"
-        local msg_count
-        msg_count=$(jq '.messages | length' "$tmpdir/thread.json")
-        echo "Thread — ${msg_count} messages"
-        echo "---"
-        jq -c '.messages[]' "$tmpdir/thread.json" | while IFS= read -r msg; do
-          format_message "$msg"
-        done
-      fi
-    fi
-  fi
-}
-
-cmd_search() {
-  local query="${1:?Usage: SlackRead.sh search <query>}"
-  shift
-
-  ensure_user_cache
-
-  if [[ "$BACKEND" == "curl" ]]; then
-    local response
-    response=$(slack_api "search.messages" \
-      -d "query=$query" \
-      -d "sort=timestamp" \
-      -d "sort_dir=desc" \
-      -d "count=20")
-
-    local total
-    total=$(echo "$response" | jq '.messages.total // 0')
-    echo "Search: \"${query}\" — ${total} results (showing up to 20)"
-    echo "---"
-
-    # Cache discovered channels and users from results
-    echo "$response" | jq -r '.messages.matches[] | "\(.channel.id)\t\(.channel.name)"' 2>/dev/null \
-      | sort -u | while IFS=$'\t' read -r cid cname; do
-        [[ -n "$cid" && -n "$cname" ]] && {
-          local known="${CACHE_DIR}/known_channels.json"
-          if [[ -f "$known" ]]; then
-            jq --arg id "$cid" --arg name "$cname" '. + {($id): $name}' "$known" > "$known.tmp" && mv "$known.tmp" "$known"
-          else
-            echo "{\"$cid\": \"$cname\"}" > "$known"
-          fi
-        }
-      done 2>/dev/null
-
-    echo "$response" | jq -r '.messages.matches[] | "\(.user // "")\t\(.username // "")"' 2>/dev/null \
-      | sort -u | while IFS=$'\t' read -r uid uname; do
-        cache_user_from_result "$uid" "$uname"
-      done 2>/dev/null
-
-    echo "$response" | jq -c '.messages.matches[]' 2>/dev/null | while IFS= read -r msg; do
-      local user_id ts text chan
-      user_id=$(echo "$msg" | jq -r '.user // .username // "unknown"')
-      ts=$(echo "$msg" | jq -r '.ts')
-      text=$(echo "$msg" | jq -r '.text // "" | gsub("\n"; " ") | .[0:300]')
-      chan=$(echo "$msg" | jq -r '.channel.name // "unknown"')
-
-      local username
-      username=$(echo "$msg" | jq -r '.username // ""')
-      [[ -z "$username" ]] && username=$(resolve_user "$user_id")
-      local time_str
-      time_str=$(format_ts "$ts")
-      local clean
-      clean=$(clean_text "$text")
-
-      echo "${username} [${time_str}] in #${chan}: ${clean}"
-    done
-  else
-    check_slackdump
-    local tmpdir
-    tmpdir=$(mktemp -d /tmp/slack-read-XXXXXX)
-    trap "rm -rf '$tmpdir'" EXIT
-
-    "$SLACKDUMP" search messages $(ws_flags) -o "$tmpdir" -y "$query" 2>/dev/null 1>/dev/null
-
-    if [[ -f "$tmpdir/slackdump.sqlite" ]]; then
-      local count
-      count=$(sqlite3 "$tmpdir/slackdump.sqlite" "SELECT count(*) FROM SEARCH_MESSAGE;" 2>/dev/null || echo 0)
-      echo "Search: \"${query}\" — ${count} results"
-      echo "---"
-
-      sqlite3 "$tmpdir/slackdump.sqlite" \
-        "SELECT CAST(DATA as TEXT) FROM SEARCH_MESSAGE ORDER BY TS DESC LIMIT 20;" 2>/dev/null \
-        | jq -r --slurpfile users "$USER_CACHE" '
-          ($users[0][.user] // .username // .user // "unknown") as $username |
-          (.channel.name // "unknown") as $chan |
-          (.ts // "0") as $ts |
-          (.text // "" | gsub("\n"; " ") | ltrimstr(" ") | .[0:300]) as $text |
-          "\($username)\t\($ts)\t\($chan)\t\($text)"
-        ' 2>/dev/null | while IFS=$'\t' read -r username ts chan text; do
-          [[ -z "$username" ]] && continue
-          local time_str
-          time_str=$(format_ts "$ts")
-          local clean
-          clean=$(clean_text "$text")
-          echo "${username} [${time_str}] in #${chan}: ${clean}"
-        done
-    else
-      echo "No results found."
-    fi
-  fi
-}
-
-cmd_users() {
-  local query="${1:-}"
-
-  if [[ "$BACKEND" == "curl" ]]; then
-    if [[ -n "$query" ]]; then
-      # Search for user via messages they've sent
-      local response
-      response=$(slack_api "search.messages" \
-        -d "query=from:@${query}" \
-        -d "count=5" \
-        -d "sort=timestamp" \
-        -d "sort_dir=desc")
-      echo "$response" | jq -r '.messages.matches[] | "\(.username // .user)\t\(.user)"' 2>/dev/null | sort -u | column -t -s$'\t'
-    else
-      # Show cached users
-      ensure_user_cache
-      if [[ -s "$USER_CACHE" ]] && [[ "$(cat "$USER_CACHE")" != "{}" ]]; then
-        jq -r 'to_entries[] | "\(.value)\t\(.key)"' "$USER_CACHE" | column -t -s$'\t' | head -50
-      else
-        echo "User cache is empty. Users are discovered incrementally from search results."
-        echo "Use: SlackRead.sh users <name> to search for a specific user."
-      fi
-    fi
-  else
-    check_slackdump
-    local users_output
-    users_output=$("$SLACKDUMP" list users -no-json -format text $(ws_flags) -y 2>&1 \
-      | grep -v '^\[' | grep -v '^$' | grep -v 'user cache')
-    if [[ -n "$query" ]]; then
-      echo "$users_output" | grep -i "$query"
-    else
-      echo "$users_output" | head -50
-    fi
-  fi
-}
-
-# --- Main ---
-cmd="${1:-help}"
-shift || true
-
-case "$cmd" in
-  auth)     cmd_auth "$@" ;;
-  channels) cmd_channels "$@" ;;
-  history)  cmd_history "$@" ;;
-  thread)   cmd_thread "$@" ;;
-  search)   cmd_search "$@" ;;
-  users)    cmd_users "$@" ;;
-  help|--help|-h) cmd_help ;;
-  *) die "Unknown command: $cmd. Run 'SlackRead.sh help' for usage." ;;
-esac
dots/agents/skills/slack/SKILL.md
@@ -1,191 +0,0 @@
----
-name: slack
-description: Read-only Slack workspace access. USE WHEN user asks about Slack messages OR channel activity OR thread discussions OR searching Slack OR wants to catch up on conversations OR references a Slack URL.
----
-
-# Slack
-
-Read-only access to Slack workspaces. Supports Enterprise Grid (Red Hat) via direct curl API and regular workspaces (Tekton) via slackdump.
-
-## Workflow Routing
-
-| Workflow | Trigger | Action |
-|----------|---------|--------|
-| **Read History** | "what happened in #channel", "catch up on channel" | `SlackRead.sh history <channel>` |
-| **Read Thread** | "read this thread", Slack URL pasted | `SlackRead.sh thread <url>` |
-| **Search** | "search slack for", "find discussion about" | `SlackRead.sh search <query>` |
-| **List Channels** | "list slack channels", "which channels" | `SlackRead.sh channels` |
-| **Find User** | "who is", "find user" | `SlackRead.sh users <query>` |
-
-## Tools
-
-| Tool | Purpose |
-|------|---------|
-| `tools/SlackRead.sh` | Main CLI — all Slack read operations |
-
-**Tool path:** `~/.config/claude/skills/Slack/tools/SlackRead.sh`
-
-## Setup
-
-### Red Hat Workspace (curl backend)
-
-Tokens stored in `passage` (age-encrypted password store):
-
-```bash
-# Store token and cookie from browser DevTools
-echo 'xoxc-...' | passage insert -m slack/redhat/token
-echo 'xoxd-...' | passage insert -m slack/redhat/cookie
-```
-
-**Getting tokens from browser:**
-1. Open Red Hat Slack in browser → DevTools (F12)
-2. **Network** tab → filter `api/` → click any request → copy `token` from request body
-3. **Storage** tab → Cookies → `.slack.com` → cookie `d` (starts with `xoxd-`)
-
-### Tekton Workspace (slackdump backend)
-
-```bash
-slackdump workspace new https://tektoncd.slack.com
-```
-
-Uses EZ-Login 3000 (browser-based, tokens encrypted in `~/.local/cache/slackdump/`).
-
-## Operational Guidance for the Agent
-
-1. **Default workspace is `redhat`** — use `-w tekton` for Tekton.
-2. **Red Hat uses search.messages for channel discovery** — `conversations.list` is blocked on Enterprise Grid.
-3. **Use channel IDs when known** — faster than name resolution for Red Hat.
-4. **Search is the most versatile command** on Enterprise Grid — use `in:#channel after:date` syntax.
-5. **User names resolve incrementally** on Red Hat — discovered from search results, not bulk-fetched.
-6. **Output is compact text** — summarize and present nicely to the user.
-7. **Read-only** — no sending, reacting, or modifying.
-8. **If auth fails** with `not_authed` or `invalid_auth`, tokens may have expired — ask user to refresh from browser.
-
-## Commands
-
-### Check Auth
-
-```bash
-~/.config/claude/skills/Slack/tools/SlackRead.sh auth
-~/.config/claude/skills/Slack/tools/SlackRead.sh -w tekton auth
-```
-
-### Read Channel History
-
-```bash
-# Red Hat (default workspace)
-~/.config/claude/skills/Slack/tools/SlackRead.sh history team-ocp-pipeline
-~/.config/claude/skills/Slack/tools/SlackRead.sh history team-ocp-pipeline --since 2026-03-25 --limit 20
-~/.config/claude/skills/Slack/tools/SlackRead.sh history CG5GV6CJD
-
-# Tekton
-~/.config/claude/skills/Slack/tools/SlackRead.sh -w tekton history pipeline-dev
-```
-
-### Read a Thread
-
-```bash
-# By channel ID + thread timestamp
-~/.config/claude/skills/Slack/tools/SlackRead.sh thread CG5GV6CJD 1774841643.132339
-
-# By Slack URL (tekton)
-~/.config/claude/skills/Slack/tools/SlackRead.sh -w tekton thread https://tektoncd.slack.com/archives/CLCCEBUMU/p1772459500074799
-```
-
-### Search Messages
-
-```bash
-# Slack search query syntax works
-~/.config/claude/skills/Slack/tools/SlackRead.sh search "release 1.22 in:#team-ocp-pipeline"
-~/.config/claude/skills/Slack/tools/SlackRead.sh search "from:@chmouel after:2026-03-20"
-~/.config/claude/skills/Slack/tools/SlackRead.sh -w tekton search "resolver caching"
-```
-
-### List/Search Users
-
-```bash
-~/.config/claude/skills/Slack/tools/SlackRead.sh users vincent
-~/.config/claude/skills/Slack/tools/SlackRead.sh -w tekton users vdemeest
-```
-
-## Output Format
-
-### Channel History
-```
-#channel-name — N messages
----username [YYYY-MM-DD HH:MM]: [N replies] message text
-```
-
-### Search Results
-```
-Search: "query" — N results (showing up to 20)
----
-username [YYYY-MM-DD HH:MM] in #channel: message text (max 300 chars)
-```
-
-### Thread
-```
-Thread — N messages
----
-username [YYYY-MM-DD HH:MM]: message text
-```
-
-## Workspaces
-
-| Workspace | Backend | Auth | Notes |
-|-----------|---------|------|-------|
-| `redhat` (default) | curl | passage `slack/redhat/{token,cookie}` | Enterprise Grid — `conversations.list` blocked, use search |
-| `tekton` | slackdump | EZ-Login 3000 | Regular workspace — all features work |
-
-## Examples
-
-**Example 1: Catch up on team channel**
-```
-User: "What happened in #team-ocp-pipeline this week?"
-→ Run: SlackRead.sh history team-ocp-pipeline --since 2026-03-24
-→ Agent summarizes release planning, defects, discussions
-```
-
-**Example 2: Read a Slack thread**
-```
-User: "Read this thread CG5GV6CJD 1774841643.132339"
-→ Run: SlackRead.sh thread CG5GV6CJD 1774841643.132339
-→ Agent presents the full thread conversation
-```
-
-**Example 3: Search across Slack**
-```
-User: "Search for discussions about TLS on Slack"
-→ Run: SlackRead.sh search "TLS in:#team-ocp-pipeline"
-→ Agent summarizes relevant messages
-```
-
-**Example 4: Tekton workspace**
-```
-User: "What's happening in Tekton #pipeline-dev?"
-→ Run: SlackRead.sh -w tekton history pipeline-dev
-→ Agent summarizes upstream Tekton discussions
-```
-
-## Dependencies
-
-| Tool | Purpose | Source |
-|------|---------|-------|
-| `curl` + `jq` | Slack API calls (curl backend) | nixpkgs |
-| `passage` | Token/cookie storage (curl backend) | nixpkgs |
-| `slackdump` | Slack access (slackdump backend) | nixpkgs (v4.0.2) |
-| `sqlite3` | Search results (slackdump backend) | nixpkgs |
-
-## Limitations
-
-- **Read-only** — cannot send messages, react, or modify anything
-- **Enterprise Grid** — `conversations.list` and `users.list` are blocked; use search to discover channels and users
-- **Token expiry** — browser tokens (xoxc/xoxd) expire; re-extract from browser when auth fails
-- **Speed** — curl backend is fast (~2-5s), slackdump backend is slower (~15-25s)
-
-## Integration
-
-This skill works with:
-- **Email** skill — cross-reference Slack discussions with email threads
-- **GitHub** skill — follow up on PRs/issues mentioned in Slack messages
-- **Jira** skill — connect Slack discussions to Jira tickets (SRVKP-* references)
pkgs/default.nix
@@ -33,7 +33,6 @@ in
   jellyfin-manage-playlist = pkgs.callPackage ../tools/jellyfin-manage-playlist { };
   music-playlist-dl = pkgs.callPackage ../tools/music-playlist-dl { };
   readwise-reader = pkgs.callPackage ../tools/readwise-reader { };
-  slack-archive = pkgs.callPackage ../tools/slack-archive { };
   gcal-to-org = pkgs.callPackage ../tools/gcal-to-org { };
   daily-plan = pkgs.callPackage ../tools/daily-plan { };
   github-notif-manager = pkgs.callPackage ../tools/github-notif-manager { };
systems/kyushu/extra.nix
@@ -177,57 +177,4 @@ in
 
   # Make sure we don't start docker until required
   systemd.services.docker.wantedBy = lib.mkForce [ ];
-
-  # Slack Archive - daily backup of public Slack channels
-  systemd.tmpfiles.rules = [
-    "d /var/lib/slack-archive 0750 vincent users -"
-  ];
-
-  systemd.services.slack-archive = {
-    description = "Slack Public Channel Archiver";
-    after = [ "network-online.target" ];
-    wants = [ "network-online.target" ];
-
-    serviceConfig = {
-      Type = "oneshot";
-      User = "vincent";
-      Group = "users";
-      ExecStart = "${pkgs.slack-archive}/bin/slack-archive archive";
-      Environment = [
-        "SLACK_ARCHIVE_DIR=/var/lib/slack-archive"
-        "SLACK_ARCHIVE_HTML_DIR=/home/vincent/src/experiments/tektoncd-slack-archive"
-        "HOME=/home/vincent"
-        "XDG_CACHE_HOME=/home/vincent/.local/cache"
-      ];
-
-      # Security hardening
-      PrivateTmp = true;
-      ProtectSystem = "strict";
-      ProtectHome = "read-only";
-      ReadWritePaths = [
-        "/var/lib/slack-archive"
-        "/home/vincent/.local/cache/slackdump"
-        "/home/vincent/.local/cache/uv"
-        "/home/vincent/.local/share/uv"
-        "/home/vincent/src/experiments/tektoncd-slack-archive"
-      ];
-      NoNewPrivileges = true;
-
-      # Logging
-      StandardOutput = "journal";
-      StandardError = "journal";
-      SyslogIdentifier = "slack-archive";
-    };
-  };
-
-  systemd.timers.slack-archive = {
-    description = "Daily Slack Archive Timer";
-    wantedBy = [ "timers.target" ];
-
-    timerConfig = {
-      OnCalendar = "09:00"; # Run at 9 AM daily (system should be fully up by then)
-      RandomizedDelaySec = 1800; # 0-30 min random delay
-      Persistent = true;
-    };
-  };
 }
systems/kyushu/home.nix
@@ -78,7 +78,6 @@ in
     shpool-remote
     cliphist-cleanup
 
-    slackdump
     tekton-lsp
     pi-acp
 
systems/okinawa/home.nix
@@ -119,8 +119,6 @@ in
     shpool-remote
     cliphist-cleanup
 
-    slackdump
-
     ssh-to-age
 
     jayrah
tools/slack-archive/default.nix
@@ -1,48 +0,0 @@
-{
-  lib,
-  stdenv,
-  makeWrapper,
-  slackdump,
-  jq,
-  findutils,
-  gnused,
-  uv,
-  python3,
-}:
-
-stdenv.mkDerivation {
-  pname = "slack-archive";
-  version = "0.1.0";
-
-  src = ./.;
-
-  nativeBuildInputs = [ makeWrapper ];
-
-  installPhase = ''
-    runHook preInstall
-
-    mkdir -p $out/bin
-    cp slack-archive.sh $out/bin/slack-archive
-    chmod +x $out/bin/slack-archive
-
-    wrapProgram $out/bin/slack-archive \
-      --prefix PATH : ${
-        lib.makeBinPath [
-          slackdump
-          jq
-          findutils
-          gnused
-          uv
-          python3
-        ]
-      }
-
-    runHook postInstall
-  '';
-
-  meta = with lib; {
-    description = "Slack public channel archiver with static HTML export";
-    license = licenses.mit;
-    platforms = platforms.linux;
-  };
-}
tools/slack-archive/slack-archive.sh
@@ -1,265 +0,0 @@
-#!/usr/bin/env bash
-# Slack Public Channels Archive Script
-# Archives public channels incrementally and generates static HTML viewer
-set -euo pipefail
-
-# --- Configuration ---
-DATA_DIR="${SLACK_ARCHIVE_DIR:-/var/lib/slack-archive}"
-ARCHIVE_DIR="$DATA_DIR/archive"
-EXPORT_DIR="$DATA_DIR/exports"
-HTML_DIR="${SLACK_ARCHIVE_HTML_DIR:-$DATA_DIR/html}"
-CHANNELS_FILE="$DATA_DIR/public-channels.txt"
-CHANNELS_JSON="$DATA_DIR/channels.json"
-
-# How often to refresh channel list (in days)
-CHANNEL_REFRESH_DAYS="${CHANNEL_REFRESH_DAYS:-7}"
-
-# --- Functions ---
-
-log() {
-    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
-}
-
-die() {
-    log "ERROR: $*" >&2
-    exit 1
-}
-
-check_auth() {
-    # Check for cached credentials
-    local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/slackdump"
-    if [[ -f "$cache_dir/provider.bin" ]]; then
-        log "Using cached credentials from $cache_dir"
-        return 0
-    fi
-
-    die "No cached credentials found. Run 'slackdump login' interactively first."
-}
-
-setup_dirs() {
-    mkdir -p "$DATA_DIR" "$EXPORT_DIR" "$HTML_DIR"
-}
-
-refresh_channel_list() {
-    local should_refresh=false
-
-    if [[ ! -f "$CHANNELS_FILE" ]]; then
-        log "Channel list not found, fetching..."
-        should_refresh=true
-    elif [[ -n "$(find "$CHANNELS_FILE" -mtime +"$CHANNEL_REFRESH_DAYS" 2>/dev/null)" ]]; then
-        log "Channel list older than $CHANNEL_REFRESH_DAYS days, refreshing..."
-        should_refresh=true
-    fi
-
-    if [[ "$should_refresh" == "true" ]] || [[ "${FORCE_REFRESH:-}" == "true" ]]; then
-        log "Fetching channel list from Slack..."
-        slackdump list channels -format JSON -no-json > "$CHANNELS_JSON"
-
-        # Filter to public channels only (exclude private, DMs, group DMs)
-        jq -r '.[] | select(.is_private == false and .is_im == false and .is_mpim == false) | .id' \
-            "$CHANNELS_JSON" > "$CHANNELS_FILE"
-
-        local count
-        count=$(wc -l < "$CHANNELS_FILE")
-        log "Found $count public channels"
-    else
-        log "Using cached channel list ($(wc -l < "$CHANNELS_FILE") channels)"
-    fi
-}
-
-run_archive() {
-    if [[ -d "$ARCHIVE_DIR" ]]; then
-        log "Resuming archive from previous state..."
-        slackdump resume "$ARCHIVE_DIR"
-    else
-        log "Starting fresh archive..."
-        slackdump archive -o "$ARCHIVE_DIR" @"$CHANNELS_FILE"
-    fi
-}
-
-convert_to_export() {
-    EXPORT_FILE="$EXPORT_DIR/slack-export-$(date +%Y-%m-%d).zip"
-    log "Converting archive to export format: $EXPORT_FILE"
-    slackdump convert -f export -storage standard -o "$EXPORT_FILE" "$ARCHIVE_DIR"
-}
-
-generate_html() {
-    local export_file="$1"
-    log "Generating HTML viewer..."
-    uvx slack-export-viewer -z "$export_file" --html-only -o "$HTML_DIR"
-
-    # Fix empty anchor tags (slack-export-viewer bug)
-    # Converts <a href='URL'></a> to <a href='URL'>URL</a>
-    log "Fixing empty links..."
-    find "$HTML_DIR" -name "*.html" -exec sed -i -E "s|<a href='([^']+)'></a>|<a href='\\1'>\\1</a>|g" {} \;
-
-    # Reverse message order (newest first)
-    log "Reversing message order (newest first)..."
-    find "$HTML_DIR" -name "*.html" -exec python3 -c '
-import sys
-from pathlib import Path
-from html.parser import HTMLParser
-
-class MessageReverser(HTMLParser):
-    def __init__(self):
-        super().__init__()
-        self.output = []
-        self.in_messages = False
-        self.messages_depth = 0
-        self.message_containers = []
-        self.current_container = []
-        self.in_container = False
-        self.container_depth = 0
-
-    def handle_starttag(self, tag, attrs):
-        attrs_dict = dict(attrs)
-        class_attr = attrs_dict.get("class", "")
-
-        raw = self.get_starttag_text()
-
-        if tag == "div" and "messages" in class_attr.split():
-            self.in_messages = True
-            self.messages_depth = 1
-            self.output.append(raw)
-            return
-
-        if self.in_messages and not self.in_container:
-            if tag == "div" and "message-container" in class_attr:
-                self.in_container = True
-                self.container_depth = 1
-                self.current_container = [raw]
-                return
-            elif tag == "div":
-                self.messages_depth += 1
-
-        if self.in_container:
-            self.current_container.append(raw)
-            if tag == "div":
-                self.container_depth += 1
-        else:
-            self.output.append(raw)
-
-    def handle_endtag(self, tag):
-        raw = f"</{tag}>"
-
-        if self.in_container:
-            if tag == "div":
-                self.container_depth -= 1
-            self.current_container.append(raw)
-            if self.container_depth == 0:
-                self.message_containers.append("".join(self.current_container))
-                self.current_container = []
-                self.in_container = False
-        elif self.in_messages:
-            if tag == "div":
-                self.messages_depth -= 1
-            if self.messages_depth == 0:
-                # Flush reversed containers
-                for container in reversed(self.message_containers):
-                    self.output.append(container)
-                self.message_containers = []
-                self.in_messages = False
-            self.output.append(raw)
-        else:
-            self.output.append(raw)
-
-    def handle_data(self, data):
-        if self.in_container:
-            self.current_container.append(data)
-        else:
-            self.output.append(data)
-
-    def handle_entityref(self, name):
-        raw = f"&{name};"
-        if self.in_container:
-            self.current_container.append(raw)
-        else:
-            self.output.append(raw)
-
-    def handle_charref(self, name):
-        raw = f"&#{name};"
-        if self.in_container:
-            self.current_container.append(raw)
-        else:
-            self.output.append(raw)
-
-    def handle_comment(self, data):
-        raw = f"<!--{data}-->"
-        if self.in_container:
-            self.current_container.append(raw)
-        else:
-            self.output.append(raw)
-
-    def handle_decl(self, decl):
-        self.output.append(f"<!{decl}>")
-
-    def get_result(self):
-        return "".join(self.output)
-
-path = Path(sys.argv[1])
-content = path.read_text()
-parser = MessageReverser()
-parser.feed(content)
-path.write_text(parser.get_result())
-' {} \;
-
-    log "HTML generated at: $HTML_DIR"
-}
-
-serve_html() {
-    local port="${1:-8080}"
-    log "Starting server at http://localhost:$port"
-    python3 -m http.server -d "$HTML_DIR" "$port"
-}
-
-# --- Main ---
-
-main() {
-    local cmd="${1:-archive}"
-
-    case "$cmd" in
-        archive)
-            check_auth
-            setup_dirs
-            refresh_channel_list
-            run_archive
-            convert_to_export
-            generate_html "$EXPORT_FILE"
-            log "Done! View at: $HTML_DIR/index.html"
-            ;;
-        channels)
-            check_auth
-            setup_dirs
-            FORCE_REFRESH=true refresh_channel_list
-            ;;
-        html)
-            local latest_export
-            latest_export=$(find "$EXPORT_DIR" -maxdepth 1 -name "*.zip" -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-)
-            if [[ -z "$latest_export" ]]; then
-                die "No export found. Run '$0 archive' first."
-            fi
-            generate_html "$latest_export"
-            ;;
-        serve)
-            serve_html "${2:-8080}"
-            ;;
-        help|--help|-h)
-            echo "Usage: $0 [command]"
-            echo ""
-            echo "Commands:"
-            echo "  archive   Fetch channels, archive, convert, generate HTML (default)"
-            echo "  channels  Refresh channel list only"
-            echo "  html      Regenerate HTML from latest export"
-            echo "  serve     Start local HTTP server (port 8080 or specify)"
-            echo "  help      Show this help"
-            echo ""
-            echo "Environment:"
-            echo "  SLACK_ARCHIVE_DIR   Data directory (default: /var/lib/slack-archive)"
-            ;;
-        *)
-            die "Unknown command: $cmd. Run '$0 help' for usage."
-            ;;
-    esac
-}
-
-main "$@"