@alankyshum/slack-cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/bin/slack-cli +130 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/@alankyshum/slack-cli)
|
|
2
|
+
[](https://pypi.org/project/slack-cli-agent/)
|
|
3
|
+
|
|
1
4
|
# slack-cli
|
|
2
5
|
|
|
3
6
|
A Slack CLI designed for coding agents (Claude Code, Cursor, etc.) that provides structured JSON access to Slack workspaces via the Web API.
|
package/bin/slack-cli
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# slack-cli auth [--domain <workspace>.slack.com] - Extract tokens from browser
|
|
8
8
|
# slack-cli search <query> - Search messages (Slack query syntax)
|
|
9
9
|
# slack-cli read <channel> [n] - Read messages from a channel
|
|
10
|
-
# slack-cli draft <channel> <msg> - Save a draft
|
|
10
|
+
# slack-cli draft <channel> <msg> - Save a draft to Slack (syncs across devices)
|
|
11
11
|
# slack-cli send <draft_id> - Send a draft (with confirmation)
|
|
12
12
|
# slack-cli help - Show full help
|
|
13
13
|
|
|
@@ -15,7 +15,6 @@ set -euo pipefail
|
|
|
15
15
|
|
|
16
16
|
CONFIG_DIR="${SLACK_CLI_HOME:-$HOME/.slack-cli}"
|
|
17
17
|
CREDS_FILE="$CONFIG_DIR/credentials.json"
|
|
18
|
-
DRAFTS_FILE="$CONFIG_DIR/drafts.json"
|
|
19
18
|
SLACK_API="https://slack.com/api"
|
|
20
19
|
|
|
21
20
|
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -36,6 +35,13 @@ _check_creds() {
|
|
|
36
35
|
_token() { jq -r .token "$CREDS_FILE"; }
|
|
37
36
|
_cookie() { jq -r .d_cookie "$CREDS_FILE"; }
|
|
38
37
|
_domain() { jq -r '.domain // empty' "$CREDS_FILE"; }
|
|
38
|
+
_workspace_api() {
|
|
39
|
+
# Enterprise Grid uses workspace-specific URL for drafts API
|
|
40
|
+
local domain
|
|
41
|
+
domain=$(_domain)
|
|
42
|
+
local workspace="${domain%%.*}"
|
|
43
|
+
echo "https://grid-${workspace}.enterprise.slack.com/api"
|
|
44
|
+
}
|
|
39
45
|
|
|
40
46
|
_api() {
|
|
41
47
|
local method="$1"; shift
|
|
@@ -57,12 +63,46 @@ _api() {
|
|
|
57
63
|
echo "$response"
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
_api_workspace() {
|
|
67
|
+
# Multipart form API call to workspace-specific URL (matches Slack client behavior)
|
|
68
|
+
local method="$1"; shift
|
|
69
|
+
# Remaining args are -F field pairs
|
|
70
|
+
local response
|
|
71
|
+
response=$(curl -s "$(_workspace_api)/$method" \
|
|
72
|
+
-H "Cookie: d=$(_cookie)" \
|
|
73
|
+
-F "token=$(_token)" \
|
|
74
|
+
"$@")
|
|
75
|
+
|
|
76
|
+
local ok err
|
|
77
|
+
ok=$(echo "$response" | jq -r '.ok')
|
|
78
|
+
if [[ "$ok" != "true" ]]; then
|
|
79
|
+
err=$(echo "$response" | jq -r '.error // empty')
|
|
80
|
+
if [[ "$err" == "token_revoked" || "$err" == "invalid_auth" || "$err" == "not_authed" ]]; then
|
|
81
|
+
echo '{"ok":false,"error":"Token expired. Run: slack-cli auth --domain '"$(_domain)"'"}' | jq .
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
echo "$response"
|
|
86
|
+
}
|
|
87
|
+
|
|
60
88
|
_urlencode() {
|
|
61
89
|
python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))" 2>/dev/null
|
|
62
90
|
}
|
|
63
91
|
|
|
64
92
|
_resolve_channel() {
|
|
65
93
|
local name="${1#\#}"
|
|
94
|
+
name="${name#@}"
|
|
95
|
+
|
|
96
|
+
# User ID → open a DM conversation to get channel ID
|
|
97
|
+
if [[ "$name" =~ ^U[A-Z0-9]+$ ]]; then
|
|
98
|
+
local response channel_id
|
|
99
|
+
response=$(_api conversations.open "users=$name")
|
|
100
|
+
channel_id=$(echo "$response" | jq -r '.channel.id // empty')
|
|
101
|
+
echo "$channel_id"
|
|
102
|
+
return
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Otherwise search for channel by name
|
|
66
106
|
local response channel_id
|
|
67
107
|
response=$(_api search.messages "query=in:%23${name}&count=1")
|
|
68
108
|
channel_id=$(echo "$response" | jq -r '.messages.matches[0]?.channel.id // empty')
|
|
@@ -359,7 +399,15 @@ cmd_draft() {
|
|
|
359
399
|
fi
|
|
360
400
|
|
|
361
401
|
local channel_id="$channel"
|
|
362
|
-
if [[
|
|
402
|
+
if [[ "$channel" =~ ^[CDG][A-Z0-9]+$ ]]; then
|
|
403
|
+
: # Already a channel ID
|
|
404
|
+
elif [[ "$channel" =~ ^U[A-Z0-9]+$ ]]; then
|
|
405
|
+
channel_id=$(_resolve_channel "$channel")
|
|
406
|
+
if [[ -z "$channel_id" || "$channel_id" == "null" ]]; then
|
|
407
|
+
echo "{\"ok\":false,\"error\":\"Could not open DM with user: $channel\"}" | jq .
|
|
408
|
+
return 1
|
|
409
|
+
fi
|
|
410
|
+
else
|
|
363
411
|
channel_id=$(_resolve_channel "$channel")
|
|
364
412
|
if [[ -z "$channel_id" || "$channel_id" == "null" ]]; then
|
|
365
413
|
echo "{\"ok\":false,\"error\":\"Could not resolve channel: $channel\"}" | jq .
|
|
@@ -367,35 +415,66 @@ cmd_draft() {
|
|
|
367
415
|
fi
|
|
368
416
|
fi
|
|
369
417
|
|
|
370
|
-
|
|
371
|
-
local
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
418
|
+
# Create server-side draft via Slack API (syncs across devices)
|
|
419
|
+
local client_msg_id
|
|
420
|
+
client_msg_id=$(python3 -c "import uuid; print(str(uuid.uuid4()))")
|
|
421
|
+
|
|
422
|
+
local blocks
|
|
423
|
+
blocks=$(jq -n --arg text "$message" \
|
|
424
|
+
'[{type:"rich_text", elements:[{type:"rich_text_section", elements:[{type:"text", text:$text}]}]}]')
|
|
425
|
+
local destinations
|
|
426
|
+
destinations=$(jq -n --arg ch "$channel_id" '[{channel_id:$ch}]')
|
|
427
|
+
|
|
428
|
+
local response
|
|
429
|
+
response=$(_api_workspace drafts.create \
|
|
430
|
+
-F "blocks=$blocks" \
|
|
431
|
+
-F "client_msg_id=$client_msg_id" \
|
|
432
|
+
-F "attachments=" \
|
|
433
|
+
-F "destinations=$destinations" \
|
|
434
|
+
-F "file_ids=[]" \
|
|
435
|
+
-F "is_from_composer=false" \
|
|
436
|
+
-F "_x_reason=MessageInput:updateDraft" \
|
|
437
|
+
-F "_x_mode=online" \
|
|
438
|
+
-F "_x_sonic=true" \
|
|
439
|
+
-F "_x_app_name=client")
|
|
440
|
+
|
|
441
|
+
local ok
|
|
442
|
+
ok=$(echo "$response" | jq -r '.ok')
|
|
443
|
+
if [[ "$ok" == "true" ]]; then
|
|
444
|
+
echo "$response" | jq '{ok, action:"draft_saved", info:"Draft saved to Slack (syncs across devices). Use slack-cli drafts to list, slack-cli send <id> to send.", draft: {id:.draft.id, channel_id:.draft.destinations[0].channel_id, text:([.draft.blocks[]?.elements[]?.elements[]? | select(.type == "text") | .text] | join("")), created:.draft.date_created}}'
|
|
386
445
|
else
|
|
387
|
-
echo "
|
|
446
|
+
echo "$response" | jq '{ok, error}'
|
|
447
|
+
return 1
|
|
388
448
|
fi
|
|
389
|
-
|
|
390
|
-
echo "$draft_entry" | jq '{ok:true, action:"draft_saved", info:"Use '\''slack-cli drafts'\'' to list, '\''slack-cli send <id>'\'' to send.", draft:.}'
|
|
391
449
|
}
|
|
392
450
|
|
|
393
451
|
cmd_drafts() {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
452
|
+
_check_creds
|
|
453
|
+
|
|
454
|
+
# List server-side drafts from Slack API
|
|
455
|
+
local response
|
|
456
|
+
response=$(_api_workspace drafts.list \
|
|
457
|
+
-F "is_active=true" \
|
|
458
|
+
-F "limit=25" \
|
|
459
|
+
-F "_x_reason=client-drafts-list" \
|
|
460
|
+
-F "_x_mode=online" \
|
|
461
|
+
-F "_x_sonic=true" \
|
|
462
|
+
-F "_x_app_name=client")
|
|
463
|
+
|
|
464
|
+
local ok
|
|
465
|
+
ok=$(echo "$response" | jq -r '.ok')
|
|
466
|
+
if [[ "$ok" == "true" ]]; then
|
|
467
|
+
echo "$response" | jq '{ok, drafts: [.drafts[]? | select(.is_deleted == false and .is_sent == false) | {
|
|
468
|
+
id: .id,
|
|
469
|
+
channel_id: .destinations[0].channel_id,
|
|
470
|
+
text: ([.blocks[]?.elements[]?.elements[]? | select(.type == "text") | .text] | join("")),
|
|
471
|
+
created: .date_created,
|
|
472
|
+
last_updated_ts: .last_updated_ts
|
|
473
|
+
}]}'
|
|
474
|
+
else
|
|
475
|
+
echo "$response" | jq '{ok, error}'
|
|
476
|
+
return 1
|
|
397
477
|
fi
|
|
398
|
-
jq '{ok:true, drafts:[.[] | select(.status == "draft")]}' "$DRAFTS_FILE"
|
|
399
478
|
}
|
|
400
479
|
|
|
401
480
|
cmd_send() {
|
|
@@ -406,21 +485,26 @@ cmd_send() {
|
|
|
406
485
|
return 1
|
|
407
486
|
fi
|
|
408
487
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
488
|
+
# Fetch drafts to find the one to send
|
|
489
|
+
local drafts_response
|
|
490
|
+
drafts_response=$(_api_workspace drafts.list \
|
|
491
|
+
-F "is_active=true" \
|
|
492
|
+
-F "limit=25" \
|
|
493
|
+
-F "_x_reason=client-drafts-list" \
|
|
494
|
+
-F "_x_mode=online" \
|
|
495
|
+
-F "_x_sonic=true" \
|
|
496
|
+
-F "_x_app_name=client")
|
|
413
497
|
|
|
414
498
|
local draft
|
|
415
|
-
draft=$(jq -r --arg id "$draft_id" '.[] | select(.id == $id and .
|
|
499
|
+
draft=$(echo "$drafts_response" | jq -r --arg id "$draft_id" '.drafts[]? | select(.id == $id and .is_deleted == false and .is_sent == false)')
|
|
416
500
|
if [[ -z "$draft" ]]; then
|
|
417
|
-
echo '{"ok":false,"error":"Draft not found or already sent"}' | jq .
|
|
501
|
+
echo '{"ok":false,"error":"Draft not found or already sent. Use slack-cli drafts to list available drafts."}' | jq .
|
|
418
502
|
return 1
|
|
419
503
|
fi
|
|
420
504
|
|
|
421
505
|
local channel_id message
|
|
422
|
-
channel_id=$(echo "$draft" | jq -r '.channel_id')
|
|
423
|
-
message=$(echo "$draft" | jq -r '.
|
|
506
|
+
channel_id=$(echo "$draft" | jq -r '.destinations[0].channel_id')
|
|
507
|
+
message=$(echo "$draft" | jq -r '[.blocks[]?.elements[]?.elements[]? | select(.type == "text") | .text] | join("")')
|
|
424
508
|
|
|
425
509
|
echo "About to send to channel $channel_id:" >&2
|
|
426
510
|
echo "$message" >&2
|
|
@@ -440,8 +524,17 @@ cmd_send() {
|
|
|
440
524
|
local ok
|
|
441
525
|
ok=$(echo "$response" | jq -r '.ok')
|
|
442
526
|
if [[ "$ok" == "true" ]]; then
|
|
443
|
-
|
|
444
|
-
|
|
527
|
+
# Delete the server-side draft after successful send
|
|
528
|
+
local now_ts
|
|
529
|
+
now_ts=$(python3 -c "import time; print(f'{time.time():.6f}')")
|
|
530
|
+
_api_workspace drafts.delete \
|
|
531
|
+
-F "draft_id=$draft_id" \
|
|
532
|
+
-F "client_last_updated_ts=$now_ts" \
|
|
533
|
+
-F "_x_reason=MessageInput:deleteDraft" \
|
|
534
|
+
-F "_x_mode=online" \
|
|
535
|
+
-F "_x_sonic=true" \
|
|
536
|
+
-F "_x_app_name=client" > /dev/null 2>&1
|
|
537
|
+
|
|
445
538
|
echo "$response" | jq '{ok, action:"sent", channel:.channel, ts:.ts}'
|
|
446
539
|
else
|
|
447
540
|
echo "$response" | jq '{ok, error}'
|
|
@@ -523,7 +616,7 @@ COMMANDS:
|
|
|
523
616
|
search <query> Search messages (full Slack query syntax)
|
|
524
617
|
read <channel> [count] Read messages from a channel (name or ID)
|
|
525
618
|
unreads Show recent activity
|
|
526
|
-
draft <channel> <message> Save a draft
|
|
619
|
+
draft <channel> <message> Save a draft to Slack (syncs across devices)
|
|
527
620
|
drafts List saved drafts
|
|
528
621
|
send <draft_id> Send a draft (interactive confirmation)
|
|
529
622
|
channels List your channels
|
package/package.json
CHANGED