flake-update-20260505
1#!/usr/bin/env bash
2# shellcheck disable=SC2046,SC2064,SC2086,SC2015
3# slack-read.sh: Read-only Slack CLI
4# Supports two backends:
5# - curl: for Enterprise Grid workspaces (Red Hat) using passage-stored tokens
6# - slackdump: for regular workspaces (Tekton) using slackdump's auth
7set -euo pipefail
8
9# --- Configuration ---
10SLACKDUMP="${SLACKDUMP_BIN:-slackdump}"
11WORKSPACE=""
12BACKEND="" # auto-detect: "curl" or "slackdump"
13CACHE_DIR="${SLACK_READ_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/slack-read}"
14USER_CACHE_MAX_AGE=86400 # 24 hours
15
16# Known workspaces and their backends
17declare -A WORKSPACE_BACKENDS=(
18 [redhat]=curl
19 [tekton]=slackdump
20)
21# Mapping from our workspace name to slackdump's workspace name
22declare -A SLACKDUMP_WORKSPACE_NAMES=(
23 [tekton]=default
24)
25# Default workspace
26DEFAULT_WORKSPACE="redhat"
27
28# Parse global flags before subcommand
29while [[ $# -gt 0 ]]; do
30 case "$1" in
31 --workspace|-w) WORKSPACE="$2"; shift 2 ;;
32 *) break ;;
33 esac
34done
35
36# Set workspace and backend
37WORKSPACE="${WORKSPACE:-$DEFAULT_WORKSPACE}"
38BACKEND="${WORKSPACE_BACKENDS[$WORKSPACE]:-slackdump}"
39
40# Workspace-specific cache
41CACHE_DIR="$CACHE_DIR/$WORKSPACE"
42USER_CACHE="$CACHE_DIR/users.json"
43mkdir -p "$CACHE_DIR"
44
45# --- Helpers ---
46die() { echo "ERROR: $*" >&2; exit 1; }
47
48# --- Curl Backend (Enterprise Grid) ---
49
50# Get token and cookie from passage
51curl_get_token() { passage show "slack/$WORKSPACE/token" 2>/dev/null || die "No token in passage at slack/$WORKSPACE/token"; }
52curl_get_cookie() { passage show "slack/$WORKSPACE/cookie" 2>/dev/null || die "No cookie in passage at slack/$WORKSPACE/cookie"; }
53
54# Make authenticated Slack API call
55slack_api() {
56 local method="$1"; shift
57 local token cookie response
58 token=$(curl_get_token)
59 cookie=$(curl_get_cookie)
60
61 response=$(curl -s -X POST "https://slack.com/api/$method" \
62 -H "Authorization: Bearer $token" \
63 -b "d=$cookie" \
64 "$@")
65
66 local ok
67 ok=$(echo "$response" | jq -r '.ok' 2>/dev/null)
68 if [[ "$ok" != "true" ]]; then
69 local err
70 err=$(echo "$response" | jq -r '.error // "unknown"' 2>/dev/null)
71 die "Slack API $method failed: $err"
72 fi
73 echo "$response"
74}
75
76# --- User Cache ---
77
78ensure_user_cache() {
79 local rebuild=false
80 if [[ ! -f "$USER_CACHE" ]]; then
81 rebuild=true
82 else
83 local age=$(( $(date +%s) - $(stat -c %Y "$USER_CACHE" 2>/dev/null || echo 0) ))
84 [[ $age -gt $USER_CACHE_MAX_AGE ]] && rebuild=true
85 fi
86
87 if [[ "$rebuild" == "true" ]]; then
88 echo "Refreshing user cache..." >&2
89 if [[ "$BACKEND" == "curl" ]]; then
90 curl_build_user_cache
91 else
92 slackdump_build_user_cache
93 fi
94 fi
95}
96
97curl_build_user_cache() {
98 # Enterprise Grid blocks users.list, so start with empty cache
99 # and populate incrementally from search results
100 if [[ ! -f "$USER_CACHE" ]]; then
101 echo '{}' > "$USER_CACHE"
102 fi
103}
104
105# Add user to cache from search results (incremental)
106cache_user_from_result() {
107 local user_id="$1" username="$2"
108 if [[ -f "$USER_CACHE" ]] && [[ -n "$user_id" ]] && [[ -n "$username" ]] && [[ "$username" != "null" ]]; then
109 local existing
110 existing=$(jq -r --arg id "$user_id" '.[$id] // ""' "$USER_CACHE" 2>/dev/null)
111 if [[ -z "$existing" ]]; then
112 local tmp
113 tmp=$(jq --arg id "$user_id" --arg name "$username" '. + {($id): $name}' "$USER_CACHE")
114 echo "$tmp" > "$USER_CACHE"
115 fi
116 fi
117}
118
119slackdump_build_user_cache() {
120 local tmpdir
121 tmpdir=$(mktemp -d /tmp/slack-read-users-XXXXXX)
122 (cd "$tmpdir" && "$SLACKDUMP" list users -format json -q -y $(ws_flags) 2>/dev/null)
123 local json_file
124 json_file=$(find "$tmpdir" -name 'users-*.json' -type f 2>/dev/null | head -1)
125 if [[ -n "$json_file" ]] && [[ -s "$json_file" ]]; then
126 jq 'map({
127 (.id): (
128 if (.profile.display_name // "") != "" then .profile.display_name
129 elif (.profile.real_name // "") != "" then .profile.real_name
130 elif (.name // "") != "" then .name
131 else .id end
132 )
133 }) | add // {}' "$json_file" > "$USER_CACHE"
134 else
135 echo '{}' > "$USER_CACHE"
136 fi
137 rm -rf "$tmpdir"
138}
139
140resolve_user() {
141 local user_id="$1"
142 if [[ -f "$USER_CACHE" ]]; then
143 local name
144 name=$(jq -r --arg id "$user_id" '.[$id] // empty' "$USER_CACHE" 2>/dev/null)
145 [[ -n "$name" ]] && { echo "$name"; return; }
146 fi
147 echo "$user_id"
148}
149
150# --- Text Formatting ---
151
152clean_text() {
153 local text="$1"
154 # Resolve <@UID|name> mentions
155 text=$(echo "$text" | sed -E 's/<@(U[A-Z0-9]+)\|([^>]+)>/@\2/g')
156 # Resolve <@UID> mentions
157 while [[ "$text" =~ \<@(U[A-Z0-9]+)\> ]]; do
158 local uid="${BASH_REMATCH[1]}"
159 local uname
160 uname=$(resolve_user "$uid")
161 text="${text//<@$uid>/@$uname}"
162 done
163 # <URL|label> -> label (URL)
164 text=$(echo "$text" | sed -E 's/<(https?:[^|>]+)\|([^>]+)>/\2 (\1)/g')
165 # <URL> -> URL
166 text=$(echo "$text" | sed -E 's/<(https?:[^>]+)>/\1/g')
167 # <#C123|name> -> #name
168 text=$(echo "$text" | sed -E 's/<#[A-Z0-9]+\|([^>]+)>/#\1/g')
169 echo "$text"
170}
171
172format_ts() {
173 local ts="$1"
174 local unix_ts="${ts%%.*}"
175 date -d "@$unix_ts" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$ts"
176}
177
178format_message() {
179 local msg="$1"
180 local indent="${2:-}"
181 local user_id ts text reply_count
182 user_id=$(echo "$msg" | jq -r '.user // "unknown"')
183 ts=$(echo "$msg" | jq -r '.ts')
184 text=$(echo "$msg" | jq -r '.text // ""')
185 reply_count=$(echo "$msg" | jq -r '.reply_count // 0')
186
187 local username
188 username=$(resolve_user "$user_id")
189 local time_str
190 time_str=$(format_ts "$ts")
191 local clean
192 clean=$(clean_text "$text")
193
194 local reply_indicator=""
195 [[ "$reply_count" -gt 0 ]] && reply_indicator=" [${reply_count} replies]"
196
197 echo "${indent}${username} [${time_str}]:${reply_indicator} ${clean}"
198}
199
200# --- Slackdump helpers ---
201
202ws_flags() {
203 local sd_name="${SLACKDUMP_WORKSPACE_NAMES[$WORKSPACE]:-$WORKSPACE}"
204 [[ -n "$sd_name" ]] && echo "-workspace" "$sd_name"
205}
206
207check_slackdump() {
208 command -v "$SLACKDUMP" &>/dev/null || die "slackdump not found. Install: nix profile install nixpkgs#slackdump"
209}
210
211# --- Commands ---
212
213cmd_help() {
214 cat <<'EOF'
215SlackRead.sh: Read-only Slack CLI
216
217Usage: SlackRead.sh [--workspace NAME] <command> [options]
218
219Global options:
220 --workspace, -w NAME Target workspace (default: redhat)
221 Known: redhat (curl), tekton (slackdump)
222
223Commands:
224 auth Check authentication status
225 channels [options] List channels
226 history <channel> [options] Read channel messages
227 thread <channel> <ts|URL> Read a thread
228 search <query> Search messages
229 users [query] List or search users
230
231History options:
232 --since YYYY-MM-DD Messages since date (default: 7 days ago)
233 --limit N Max messages (default: 50, curl only)
234
235Examples:
236 SlackRead.sh auth
237 SlackRead.sh channels --member-only
238 SlackRead.sh history team-ocp-pipeline
239 SlackRead.sh history team-ocp-pipeline --since 2026-03-25
240 SlackRead.sh -w tekton history pipeline-dev
241 SlackRead.sh thread https://redhat.enterprise.slack.com/archives/C123/p456
242 SlackRead.sh search "release 1.22"
243 SlackRead.sh users vincent
244EOF
245}
246
247cmd_auth() {
248 if [[ "$BACKEND" == "curl" ]]; then
249 echo "Workspace: $WORKSPACE (curl backend)"
250 echo "Token source: passage slack/$WORKSPACE/token"
251 echo "Cookie source: passage slack/$WORKSPACE/cookie"
252 echo "---"
253 local result
254 result=$(slack_api "auth.test")
255 echo "$result" | jq -r '"Status: OK\nUser: \(.user)\nTeam: \(.team)\nEnterprise: \(.enterprise_id // "n/a")"'
256 else
257 check_slackdump
258 echo "Workspace: $WORKSPACE (slackdump backend)"
259 "$SLACKDUMP" workspace list -a 2>/dev/null || die "No workspace configured"
260 fi
261}
262
263cmd_channels() {
264 local member_only=false
265 local include_all=false
266 while [[ $# -gt 0 ]]; do
267 case "$1" in
268 --member-only|-m) member_only=true; shift ;;
269 --all|-a) include_all=true; shift ;;
270 *) shift ;;
271 esac
272 done
273
274 if [[ "$BACKEND" == "curl" ]]; then
275 # Enterprise Grid blocks conversations.list
276 # Suggest using search to discover channels instead
277 echo "Note: Enterprise Grid restricts channel listing."
278 echo "Use 'SlackRead.sh search \"in:#channel-name\"' to find channels."
279 echo "Or provide the channel ID directly to history/thread commands."
280 echo ""
281 echo "Common channels (from search history):"
282 # Show any channels we've seen in search results from cache
283 if [[ -f "$CACHE_DIR/known_channels.json" ]]; then
284 jq -r 'to_entries[] | "\(.key)\t#\(.value)"' "$CACHE_DIR/known_channels.json" 2>/dev/null | sort -t$'\t' -k2 | column -t -s$'\t'
285 else
286 echo "(none cached yet — run a search first)"
287 fi
288 else
289 check_slackdump
290 local flags="-no-json -format text -chan-types public_channel,private_channel"
291 [[ "$member_only" == "true" ]] && flags="$flags -member-only"
292 [[ "$include_all" == "true" ]] && flags="$flags -chan-types public_channel,private_channel,mpim,im"
293 "$SLACKDUMP" list channels $flags $(ws_flags) -y 2>/dev/null | grep -v '^\[' | grep -v '^$'
294 fi
295}
296
297cmd_history() {
298 local channel="${1:?Usage: SlackRead.sh history <channel> [--since DATE] [--limit N]}"
299 shift
300 local since="" limit=50
301 since=$(date -d "7 days ago" "+%Y-%m-%d" 2>/dev/null || echo "")
302
303 while [[ $# -gt 0 ]]; do
304 case "$1" in
305 --since|-s) since="$2"; shift 2 ;;
306 --limit|-l) limit="$2"; shift 2 ;;
307 *) shift ;;
308 esac
309 done
310
311 ensure_user_cache
312
313 if [[ "$BACKEND" == "curl" ]]; then
314 curl_history "$channel" "$since" "$limit"
315 else
316 slackdump_history "$channel" "$since"
317 fi
318}
319
320curl_resolve_channel() {
321 local channel="$1"
322 # If already a channel ID, return as-is
323 [[ "$channel" =~ ^[CDG][A-Z0-9]+$ ]] && { echo "$channel"; return; }
324
325 # Use search to discover channel ID from name
326 local response
327 response=$(slack_api "search.messages" \
328 -d "query=in:#${channel}" \
329 -d "count=1")
330 local chan_id
331 chan_id=$(echo "$response" | jq -r '.messages.matches[0].channel.id // ""')
332 if [[ -n "$chan_id" ]]; then
333 echo "$chan_id"
334 else
335 die "Channel '#${channel}' not found. Try using the channel ID directly."
336 fi
337}
338
339curl_history() {
340 local channel="$1" since="$2" limit="$3"
341 local channel_name="$channel"
342
343 # Resolve channel name to ID
344 local channel_id
345 channel_id=$(curl_resolve_channel "$channel")
346
347 # Convert since date to unix timestamp for oldest param
348 local oldest=""
349 if [[ -n "$since" ]]; then
350 oldest=$(date -d "$since" "+%s" 2>/dev/null || echo "")
351 fi
352
353 # Fetch via conversations.history (works on Enterprise Grid with channel ID)
354 local args=(-d "channel=$channel_id" -d "limit=$limit")
355 [[ -n "$oldest" ]] && args+=(-d "oldest=$oldest")
356
357 local response
358 response=$(slack_api "conversations.history" "${args[@]}")
359
360 local msg_count
361 msg_count=$(echo "$response" | jq '.messages | length')
362 echo "#${channel_name} — ${msg_count} messages"
363 echo "---"
364
365 # Messages come newest-first, reverse for chronological order
366 echo "$response" | jq -c '.messages | reverse | .[]' 2>/dev/null | while IFS= read -r msg; do
367 local user_id username ts text reply_count
368 user_id=$(echo "$msg" | jq -r '.user // "unknown"')
369 username=$(echo "$msg" | jq -r '.user // "unknown"')
370 ts=$(echo "$msg" | jq -r '.ts')
371 text=$(echo "$msg" | jq -r '.text // ""')
372 reply_count=$(echo "$msg" | jq -r '.reply_count // 0')
373
374 # Resolve user - try cache, fallback to user_id
375 local display_name
376 display_name=$(resolve_user "$user_id")
377
378 local time_str
379 time_str=$(format_ts "$ts")
380 local clean
381 clean=$(clean_text "$text")
382
383 local reply_indicator=""
384 [[ "$reply_count" -gt 0 ]] && reply_indicator=" [${reply_count} replies]"
385
386 echo "${display_name} [${time_str}]:${reply_indicator} ${clean}"
387 done
388}
389
390slackdump_history() {
391 local channel="$1" since="$2"
392 check_slackdump
393
394 # Resolve channel name to ID if needed
395 if [[ ! "$channel" =~ ^[CDG] ]]; then
396 local resolved
397 resolved=$("$SLACKDUMP" list channels -no-json -format text -b $(ws_flags) -y 2>/dev/null \
398 | grep -i "#${channel}" | head -1 | awk '{print $1}')
399 [[ -n "$resolved" ]] && channel="$resolved" || die "Channel '${channel}' not found."
400 fi
401
402 local tmpdir
403 tmpdir=$(mktemp -d /tmp/slack-read-XXXXXX)
404 trap "rm -rf '$tmpdir'" EXIT
405
406 local dump_args=(-o "$tmpdir/dump.zip" -y)
407 [[ -n "$since" ]] && dump_args+=(-time-from "${since}T00:00:00")
408
409 "$SLACKDUMP" dump $(ws_flags) "${dump_args[@]}" "$channel" 2>/dev/null
410
411 if [[ -f "$tmpdir/dump.zip" ]]; then
412 local json_file
413 json_file=$(unzip -l "$tmpdir/dump.zip" 2>/dev/null | grep '\.json$' | awk '{print $NF}' | head -1)
414 if [[ -n "$json_file" ]]; then
415 unzip -p "$tmpdir/dump.zip" "$json_file" > "$tmpdir/messages.json"
416 local channel_name msg_count
417 channel_name=$(jq -r '.name // .channel_id // "unknown"' "$tmpdir/messages.json")
418 msg_count=$(jq '.messages | length' "$tmpdir/messages.json")
419
420 if [[ "$msg_count" -eq 0 ]]; then
421 echo "No messages found in #${channel_name}"
422 return
423 fi
424
425 echo "#${channel_name} — ${msg_count} messages"
426 echo "---"
427
428 jq -c '.messages[]' "$tmpdir/messages.json" | while IFS= read -r msg; do
429 format_message "$msg"
430 # Thread replies inline
431 local replies
432 replies=$(echo "$msg" | jq -c '.slackdump_thread_replies // [] | .[1:]')
433 if [[ "$replies" != "[]" ]]; then
434 echo "$replies" | jq -c '.[]' | while IFS= read -r reply; do
435 format_message "$reply" " ↳ "
436 done
437 fi
438 done
439 else
440 echo "No messages found."
441 fi
442 else
443 die "Failed to dump channel $channel"
444 fi
445}
446
447cmd_thread() {
448 local input="${1:?Usage: SlackRead.sh thread <channel> <ts> OR SlackRead.sh thread <URL>}"
449 shift
450 local channel="" thread_ts=""
451
452 # Parse Slack URL
453 if [[ "$input" =~ ^https:// ]]; then
454 channel=$(echo "$input" | sed -E 's|.*/archives/([^/]+)/.*|\1|')
455 local raw_ts
456 raw_ts=$(echo "$input" | sed -E 's|.*/p([0-9]+)$|\1|')
457 thread_ts="${raw_ts:0:10}.${raw_ts:10}"
458 else
459 channel="$input"
460 thread_ts="${1:?Usage: SlackRead.sh thread <channel> <ts>}"
461 shift || true
462 fi
463
464 ensure_user_cache
465
466 if [[ "$BACKEND" == "curl" ]]; then
467 local response
468 response=$(slack_api "conversations.replies" \
469 -d "channel=$channel" \
470 -d "ts=$thread_ts" \
471 -d "limit=100")
472
473 local msg_count
474 msg_count=$(echo "$response" | jq '.messages | length')
475 echo "Thread — ${msg_count} messages"
476 echo "---"
477
478 echo "$response" | jq -c '.messages[]' | while IFS= read -r msg; do
479 format_message "$msg"
480 done
481 else
482 check_slackdump
483 local tmpdir
484 tmpdir=$(mktemp -d /tmp/slack-read-XXXXXX)
485 trap "rm -rf '$tmpdir'" EXIT
486
487 "$SLACKDUMP" dump $(ws_flags) -o "$tmpdir/dump.zip" -y "${channel}:${thread_ts}" 2>/dev/null
488
489 if [[ -f "$tmpdir/dump.zip" ]]; then
490 local json_file
491 json_file=$(unzip -l "$tmpdir/dump.zip" 2>/dev/null | grep '\.json$' | awk '{print $NF}' | head -1)
492 if [[ -n "$json_file" ]]; then
493 unzip -p "$tmpdir/dump.zip" "$json_file" > "$tmpdir/thread.json"
494 local msg_count
495 msg_count=$(jq '.messages | length' "$tmpdir/thread.json")
496 echo "Thread — ${msg_count} messages"
497 echo "---"
498 jq -c '.messages[]' "$tmpdir/thread.json" | while IFS= read -r msg; do
499 format_message "$msg"
500 done
501 fi
502 fi
503 fi
504}
505
506cmd_search() {
507 local query="${1:?Usage: SlackRead.sh search <query>}"
508 shift
509
510 ensure_user_cache
511
512 if [[ "$BACKEND" == "curl" ]]; then
513 local response
514 response=$(slack_api "search.messages" \
515 -d "query=$query" \
516 -d "sort=timestamp" \
517 -d "sort_dir=desc" \
518 -d "count=20")
519
520 local total
521 total=$(echo "$response" | jq '.messages.total // 0')
522 echo "Search: \"${query}\" — ${total} results (showing up to 20)"
523 echo "---"
524
525 # Cache discovered channels and users from results
526 echo "$response" | jq -r '.messages.matches[] | "\(.channel.id)\t\(.channel.name)"' 2>/dev/null \
527 | sort -u | while IFS=$'\t' read -r cid cname; do
528 [[ -n "$cid" && -n "$cname" ]] && {
529 local known="${CACHE_DIR}/known_channels.json"
530 if [[ -f "$known" ]]; then
531 jq --arg id "$cid" --arg name "$cname" '. + {($id): $name}' "$known" > "$known.tmp" && mv "$known.tmp" "$known"
532 else
533 echo "{\"$cid\": \"$cname\"}" > "$known"
534 fi
535 }
536 done 2>/dev/null
537
538 echo "$response" | jq -r '.messages.matches[] | "\(.user // "")\t\(.username // "")"' 2>/dev/null \
539 | sort -u | while IFS=$'\t' read -r uid uname; do
540 cache_user_from_result "$uid" "$uname"
541 done 2>/dev/null
542
543 echo "$response" | jq -c '.messages.matches[]' 2>/dev/null | while IFS= read -r msg; do
544 local user_id ts text chan
545 user_id=$(echo "$msg" | jq -r '.user // .username // "unknown"')
546 ts=$(echo "$msg" | jq -r '.ts')
547 text=$(echo "$msg" | jq -r '.text // "" | gsub("\n"; " ") | .[0:300]')
548 chan=$(echo "$msg" | jq -r '.channel.name // "unknown"')
549
550 local username
551 username=$(echo "$msg" | jq -r '.username // ""')
552 [[ -z "$username" ]] && username=$(resolve_user "$user_id")
553 local time_str
554 time_str=$(format_ts "$ts")
555 local clean
556 clean=$(clean_text "$text")
557
558 echo "${username} [${time_str}] in #${chan}: ${clean}"
559 done
560 else
561 check_slackdump
562 local tmpdir
563 tmpdir=$(mktemp -d /tmp/slack-read-XXXXXX)
564 trap "rm -rf '$tmpdir'" EXIT
565
566 "$SLACKDUMP" search messages $(ws_flags) -o "$tmpdir" -y "$query" 2>/dev/null 1>/dev/null
567
568 if [[ -f "$tmpdir/slackdump.sqlite" ]]; then
569 local count
570 count=$(sqlite3 "$tmpdir/slackdump.sqlite" "SELECT count(*) FROM SEARCH_MESSAGE;" 2>/dev/null || echo 0)
571 echo "Search: \"${query}\" — ${count} results"
572 echo "---"
573
574 sqlite3 "$tmpdir/slackdump.sqlite" \
575 "SELECT CAST(DATA as TEXT) FROM SEARCH_MESSAGE ORDER BY TS DESC LIMIT 20;" 2>/dev/null \
576 | jq -r --slurpfile users "$USER_CACHE" '
577 ($users[0][.user] // .username // .user // "unknown") as $username |
578 (.channel.name // "unknown") as $chan |
579 (.ts // "0") as $ts |
580 (.text // "" | gsub("\n"; " ") | ltrimstr(" ") | .[0:300]) as $text |
581 "\($username)\t\($ts)\t\($chan)\t\($text)"
582 ' 2>/dev/null | while IFS=$'\t' read -r username ts chan text; do
583 [[ -z "$username" ]] && continue
584 local time_str
585 time_str=$(format_ts "$ts")
586 local clean
587 clean=$(clean_text "$text")
588 echo "${username} [${time_str}] in #${chan}: ${clean}"
589 done
590 else
591 echo "No results found."
592 fi
593 fi
594}
595
596cmd_users() {
597 local query="${1:-}"
598
599 if [[ "$BACKEND" == "curl" ]]; then
600 if [[ -n "$query" ]]; then
601 # Search for user via messages they've sent
602 local response
603 response=$(slack_api "search.messages" \
604 -d "query=from:@${query}" \
605 -d "count=5" \
606 -d "sort=timestamp" \
607 -d "sort_dir=desc")
608 echo "$response" | jq -r '.messages.matches[] | "\(.username // .user)\t\(.user)"' 2>/dev/null | sort -u | column -t -s$'\t'
609 else
610 # Show cached users
611 ensure_user_cache
612 if [[ -s "$USER_CACHE" ]] && [[ "$(cat "$USER_CACHE")" != "{}" ]]; then
613 jq -r 'to_entries[] | "\(.value)\t\(.key)"' "$USER_CACHE" | column -t -s$'\t' | head -50
614 else
615 echo "User cache is empty. Users are discovered incrementally from search results."
616 echo "Use: SlackRead.sh users <name> to search for a specific user."
617 fi
618 fi
619 else
620 check_slackdump
621 local users_output
622 users_output=$("$SLACKDUMP" list users -no-json -format text $(ws_flags) -y 2>&1 \
623 | grep -v '^\[' | grep -v '^$' | grep -v 'user cache')
624 if [[ -n "$query" ]]; then
625 echo "$users_output" | grep -i "$query"
626 else
627 echo "$users_output" | head -50
628 fi
629 fi
630}
631
632# --- Main ---
633cmd="${1:-help}"
634shift || true
635
636case "$cmd" in
637 auth) cmd_auth "$@" ;;
638 channels) cmd_channels "$@" ;;
639 history) cmd_history "$@" ;;
640 thread) cmd_thread "$@" ;;
641 search) cmd_search "$@" ;;
642 users) cmd_users "$@" ;;
643 help|--help|-h) cmd_help ;;
644 *) die "Unknown command: $cmd. Run 'SlackRead.sh help' for usage." ;;
645esac