slack-term 1.7.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sno
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # slack — Slack CLI
2
+
3
+ A lightweight Slack CLI for quick workspace interaction from the terminal.
4
+ Two implementations — **TypeScript** (bun-first, published to npm) and **Rust**
5
+ (native binary, `cargo install`) — share one command surface and are verified
6
+ byte-for-byte by [`tests/parity.sh`](tests/parity.sh).
7
+
8
+ ## Features
9
+
10
+ - **News** — Activity feed showing recent mentions (`to:me`), grouped by day with human-readable timestamps
11
+ - **Messages** — Browse recent messages across joined channels
12
+ - **Search** — Full-text search across the workspace
13
+ - **Send** — Send messages to channels or DMs with a confirm-hash safety gate (prevents accidental sends)
14
+ - **Dump** — Bulk-export channel history as markdown
15
+
16
+ ### Output formatting
17
+
18
+ - DM channels display as `@DisplayName`, public channels as `#channel-name`
19
+ - Slack `<@UID>` mention tokens are resolved to display names
20
+ - Slack `<!date^...>` markup is rendered as human-readable dates
21
+ - Messages are grouped by day (Today / Yesterday / weekday)
22
+
23
+ ## Installation
24
+
25
+ ### TypeScript (npm, recommended)
26
+
27
+ One package, no native binaries, any platform with Node 18+.
28
+
29
+ ```sh
30
+ npm install -g @snomiao/slack
31
+ # or: bun add -g @snomiao/slack | pnpm add -g @snomiao/slack
32
+ ```
33
+
34
+ ### Rust (cargo)
35
+
36
+ ```sh
37
+ cargo install --path rs
38
+ ```
39
+
40
+ Both expose the same `slack` command.
41
+
42
+ ## Usage
43
+
44
+ ```sh
45
+ # Activity feed (mentions directed to you)
46
+ slack news
47
+ slack news --limit 5
48
+
49
+ # Recent messages across joined channels
50
+ slack msgs
51
+
52
+ # Search messages
53
+ slack search "deploy"
54
+ slack search "deploy" --count 50
55
+
56
+ # Send a message (two-step confirm — quote #channel)
57
+ slack send "#general" "Hello team"
58
+ # Prints preview + confirm hash; rerun with --confirm=<hash> to actually send
59
+ slack send "#general" "Hello team" --confirm=<hash>
60
+
61
+ # Bulk export channel history
62
+ slack dump --days 7 --filter eng
63
+ ```
64
+
65
+ ## Configuration
66
+
67
+ Requires a Slack user token (`xoxp-...`) with the following scopes:
68
+
69
+ - `search:read` — for search and news
70
+ - `channels:history`, `groups:history`, `im:history`, `mpim:history` — for message history
71
+ - `channels:read`, `groups:read`, `im:read`, `mpim:read` — for channel listing
72
+ - `users:read` — for resolving display names
73
+ - `chat:write` — for sending messages
74
+
75
+ Set the token via environment variable:
76
+
77
+ ```sh
78
+ export SLACK_MCP_XOXP_TOKEN=xoxp-...
79
+ ```
80
+
81
+ Or place it in `~/.config/slack-cli/.env` or a local `.env` file.
82
+
83
+ See [`SKILL.md`](SKILL.md) for a full token-acquisition walkthrough.
84
+
85
+ ## Development
86
+
87
+ ```sh
88
+ # TypeScript
89
+ bun install
90
+ bun run dev -- news --limit 3 # run straight from source
91
+ bun run typecheck
92
+ bun run build # produces dist/cli.js
93
+
94
+ # Rust
95
+ cargo run --manifest-path rs/Cargo.toml --release --bin slack -- news --limit 3
96
+
97
+ # Parity test (requires a token — compares Rust and TS stdout)
98
+ bun run test:parity
99
+ ```
100
+
101
+ ## Dependencies
102
+
103
+ **TypeScript impl** — zero runtime deps; uses built-in `fetch`, `node:crypto`,
104
+ `node:util` argument parsing.
105
+
106
+ **Rust impl**
107
+
108
+ - [clap](https://crates.io/crates/clap) — CLI argument parsing
109
+ - [reqwest](https://crates.io/crates/reqwest) — HTTP client for Slack Web API
110
+ - [tokio](https://crates.io/crates/tokio) — async runtime
111
+ - [chrono](https://crates.io/crates/chrono) — date/time formatting
112
+ - [ring](https://crates.io/crates/ring) — SHA-256 for confirm hashes
113
+
114
+ ## Related / prior art
115
+
116
+ - [`slkcli`](https://www.npmjs.com/package/slkcli) by
117
+ [@therohitdas](https://github.com/therohitdas) — a macOS-only Node CLI that
118
+ auto-extracts `xoxc-` session tokens from the Slack desktop app. Different
119
+ tradeoffs (zero-config on macOS vs. our cross-platform explicit-token
120
+ approach). See [`docs/comparison-slkcli.md`](docs/comparison-slkcli.md) for
121
+ a full UX side-by-side.
122
+ - [`docs/ecosystem.md`](docs/ecosystem.md) — survey of other terminal Slack
123
+ tools (official, `slack-term`, `wee-slack`, `slackcat`, `slackdump`, …).
124
+
125
+ ## License
126
+
127
+ MIT
package/SKILL.md ADDED
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: slack-cli
3
+ description: "Terminal Slack CLI — read news/mentions/DMs/channel history, full-text search, and send messages with a confirm-hash gate. Also covers first-time setup of a user OAuth token (xoxp-...) via SLACK_MCP_XOXP_TOKEN. Trigger on requests like 'check my Slack', 'any mentions?', 'search Slack for X', 'DM @person', 'post to a channel', 'read a user', 'slack news', or auth errors (invalid_auth, missing_scope, not_authed) — even if the user doesn't say 'slack CLI' explicitly."
4
+ ---
5
+
6
+ # Slack CLI Skill
7
+
8
+ A lightweight Slack CLI for quick workspace interaction from the terminal.
9
+ Two implementations share one command surface (parity-tested):
10
+
11
+ - **TypeScript** (bun-first), published as `@snomiao/slack` on npm.
12
+ - **Rust** native binary, installable via `cargo install`.
13
+
14
+ Binary name: `slack` (also aliased as `sl` / invoked via `sc sl ...` in some setups).
15
+
16
+ ## When to use
17
+
18
+ - User wants to read Slack activity, mentions, DMs, or channel history without opening the Slack app.
19
+ - User wants to search the workspace full-text.
20
+ - User wants to send a message to a channel or user from the terminal.
21
+ - User is setting up this CLI for the first time or hitting auth errors.
22
+
23
+ ## Installation
24
+
25
+ TypeScript (recommended — any platform with Node 18+):
26
+
27
+ ```sh
28
+ npm install -g @snomiao/slack
29
+ # or: bun add -g @snomiao/slack / pnpm add -g @snomiao/slack
30
+ ```
31
+
32
+ Rust (native binary):
33
+
34
+ ```sh
35
+ cargo install --path rs
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ ```sh
41
+ # Activity feed — recent mentions directed to you (to:me)
42
+ slack news
43
+ slack news --limit 5
44
+
45
+ # Recent messages across joined channels
46
+ slack msgs
47
+
48
+ # Full-text search
49
+ slack search "deploy"
50
+ slack search "deploy" --count 50
51
+
52
+ # Read a specific channel or DM (quote #channel — unquoted # is a shell comment)
53
+ slack read "#general"
54
+ slack read @username
55
+
56
+ # Send a message (two-step confirm)
57
+ slack send "#general" "Hello team"
58
+ # Prints preview + a confirm hash. Re-run with --confirm=<hash> to actually send:
59
+ slack send "#general" "Hello team" --confirm=<hash>
60
+
61
+ # Bulk export a channel's history
62
+ slack dump "#channel-name"
63
+ ```
64
+
65
+ Targets for `send` must be `#channel` or `@user` — raw IDs are rejected by design.
66
+
67
+ ## Output formatting
68
+
69
+ - DM channels render as `@DisplayName`; public channels as `#channel-name`.
70
+ - `<@UID>` mention tokens are resolved to display names.
71
+ - `<!date^...>` markup is rendered as human-readable dates.
72
+ - Messages are grouped by day (Today / Yesterday / weekday).
73
+
74
+ ## Getting a Slack token (first-time setup)
75
+
76
+ The CLI needs a **User OAuth Token** (`xoxp-...`), NOT a Bot Token.
77
+
78
+ ### Required scopes
79
+
80
+ Under **User Token Scopes**:
81
+
82
+ - `search:read` — for `search` and `news`
83
+ - `channels:history`, `groups:history`, `im:history`, `mpim:history` — message history
84
+ - `channels:read`, `groups:read`, `im:read`, `mpim:read` — channel/DM listing
85
+ - `users:read` — resolve display names
86
+ - `chat:write` — send messages
87
+
88
+ ### Steps
89
+
90
+ 1. Go to https://api.slack.com/apps → **Create New App → From scratch**.
91
+ 2. Name the app (e.g. `slack-cli-<you>`) and pick the workspace.
92
+ 3. Sidebar → **OAuth & Permissions**.
93
+ 4. Scroll to **User Token Scopes** (NOT Bot Token Scopes) and add every scope above.
94
+ 5. Scroll up → **Install to Workspace** → **Allow**.
95
+ - If admin approval is required, the button becomes **Request to Install**. Ask a workspace admin.
96
+ 6. Copy the **User OAuth Token** (starts with `xoxp-`).
97
+
98
+ ### Configure the token
99
+
100
+ ```sh
101
+ # Option A — per-shell
102
+ export SLACK_MCP_XOXP_TOKEN=xoxp-...
103
+
104
+ # Option B — persistent, user-wide
105
+ mkdir -p ~/.config/slack-cli
106
+ echo 'SLACK_MCP_XOXP_TOKEN=xoxp-...' >> ~/.config/slack-cli/.env
107
+
108
+ # Option C — project-local
109
+ echo 'SLACK_MCP_XOXP_TOKEN=xoxp-...' >> .env
110
+ ```
111
+
112
+ Verify:
113
+
114
+ ```sh
115
+ slack news --limit 1
116
+ ```
117
+
118
+ ## Troubleshooting
119
+
120
+ - **`invalid_auth` / `not_authed`** — token missing, mistyped, or revoked. Re-copy from **OAuth & Permissions**.
121
+ - **`missing_scope`** — add the scope from the error, then click **Reinstall to Workspace** (scope changes require reinstall).
122
+ - **`token_revoked`** — app uninstalled; reinstall from the app page.
123
+ - **Token starts with `xoxb-`** — that's a Bot Token. Add scopes under **User Token Scopes** instead, reinstall, and copy the **User OAuth Token**.
124
+ - **Send is rejected with "use #channel or @user"** — the CLI enforces human-readable targets. Use `#channel-name` or `@display-name`, not raw IDs.
125
+ - **Confirm hash mismatch on `send`** — the message text changed between preview and confirm. Re-run without `--confirm` to get a fresh hash.
126
+ - **Enterprise Grid / admin-locked workspace** — custom app installation may need admin approval or be disabled outright.
127
+
128
+ ## Safety
129
+
130
+ - Treat `xoxp-...` like a password. Do not commit `.env` containing a real token — ensure it's gitignored.
131
+ - Revoke unused tokens from the app's **OAuth & Permissions** page.
132
+ - The `send` command's two-step confirm-hash flow is intentional — don't try to bypass it by auto-computing the hash; let the user (or Claude) see the preview first.
package/dist/cli.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ import{createRequire as nJ}from"node:module";var w=nJ(import.meta.url);import{parseArgs as L}from"util";import{createHash as WQ}from"crypto";import{existsSync as cJ,readFileSync as _Q,mkdirSync as EQ,writeFileSync as XQ}from"fs";import{homedir as iJ}from"os";import{join as C}from"path";import{existsSync as o,readFileSync as OJ,writeFileSync as s,mkdirSync as VJ}from"node:fs";import{homedir as KJ}from"node:os";import{join as h,dirname as HJ}from"node:path";function FJ(){return h(KJ(),".config","slack-cli","profiles.json")}function p(){return h(process.cwd(),".slack-cli","workspace")}function m(){return h(KJ(),".slack-cli","workspace")}function R(){let Q=FJ();if(!o(Q))return{profiles:{}};return JSON.parse(OJ(Q,"utf8"))}function a(Q){let J=FJ();VJ(HJ(J),{recursive:!0}),s(J,JSON.stringify(Q,null,2)+`
4
+ `)}function S(Q){if(!o(Q))return;return OJ(Q,"utf8").trim()||void 0}function BJ(Q,J){let Z=HJ(Q);VJ(Z,{recursive:!0});let $=h(Z,".gitignore");if(!o($))s($,`*
5
+ `);s(Q,J+`
6
+ `)}function e(){let Q=R(),J=S(p()),Z=S(m()),$=J??Z;return Object.entries(Q.profiles).map(([z,_])=>({name:z,profile:_,current:z===$}))}function k(Q,J){let Z=R();Z.profiles[Q]=J,a(Z)}function qJ(Q,J){let Z=R();if(!(Q in Z.profiles))throw Error(`Profile not found: ${Q}`);Z.profiles[Q].cookie=J,a(Z)}function MJ(Q){let J=R();if(!(Q in J.profiles))throw Error(`Profile not found: ${Q}`);delete J.profiles[Q],a(J)}function jJ(Q,J=!1){let Z=R();if(!(Q in Z.profiles))throw Error(`Profile not found: ${Q}`);if(J)BJ(m(),Q);else BJ(p(),Q)}function UJ(Q){let Z=R().profiles,$=Object.keys(Z),z=process.env.SLACK_MCP_XOXP_TOKEN;if(z&&$.length>0)throw Error(`Both SLACK_MCP_XOXP_TOKEN and workspace profiles are configured — keep only one.
7
+ `+` • Unset SLACK_MCP_XOXP_TOKEN to use profiles
8
+ `+" • Or remove all profiles: slack workspace remove <name>");if(z)return z;let _=Q??process.env.SLACK_WORKSPACE;if(_){let W=Z[_];if(!W)throw Error(`Workspace "${_}" not found. Available: ${$.join(", ")||"(none)"}`);return W.token}let E=S(p());if(E){let W=Z[E];if(!W)throw Error(`Workspace "${E}" (from .slack-cli/workspace) not found in profiles.`);return W.token}let G=S(m());if(G){let W=Z[G];if(!W)throw Error(`Workspace "${G}" (from ~/.slack-cli/workspace) not found in profiles.`);return W.token}if($.length===0)throw Error("No profiles configured. Run: slack workspace add <name> <token> — or set SLACK_MCP_XOXP_TOKEN");throw Error(`Workspace not selected (${$.join(", ")} available).
9
+ Select locally: slack workspace use <name> (writes .slack-cli/workspace)
10
+ Select globally: slack workspace use -g <name> (writes ~/.slack-cli/workspace)`)}function DJ(Q){let Z=R().profiles;if(process.env.SLACK_MCP_XOXP_TOKEN)return process.env.SLACK_MCP_XOXD_COOKIE;let z=Q??process.env.SLACK_WORKSPACE;if(z)return Z[z]?.cookie;let _=S(p());if(_)return Z[_]?.cookie;let E=S(m());if(E)return Z[E]?.cookie;return}import{readdirSync as LJ,readFileSync as RJ,existsSync as TJ}from"node:fs";import{homedir as NJ}from"node:os";import{join as T}from"node:path";import{pbkdf2Sync as rJ,createDecipheriv as tJ}from"node:crypto";import{execSync as sJ}from"node:child_process";function oJ(){let Q=NJ();if(process.platform==="darwin")return T(Q,"Library","Application Support","Slack","Local Storage","leveldb");if(process.platform==="linux")return T(Q,".config","Slack","Local Storage","leveldb");if(process.platform==="win32"){let J=process.env.APPDATA??T(Q,"AppData","Roaming");return T(J,"Slack","Local Storage","leveldb")}throw Error(`Unsupported platform: ${process.platform}`)}function aJ(){let Q=NJ();if(process.platform==="darwin")return T(Q,"Library","Application Support","Slack","Cookies");return""}function eJ(Q,J){if(Q.length<35||Q.slice(0,3).toString()!=="v10")throw Error("Not a v10 encrypted cookie");let Z=Q.slice(19,35),$=Q.slice(35),z=tJ("aes-128-cbc",J,Z);return z.setAutoPadding(!0),Buffer.concat([z.update($),z.final()]).toString("utf8")}function JQ(){if(process.platform!=="darwin")return;let Q=aJ();if(!TJ(Q))return;let J;try{J=sJ('security find-generic-password -w -s "Slack Safe Storage" -a "Slack Key"',{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trimEnd()}catch{return}let Z=rJ(J,"saltysalt",1003,16,"sha1");try{let{default:$}=w("bun:sqlite"),z=new $(Q,{readonly:!0}),_=z.prepare("SELECT encrypted_value FROM cookies WHERE name='d' AND host_key LIKE '%slack%'").get();if(z.close(),!_)return;return eJ(Buffer.from(_.encrypted_value),Z)}catch{return}}async function AJ(){let Q=oJ();if(!TJ(Q))throw Error(`Slack desktop app LevelDB not found at:
11
+ ${Q}
12
+ Is Slack installed and opened at least once?`);let J=LJ(Q).filter((G)=>G.endsWith(".ldb")||G.endsWith(".log"));if(J.length===0)throw Error(`No LevelDB data files found in ${Q}`);let Z=new Map;for(let G of J){let W;try{W=RJ(T(Q,G),"latin1")}catch{continue}if(G.endsWith(".log"))for(let X of W.matchAll(/"token":"(xoxc-[^"]+)"/g)){let B=X[1],Y=B.split("-")[1]??"";if(!Y||B.length<40)continue;let O=Math.max(0,X.index-2000),K=Math.min(W.length,X.index+2000),V=W.slice(O,K),H=V.match(/"url":"(https:\/\/[a-z0-9-]+\.slack\.com\/)"/),F=V.match(/"(?:team_name|name)":"([^"]{2,60})"/),M=Z.get(Y);if(!M||B.length>M.token.length){let j={token:B,teamId:Y},u=H?.[1]??M?.url,YJ=F?.[1]??M?.teamName;if(u)j.url=u;if(YJ)j.teamName=YJ;Z.set(Y,j)}}else for(let X of W.matchAll(/xoxc-(\d+)-(\d+)-(\d+)/g)){let B=X[1];if(Z.has(B))continue;let Y=X.index+X[0].length,O=Math.min(Y+10,W.length);while(Y<O&&(W.charCodeAt(Y)<48||W.charCodeAt(Y)>57))Y++;let K="";while(Y<W.length&&W.charCodeAt(Y)>=48&&W.charCodeAt(Y)<=57)K+=W[Y++];if(W[Y]!=="-")continue;Y++;let V="";while(Y<W.length){let F=W.charCodeAt(Y);if(F>=48&&F<=57||F>=97&&F<=102)V+=W[Y++];else break}if(V.length<20)continue;let H=`${X[0]}${K}-${V}`;Z.set(B,{token:H,teamId:B})}}let z=LJ(Q).filter((G)=>G.endsWith(".ldb")||G.endsWith(".log")).map((G)=>{try{return RJ(T(Q,G),"latin1").replace(/[^\x20-\x7e]/g," ")}catch{return""}}).join(" ");for(let G of Z.values()){if(G.teamName)continue;let W=z.indexOf(G.teamId);if(W!==-1){let X=Math.max(0,W-500),B=Math.min(z.length,W+500),O=z.slice(X,B).match(/"(?:team_name|name)":"([^"]{2,60})"/);if(O?.[1]){G.teamName=O[1];continue}}if(!G.teamName&&G.url){let X=G.url.match(/https:\/\/([a-z0-9-]+)\.slack\.com\//)?.[1];if(X){let B=z.indexOf(X);if(B!==-1){let Y=Math.max(0,B-500),O=Math.min(z.length,B+500),V=z.slice(Y,O).match(/"(?:team_name|name)":"([^"]{2,60})"/);if(V?.[1]){G.teamName=V[1];continue}}G.teamName=X.split("-").map((Y)=>Y.charAt(0).toUpperCase()+Y.slice(1)).join(" ")}}}let _=JQ(),E=[...Z.values()];if(_)for(let G of E)G.cookie=_;return E}function PJ(){return(process.env.SLACK_API_BASE??"https://slack.com/api").replace(/\/$/,"")}async function CJ(Q,J,Z,$){let z={};if($)z.Cookie=`d=${$}`;let E=await(await fetch(`${PJ()}/${J}`,{...Z,headers:{...Z.headers??{},Authorization:`Bearer ${Q}`,...z}})).json();if(E.ok!==!0){let G=E.error??"unknown";if(G==="invalid_auth"&&Q.startsWith("xoxc-")&&!$)throw Error(`Desktop app token (xoxc-) is not accepted by the public Slack API.
13
+ Replace it with an xoxp- user token:
14
+ slack workspace set-token <name> <xoxp-token>`);throw Error(`Slack error on ${J}: ${G}`)}return E}async function QQ(Q,J,Z,$){let z={};if($)z.Cookie=`d=${$}`;let E=await(await fetch(`${PJ()}/${J}`,{...Z,headers:{...Z.headers??{},Authorization:`Bearer ${Q}`,...z}})).json();if(E.ok!==!0){let G=E.error??"unknown";if((G==="invalid_auth"||G==="not_authed")&&!Q.startsWith("xoxc-"))throw Error(`The draft API requires a desktop app session token (xoxc-).
15
+ Import from Slack desktop: slack workspace import`);if((G==="invalid_auth"||G==="not_authed")&&!$)throw Error(`drafts.list also requires the xoxd session cookie.
16
+ Set it with: slack workspace set-cookie <workspace-name> <xoxd-value>
17
+ `+"Get xoxd from browser: DevTools → Application → Cookies → slack.com → d");throw Error(`Slack error on ${J}: ${G}`)}return E}function v(Q,J,Z={},$){let z=new URLSearchParams({token:Q,...Z});return QQ(Q,J,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:z.toString()},$)}function D(Q,J,Z,$){let z=new URLSearchParams(Z).toString();return CJ(Q,`${J}?${z}`,{method:"GET"},$)}function d(Q,J,Z){return CJ(Q,J,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Z)})}async function JJ(Q){let J=await D(Q,"auth.test",{});return{team:J.team??"",teamId:J.team_id??"",url:J.url??"",user:J.user??""}}async function x(Q,J,Z=20){return D(Q,"conversations.history",{channel:J,limit:String(Z)})}async function QJ(Q,J,Z,$=50){return D(Q,"conversations.replies",{channel:J,ts:Z,limit:String($)})}async function wJ(Q,J,Z,$){return D(Q,"search.messages",{query:J,sort:"timestamp",sort_dir:"desc",count:String(Math.min(Math.max(Z,1),100)),page:String(Math.max($,1))})}async function SJ(Q,J){return wJ(Q,J,100,1)}async function vJ(Q,J,Z){let z=1,_=[],E={ok:!0,messages:{}};while(!0){let W=await wJ(Q,J,100,z),X=c(W,["messages","matches"]),B=Array.isArray(X)?X:[];_.push(...B);let Y=Number(c(W,["messages","paging","pages"])??1);if(E=W,B.length===0||z>=Y||_.length>=Z)break;z+=1}let G=E;if(!G.messages)G.messages={};return G.messages.matches=_.slice(0,Z),G}async function bJ(Q,J,Z,$){let z={channel:J,text:Z,blocks:[{type:"markdown",text:Z}]};if($!==void 0)z.thread_ts=$;return(await d(Q,"chat.postMessage",z)).ts??""}async function xJ(Q,J,Z,$){return(await d(Q,"chat.update",{channel:J,ts:Z,text:$,blocks:[{type:"markdown",text:$}]})).ts??Z}async function l(Q){let J=[],Z="";while(!0){let $={limit:"200",types:"public_channel,private_channel,im,mpim"};if(Z)$.cursor=Z;let z=await D(Q,"conversations.list",$);if(J.push(...z.channels??[]),Z=z.response_metadata?.next_cursor??"",!Z)break}return{channels:J}}async function ZJ(Q,J){let $=(await d(Q,"conversations.open",{users:J})).channel?.id;if(!$)throw Error(`Failed to open DM with user ${J}`);return $}function i(Q){return Q.toLowerCase().replace(/[-_\s]/g,"")}function $J(Q){let J=Q.match(/(?:app\.slack\.com\/client\/T[A-Za-z0-9]+|[A-Za-z0-9-]+\.slack\.com\/archives)\/([A-Za-z0-9]+)(?:\/p(\d{10})(\d{6}))?/);if(!J)return;let Z=J[1],$=J[2]&&J[3]?`${J[2]}.${J[3]}`:void 0;return $?{channel:Z,ts:$}:{channel:Z}}function ZQ(Q){return $J(Q)?.channel}async function N(Q,J,Z){let $=ZQ(J);if($)return $;if(!J.startsWith("@")&&!J.startsWith("#")){if(/^[A-Za-z0-9]{9,}$/.test(J))return J;throw Error(`Target must start with # or @ (or be a Slack URL/ID), got: ${J}`)}let z=J.startsWith("@"),_=J.slice(1),E=i(_);if(z){let X="",B="";while(!0){let O={limit:"200"};if(B)O.cursor=B;let K=await D(Q,"users.list",O,Z);for(let V of K.members??[]){let H=i(V.name??""),F=i(V.real_name??""),M=i(V.profile?.display_name??"");if(H===E||F===E||M&&M===E){X=V.id??"";break}}if(X)break;if(B=K.response_metadata?.next_cursor??"",!B)break}if(!X)throw Error(`User not found: ${J}`);let Y="";while(!0){let O={types:"im",limit:"200"};if(Y)O.cursor=Y;let K=await D(Q,"conversations.list",O,Z);for(let V of K.channels??[])if(V.user===X)return String(V.id??"");if(Y=K.response_metadata?.next_cursor??"",!Y)break}throw Error(`No existing DM with ${J} (${X}). Open it once in Slack first.`)}let G=_.toLowerCase(),W="";while(!0){let X={limit:"200",types:"public_channel,private_channel",exclude_archived:"true"};if(W)X.cursor=W;let B=await D(Q,"conversations.list",X,Z);for(let Y of B.channels??[])if(String(Y.name??"").toLowerCase()===G)return String(Y.id??"");if(W=B.response_metadata?.next_cursor??"",!W)break}throw Error(`channel not found: ${J}`)}async function zJ(Q,J){try{let Z=await D(Q,"users.info",{user:J}),$=Z.user?.profile?.display_name,z=$&&$.length>0?$:Z.user?.real_name??Z.user?.name??J,_=Z.user?.name??J;return[z,_]}catch{return[J,J]}}async function GJ(Q,J){try{let Z=await D(Q,"users.info",{user:J}),$=Z.user?.profile?.display_name;if($&&$.length>0)return $;return Z.user?.real_name??Z.user?.name??J}catch{return J}}async function I(Q,J){return v(Q,"drafts.list",{},J)}async function IJ(Q,J,Z,$){let{randomUUID:z}=await import("node:crypto"),_=JSON.stringify([{type:"rich_text",elements:[{type:"rich_text_section",elements:[{type:"text",text:Z}]}]}]);return v(Q,"drafts.create",{client_msg_id:z(),destinations:JSON.stringify([{channel_id:J}]),blocks:_,file_ids:JSON.stringify([]),is_from_composer:"true"},$)}async function yJ(Q,J,Z,$,z){let _=JSON.stringify([{type:"rich_text",elements:[{type:"rich_text_section",elements:[{type:"text",text:$}]}]}]),E=(Date.now()/1000).toFixed(6);return v(Q,"drafts.update",{draft_id:J,client_last_updated_ts:E,destinations:JSON.stringify([{channel_id:Z}]),blocks:_,file_ids:JSON.stringify([])},z)}async function fJ(Q,J,Z){return v(Q,"conversations.info",{channel:J},Z)}async function gJ(Q,J,Z){let $=(Date.now()/1000).toFixed(6);return v(Q,"drafts.delete",{draft_id:J,client_last_updated_ts:$},Z)}async function WJ(Q,J){let Z=await v(Q,"auth.test",{},J);return{userId:String(Z.user_id??""),teamId:String(Z.team_id??"")}}async function uJ(Q,J,Z,$={}){let{statSync:z,readFileSync:_}=await import("node:fs"),{basename:E}=await import("node:path"),G=z(Z),W=E(Z),X=await D(Q,"files.getUploadURLExternal",{filename:W,length:String(G.size)}),B=X.upload_url,Y=X.file_id;if(!B||!Y)throw Error("files.getUploadURLExternal returned no upload_url/file_id");let O=_(Z),K=await fetch(B,{method:"POST",headers:{"Content-Type":"application/octet-stream"},body:O});if(!K.ok)throw Error(`Upload PUT failed: ${K.status} ${K.statusText}`);let V={files:[{id:Y,title:$.title??W}],channel_id:J};if($.threadTs)V.thread_ts=$.threadTs;if($.initialComment)V.initial_comment=$.initialComment;let H=await d(Q,"files.completeUploadExternal",V),F={fileId:Y},M=H.files?.[0]?.permalink;if(M)F.permalink=M;return F}function c(Q,J){let Z=Q;for(let $ of J){if(Z===void 0||Z===null)return;if(typeof $==="number"){if(!Array.isArray(Z))return;Z=Z[$]??void 0}else{if(typeof Z!=="object"||Array.isArray(Z))return;Z=Z[$]}}return Z}function $Q(){return(process.env.SLACK_API_BASE??"https://slack.com/api").replace(/\/$/,"")}async function zQ(Q,J,Z,$){let z={};if($)z.Cookie=`d=${$}`;let E=await(await fetch(`${$Q()}/${J}`,{...Z,headers:{...Z.headers??{},Authorization:`Bearer ${Q}`,...z}})).json();if(E.ok!==!0){let G=E.error??"unknown";if(G==="invalid_auth"&&Q.startsWith("xoxc-")&&!$)throw Error(`Desktop app token (xoxc-) is not accepted by the public Slack API.
18
+ Replace it with an xoxp- user token:
19
+ slack workspace set-token <name> <xoxp-token>`);throw Error(`Slack error on ${J}: ${G}`)}return E}function GQ(Q,J,Z,$){let z=new URLSearchParams(Z).toString();return zQ(Q,`${J}?${z}`,{method:"GET"},$)}async function hJ(Q,J){try{let Z=await GQ(Q,"users.info",{user:J}),$=Z.user?.profile?.display_name;if($&&$.length>0)return $;return Z.user?.real_name??Z.user?.name??J}catch{return J}}async function A(Q,J,Z){let $=J,z=[],_=$,E=0;while(!0){let G=_.indexOf("<@U",E);if(G===-1)break;let W=_.slice(G+2),X=W.search(/[>|]/),B=X===-1?W:W.slice(0,X);if(B.length>=9)z.push(B);E=G+1}for(let G of z){if(!Z.has(G))Z.set(G,await hJ(Q,G));let W=Z.get(G)??G;$=$.replaceAll(`<@${G}>`,`@${W}`),$=$.replace(new RegExp(`<@${G}\\|[^>]*>`,"g"),`@${W}`)}return $}function b(Q){return Q.replace(/<!date\^(\d+)\^[^|>]*(?:\|([^>]*))?>/g,(J,Z,$)=>{let z=Number(Z);if(!Number.isFinite(z))return $??"date";let _=new Date(z*1000);if(Number.isNaN(_.getTime()))return $??"date";let E=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],G=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],W=E[_.getUTCDay()]??"",X=G[_.getUTCMonth()]??"";return`${W}, ${X} ${String(_.getUTCDate()).padStart(2,"0")}, ${_.getUTCFullYear()}`})}function pJ(Q,J=new Date){let Z=new Date(Q*1000),$=_J(J),z=_J(new Date(J.getTime()-86400000)),_=_J(Z);if(_===$)return"Today";if(_===z)return"Yesterday";let E=Z.toLocaleDateString("en-US",{weekday:"long"}),G=Z.toLocaleDateString("en-US",{month:"short"});return`${E}, ${G} ${String(Z.getDate()).padStart(2,"0")}`}function _J(Q){return`${Q.getFullYear()}-${Q.getMonth()}-${Q.getDate()}`}function mJ(Q){let J=new Date(Q*1000);return`${String(J.getHours()).padStart(2,"0")}:${String(J.getMinutes()).padStart(2,"0")}`}function kJ(Q){let J=new Date(Q*1000),Z=J.getFullYear(),$=String(J.getMonth()+1).padStart(2,"0"),z=String(J.getDate()).padStart(2,"0"),_=String(J.getHours()).padStart(2,"0"),E=String(J.getMinutes()).padStart(2,"0");return`${Z}-${$}-${z} ${_}:${E}`}function y(Q){if(!cJ(Q))return;let J=_Q(Q,"utf8");for(let Z of J.split(`
20
+ `)){let $=Z.trim();if(!$||$.startsWith("#"))continue;let z=$.indexOf("=");if(z===-1)continue;let _=$.slice(0,z).trim(),E=$.slice(z+1).trim();if(E.startsWith('"')&&E.endsWith('"')||E.startsWith("'")&&E.endsWith("'"))E=E.slice(1,-1);if(!(_ in process.env))process.env[_]=E}}function YQ(Q){EQ(Q,{recursive:!0});let J=C(Q,".gitignore");if(!cJ(J))XQ(J,`*
21
+ `)}function BQ(){y(C(iJ(),".slack-cli",".env.local")),y(C(iJ(),".config/slack-cli",".env")),y(C(process.cwd(),".slack-cli",".env.local")),y(C(process.cwd(),".env.local")),y(C(process.cwd(),".env"))}function OQ(Q){return WQ("sha256").update(Q).digest("hex")}function q(Q){return Q&&typeof Q==="object"&&!Array.isArray(Q)?Q:{}}function U(Q){return Array.isArray(Q)?Q:[]}function n(Q){return Number(Q.ts??0)}async function EJ(Q,J,Z){let $=J.user;if(typeof $==="string"){if(!Z.has($))Z.set($,await GJ(Q,$));return Z.get($)??$}return typeof J.username==="string"?J.username:"bot"}function r(Q){let J=new Date(Q*1000),Z=J.getUTCFullYear(),$=String(J.getUTCMonth()+1).padStart(2,"0"),z=String(J.getUTCDate()).padStart(2,"0"),_=String(J.getUTCHours()).padStart(2,"0"),E=String(J.getUTCMinutes()).padStart(2,"0"),G=String(J.getUTCSeconds()).padStart(2,"0");return`${Z}-${$}-${z} ${_}:${E}:${G}`}async function dJ(Q,J,Z){let $=n(J),z=r($),_="?",E="?";if(typeof J.user==="string"){let B=J.user,Y=B,O="@"+B;if(!Z.has(Y)||!Z.has(O)){let[K,V]=await zJ(Q,B);Z.set(Y,K),Z.set(O,V)}_=Z.get(Y)??B,E=Z.get(O)??B}else if(typeof J.username==="string")_=J.username,E=J.username;let G=typeof J.text==="string"?J.text:"",X=b(await A(Q,G,Z)).split(`
22
+ `).join(" \u21B5 ");return`[${z}] <${_}|@${E}> ${X}`}async function VQ(Q,J,Z){let $=await N(Q,J),z=await x(Q,$,Z),_=U(z.messages).map(q),E=new Map;for(let G of _.reverse())console.log(await dJ(Q,G,E))}async function KQ(Q,J,Z,$){let z=await N(Q,J),_=await QJ(Q,z,Z,$),E=U(_.messages).map(q),G=new Map;for(let W of E)console.log(await dJ(Q,W,G))}async function HQ(Q){let J=await l(Q),Z=U(J.channels).map(q).filter((z)=>z.is_member===!0).sort((z,_)=>Number(_.updated??0)-Number(z.updated??0)).slice(0,10),$=new Map;for(let z of Z){let _=String(z.id??""),E=typeof z.name==="string"?z.name:typeof z.user==="string"?z.user:_,G=await x(Q,_,5),W=U(G.messages).map(q).filter((X)=>X.subtype===void 0||X.subtype===null).filter((X)=>{let B=typeof X.text==="string"?X.text:"";return B.length>0&&!B.startsWith("<@")}).slice(0,3);if(W.length===0)continue;console.log(`\u2500\u2500 #${E} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);for(let X of W){let B=await EJ(Q,X,$),Y=(typeof X.text==="string"?X.text:"").split(`
23
+ `)[0]??"",O=await A(Q,Y,$);console.log(` ${B}: ${O}`)}}}async function FQ(Q,J){let Z=await SJ(Q,"to:me"),$=U(c(Z,["messages","matches"])).map(q),z=new Map,_="";for(let E of $.slice(0,J)){let G=n(E),W=pJ(G);if(W!==_){if(_!=="")console.log("");console.log(` ${W}`),console.log(" \u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504\u2504"),_=W}let X=q(E.channel),B=X.is_im===!0,Y=typeof X.name==="string"?X.name:"dm",O;if(B&&Y.startsWith("U")){if(!z.has(Y))z.set(Y,await GJ(Q,Y));O=`@${z.get(Y)??Y}`}else if(B)O=`@${Y}`;else O=`#${Y}`;let K=await EJ(Q,E,z),V=typeof E.text==="string"?E.text:"",F=(b(await A(Q,V,z)).split(`
24
+ `)[0]??"").slice(0,80);console.log(` ${B?"\uD83D\uDCAC":"\uD83D\uDD14"} ${O} ${mJ(G)}`),console.log(` ${K}: ${F}`)}}async function qQ(Q,J,Z,$){let z=await l(Q),_=U(z.channels).map(q).filter((E)=>$||E.is_member===!0).filter((E)=>{if(!Z)return!0;return(typeof E.name==="string"?E.name:"").toLowerCase().includes(Z.toLowerCase())}).sort((E,G)=>Number(G.updated??0)-Number(E.updated??0)).slice(0,J);for(let E of _){let G=String(E.id??""),W=typeof E.name==="string"?E.name:typeof E.user==="string"?E.user:G,X=E.is_im===!0,B=E.is_mpim===!0,Y=X||B?"@":"#",O=E.is_member===!0?"":" (not joined)";console.log(`${Y}${W} ${G}${O}`)}}async function MQ(Q,J,Z){let $=await vJ(Q,J,Z);console.log(JSON.stringify($,null,2))}async function jQ(Q,J,Z,$){let z=Math.floor(Date.now()/1000)-J*86400,_=await l(Q),E=U(_.channels).map(q).filter((B)=>B.is_member===!0&&B.is_im!==!0&&B.is_mpim!==!0).filter((B)=>{if(!$)return!0;return(typeof B.name==="string"?B.name:"").toLowerCase().includes($.toLowerCase())}).sort((B,Y)=>Number(Y.updated??0)-Number(B.updated??0)),G=new Map,W=0,X=0;for(let B of E){let Y=String(B.id??""),O=typeof B.name==="string"?B.name:Y,K;try{K=await x(Q,Y,Z)}catch(H){console.error(` SKIP #${O}: ${H.message}`);continue}let V=U(K.messages).map(q).filter((H)=>(H.subtype===void 0||H.subtype===null)&&n(H)>=z);if(V.length===0)continue;X+=1,W+=V.length,console.log(`## #${O} (${V.length} msgs)
25
+ `);for(let H of[...V].reverse()){let F=await EJ(Q,H,G),M=typeof H.text==="string"?H.text:"",u=b(await A(Q,M,G)).split(`
26
+ `).join(" \u21B5 ");console.log(`[${kJ(n(H))}] ${F}: ${u}`)}console.log("")}console.error(`Dumped ${W} messages across ${X} channels (cutoff: ${J}d)`)}function UQ(Q){let J=[];function Z($){if(typeof $==="string"){J.push($);return}if(Array.isArray($)){for(let z of $)Z(z);return}if($&&typeof $==="object"){let z=$;if(typeof z.text==="string"){J.push(z.text);return}if(Array.isArray(z.elements))Z(z.elements);else if(Array.isArray(z.content))Z(z.content)}}for(let $ of Q)Z($);return J.join("").trim()}async function lJ(Q,J,Z){let $=new Map;return async(z)=>{if(!z)return"(unknown)";if($.has(z))return $.get(z);try{let _=await fJ(Q,z,J),E=q(_.channel),G;if(E.is_im===!0){let W=typeof E.user==="string"?E.user:"";if(!W||W===Z)G="@self";else{let[X]=await zJ(Q,W);G=`@${X}`}}else G=`#${typeof E.name==="string"?E.name:z}`;return $.set(z,G),G}catch{return z}}}function XJ(Q){let J=q(U(Q.destinations)[0]);return String(J.channel_id??Q.channel_id??Q.channel??"")}function t(Q){return UQ(U(Q.blocks))||(typeof Q.text==="string"?Q.text:"(no text)")}async function DQ(Q,J,Z=!1){let $=await I(Q,J),z=U($.drafts??$.messages??[]).map(q).filter((X)=>X.is_deleted!==!0),_=Z?z:z.filter((X)=>X.is_sent!==!0);if(_.length===0){console.log(Z?"No drafts.":"No pending drafts. Run with --all to include sent.");return}let E="";try{({userId:E}=await WJ(Q,J))}catch{}let G=await lJ(Q,J,E),W=new Map;for(let X of _){let B=XJ(X),Y=t(X),O=Number(X.date_created??X.date_create??0),K=O?r(O):"?",V=await G(B),H=typeof X.id==="string"?X.id:"",F=X.is_sent===!0?" [SENT]":"",M=b(await A(Q,Y,W));console.log(`\u2500\u2500 ${H} ${V} [${K}]${F}`);for(let j of M.split(`
27
+ `))console.log(` ${j}`)}}async function LQ(Q,J,Z){let $=await I(Q,J),z=U($.drafts).map(q).find((V)=>String(V.id)===Z);if(!z)console.error(`Draft not found: ${Z}`),process.exit(1);let _="";try{({userId:_}=await WJ(Q,J))}catch{}let E=await lJ(Q,J,_),G=XJ(z),W=t(z),X=Number(z.date_created??0),B=String(z.last_updated_ts??"?"),Y=await E(G),K=b(await A(Q,W,new Map));console.log(`id: ${z.id}`),console.log(`channel: ${Y} (${G})`),console.log(`created: ${X?r(X):"?"}`),console.log(`updated: ${r(Number(B.split(".")[0]))}`),console.log(`status: ${z.is_sent===!0?"sent":"pending"}`),console.log("---"),console.log(K)}function f(...Q){return OQ(Q.join(`
28
+ `)).slice(0,4)}function g(Q,J,Z){for(let $ of Z)console.log($);if(Q!==void 0)console.error(`Code mismatch (got ${Q}, expected ${J})`);console.error(`Rerun with --code=${J}`),process.exit(1)}function RQ(Q){let J=$J(Q);if(J)return J.ts?{ref:J.channel,ts:J.ts}:{ref:J.channel};if(Q.startsWith("#")||Q.startsWith("@")){let Z=Q.indexOf(":");if(Z>0){let $=Q.slice(Z+1);if(/^\d{10}\.\d{6}$/.test($))return{ref:Q.slice(0,Z),ts:$}}}return{ref:Q}}async function TQ(Q,J){let{ref:Z,ts:$}=RQ(J.target);if(!$)console.error("Error: target must embed a message ts (e.g. #chan:1700000000.000100 or a Slack permalink URL)"),process.exit(2);let z;if(J.channelId)z=J.channelId;else z=await N(Q,Z);let _=await QJ(Q,z,$,1),G=U(_.messages).map(q).find((Y)=>String(Y.ts)===$);if(!G)console.error(`Message not found at ts=${$} in channel ${z}`),process.exit(1);let W=typeof G.text==="string"?G.text:"",X=f(W,J.newText);if(J.code!==X)g(J.code,X,["\u2500\u2500\u2500 Original message \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",...W.split(`
29
+ `).map((Y)=>` ${Y}`),"\u2500\u2500\u2500 Replacing with \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",...J.newText.split(`
30
+ `).map((Y)=>` ${Y}`),"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"]);let B=await xJ(Q,z,$,J.newText);console.log(`\u2713 Edited (ts: ${B})`)}async function NQ(Q,J){let Z;if(J.channelId)Z=J.channelId;else if(J.userId)Z=await ZJ(Q,J.userId);else if(J.target.startsWith("#")||J.target.startsWith("@"))Z=await N(Q,J.target);else console.error(`Error: target must be #channel-name or @username (got: ${J.target})`),console.error("Use --channel-id=<ID> or --user-id=<ID> to send by raw ID."),process.exit(1);let $=await x(Q,Z,1),z=U($.messages).map(q).filter((X)=>X.subtype===void 0||X.subtype===null)[0],_=typeof z?.text==="string"?z.text:"",E=typeof z?.user==="string"?z.user:"?",G=f(_,J.message);if(J.code!==G)g(J.code,G,["\u2500\u2500\u2500 Last message in channel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",` ${E}: ${_.split(`
31
+ `)[0]?.slice(0,100)??"(empty)"}`,"\u2500\u2500\u2500 Sending \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",` To: ${J.target}${J.thread?` (thread ${J.thread})`:""}`,` Message: ${J.message}`,"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"]);let W=await bJ(Q,Z,J.message,J.thread);console.log(`\u2713 Sent (ts: ${W})`)}async function AQ(Q,J){let{statSync:Z,existsSync:$}=await import("fs"),{basename:z}=await import("path");if(!$(J.filePath))console.error(`Error: file not found: ${J.filePath}`),process.exit(1);let _=Z(J.filePath),E;if(J.channelId)E=J.channelId;else if(J.userId)E=await ZJ(Q,J.userId);else if(J.target.startsWith("#")||J.target.startsWith("@"))E=await N(Q,J.target);else console.error(`Error: target must be #channel-name or @username (got: ${J.target})`),process.exit(1);let G=z(J.filePath),W=J.title??G,X=_.size<1024?`${_.size} B`:_.size<1048576?`${(_.size/1024).toFixed(1)} KB`:`${(_.size/1048576).toFixed(1)} MB`,B=f(E,J.filePath,W);if(J.code!==B)g(J.code,B,["\u2500\u2500\u2500 Uploading file \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",` To: ${J.target}${J.thread?` (thread ${J.thread})`:""}`,` File: ${J.filePath}`,` Title: ${W}`,` Size: ${X}`,"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"]);let Y={title:W};if(J.thread!==void 0)Y.threadTs=J.thread;if(J.comment!==void 0)Y.initialComment=J.comment;let{fileId:O,permalink:K}=await uJ(Q,E,J.filePath,Y);console.log(`\u2713 Uploaded (file_id: ${O}${K?`, url: ${K}`:""})`)}function P(){console.error(["Usage: slack [--workspace=<name>] <command> [args]","Commands:"," msgs [<#channel|@user|url>] [-n|--limit N]"," thread <#channel|@user|url> <ts> [-n|--limit N]"," channels [-n|--limit N] [--filter STR] [--all]"," news [-l|--limit N]"," search <query> [-n|--count N]"," drafts [--all] list pending drafts (--all includes sent)"," drafts new <#channel|@user> <text>"," drafts get <draft-id>"," drafts edit <draft-id> [--code=XXXX] <new-text>"," drafts delete <draft-id> [--code=XXXX]"," send <target> <message> [--thread TS] [--code XXXX] [--channel-id ID] [--user-id ID]"," edit <#chan:ts|@user:ts|url> <new-text> [--code XXXX] [--channel-id ID]"," upload <target> <file> [--title TEXT] [--thread TS] [--comment TEXT] [--code XXXX] [--channel-id ID] [--user-id ID]"," dump [-d|--days N] [-l|--limit N] [-f|--filter STR]"," workspace ls|list"," workspace import (auto-import from Slack desktop app)"," workspace add <name> <token>"," workspace set-token <name> <token>"," workspace set-cookie <name> <xoxd> (store xoxd cookie for draft API)"," workspace use <name>"," workspace remove <name>"," workspace current"].join(`
32
+ `)),process.exit(2)}async function PQ(Q,J){switch(Q){case"list":case"ls":{let Z=e();if(Z.length===0){console.log("No workspaces configured."),console.log("Import from the Slack desktop app: slack workspace import"),console.log("Or add manually: slack workspace add <name> <token>");return}for(let{name:$,profile:z,current:_}of Z)console.log(`${_?"* ":" "}${$} ${z.team} (${z.user}) ${z.url??""}`);return}case"import":{console.error("Scanning Slack desktop app...");let Z=await AJ();if(Z.length===0){console.error("No sessions found. Make sure Slack is installed and you have logged in.");return}for(let $ of Z){let z=$.teamName??$.teamId;console.error(` Found: ${z} ${$.url??""}`);let _=z.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,""),E={token:$.token,team:$.teamName??$.teamId,teamId:$.teamId,url:$.url??"",user:""};if($.cookie)E.cookie=$.cookie;k(_,E);let G=$.cookie?" + xoxd cookie":"";console.log(`Added workspace "${_}": ${z}${G}`)}console.log(`
33
+ Note: desktop app tokens (xoxc-) are internal Slack tokens.`),console.log("If API calls fail, replace with an xoxp- token:"),console.log(" slack workspace set-token <name> <xoxp-token>"),console.log(`
34
+ Done. Run: slack workspace ls`);return}case"add":{let[Z,$]=J;if(!Z||!$)console.error("Usage: slack workspace add <name> <token>"),process.exit(2);console.error("Verifying token...");let z=await JJ($);k(Z,{token:$,...z}),console.log(`Added workspace "${Z}": ${z.team} (${z.user})`);return}case"set-token":{let[Z,$]=J;if(!Z||!$)console.error("Usage: slack workspace set-token <name> <token>"),console.error(" Updates the token for an existing workspace profile."),process.exit(2);console.error("Verifying token...");let z=await JJ($);k(Z,{token:$,...z}),console.log(`Updated workspace "${Z}": ${z.team} (${z.user})`);return}case"set-cookie":{let[Z,$]=J;if(!Z||!$)console.error("Usage: slack workspace set-cookie <name> <xoxd-value>"),console.error(" Stores the xoxd session cookie for draft API access."),console.error(" Get it from: DevTools \u2192 Application \u2192 Cookies \u2192 slack.com \u2192 d"),process.exit(2);let z=$.startsWith("d=")?$.slice(2):$;qJ(Z,z),console.log(`Cookie set for workspace "${Z}". Run: slack drafts`);return}case"use":{let Z=J.includes("-g"),$=J.find((_)=>_!=="-g");if(!$)console.error("Usage: slack workspace use [-g] <name>"),console.error(" -g write to ~/.slack-cli/workspace (global)"),console.error(" default: write to .slack-cli/workspace (local cwd)"),process.exit(2);if(!Z)YQ(C(process.cwd(),".slack-cli"));if(jJ($,Z),console.log(`Switched to workspace "${$}" ${Z?"globally (~/.slack-cli/workspace)":"locally (.slack-cli/workspace)"}`),!Z)console.log("Tip: add .slack-cli/ to your .gitignore");return}case"remove":{let[Z]=J;if(!Z)console.error("Usage: slack workspace remove <name>"),process.exit(2);MJ(Z),console.log(`Removed workspace "${Z}"`);return}case"current":{let $=e().find((z)=>z.current);if(!$){console.log("No workspace selected");return}console.log(`${$.name} ${$.profile.team} (${$.profile.user})`);return}default:P()}}async function CQ(){BQ();let Q=process.argv.slice(2),J,Z=[];for(let G of Q){let W=G.match(/^--workspace=(.+)$/);if(W)J=W[1];else Z.push(G)}let[$,...z]=Z;if($==="workspace"){await PQ(z[0]??"",z.slice(1));return}let _=UJ(J),E=DJ(J);switch($){case"msgs":{let{values:G,positionals:W}=L({args:z,allowPositionals:!0,options:{limit:{type:"string",short:"n",default:"20"}},strict:!0});if(W[0])await VQ(_,W[0],Number(G.limit));else await HQ(_);return}case"thread":{let{values:G,positionals:W}=L({args:z,allowPositionals:!0,options:{limit:{type:"string",short:"n",default:"100"}},strict:!0}),X=W[0],B=W[1];if(!X||!B)P();await KQ(_,X,B,Number(G.limit));return}case"channels":{let{values:G}=L({args:z,options:{limit:{type:"string",short:"n",default:"200"},filter:{type:"string",short:"f"},all:{type:"boolean"}},strict:!0});await qQ(_,Number(G.limit),G.filter,G.all);return}case"news":{let{values:G}=L({args:z,options:{limit:{type:"string",short:"l",default:"20"}},strict:!0});await FQ(_,Number(G.limit));return}case"drafts":{let G=z[0];if(G==="new"||G==="save"){let W=z.slice(1),X=W[0],B=W.slice(1).join(" ");if(!X||!B)console.error("Usage: slack drafts new <#channel|@user> <text>"),process.exit(2);let Y=await N(_,X,E),O=await IJ(_,Y,B,E);console.log(`\u2713 Draft created (id: ${q(O.draft).id??"?"})`)}else if(G==="get"){let W=z[1];if(!W)console.error("Usage: slack drafts get <draft-id>"),process.exit(2);await LQ(_,E,W)}else if(G==="edit"||G==="update"){let W=z[1],X=z.find((j)=>j.startsWith("--code="))?.slice(7),Y=z.slice(2).filter((j)=>!j.startsWith("--code=")).join(" ");if(!W||!Y)console.error("Usage: slack drafts edit <draft-id> <new-text>"),process.exit(2);let O=await I(_,E),K=U(O.drafts).map(q).find((j)=>String(j.id)===W);if(!K)console.error(`Draft not found: ${W}`),process.exit(1);let V=t(K),H=f(V,Y);if(X!==H)g(X,H,["\u2500\u2500\u2500 Current draft content \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",...V.split(`
35
+ `).map((j)=>` ${j}`),"\u2500\u2500\u2500 Replacing with \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",...Y.split(`
36
+ `).map((j)=>` ${j}`),"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"]);let F=XJ(K),M=await yJ(_,W,F,Y,E);console.log(`\u2713 Draft updated (id: ${q(M.draft).id??"?"})`)}else if(G==="delete"||G==="rm"){let W=z[1],X=z.find((V)=>V.startsWith("--code="))?.slice(7);if(!W)console.error("Usage: slack drafts delete <draft-id>"),process.exit(2);let B=await I(_,E),Y=U(B.drafts).map(q).find((V)=>String(V.id)===W);if(!Y)console.error(`Draft not found: ${W}`),process.exit(1);let O=t(Y),K=f(W,O);if(X!==K)g(X,K,["\u2500\u2500\u2500 Deleting draft \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",` id: ${W}`,...O.split(`
37
+ `).map((V)=>` ${V}`),"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"]);await gJ(_,W,E),console.log(`\u2713 Draft deleted (id: ${W})`)}else{let W=z.includes("--all")||z.includes("-a");await DQ(_,E,W)}return}case"search":{let{values:G,positionals:W}=L({args:z,allowPositionals:!0,options:{count:{type:"string",short:"n",default:"100"}},strict:!0}),X=W[0];if(!X)P();await MQ(_,X,Number(G.count));return}case"send":{let{values:G,positionals:W}=L({args:z,allowPositionals:!0,options:{thread:{type:"string"},code:{type:"string"},"channel-id":{type:"string"},"user-id":{type:"string"}},strict:!0}),X=W[0],B=W[1];if(!X||!B)P();let Y={target:X,message:B};if(G.thread!==void 0)Y.thread=G.thread;if(G.code!==void 0)Y.code=G.code;if(G["channel-id"]!==void 0)Y.channelId=G["channel-id"];if(G["user-id"]!==void 0)Y.userId=G["user-id"];await NQ(_,Y);return}case"edit":{let{values:G,positionals:W}=L({args:z,allowPositionals:!0,options:{code:{type:"string"},"channel-id":{type:"string"}},strict:!0}),X=W[0],B=W[1];if(!X||!B)P();let Y={target:X,newText:B};if(G.code!==void 0)Y.code=G.code;if(G["channel-id"]!==void 0)Y.channelId=G["channel-id"];await TQ(_,Y);return}case"upload":{let{values:G,positionals:W}=L({args:z,allowPositionals:!0,options:{title:{type:"string"},thread:{type:"string"},comment:{type:"string"},code:{type:"string"},"channel-id":{type:"string"},"user-id":{type:"string"}},strict:!0}),X=W[0],B=W[1];if(!X||!B)P();let Y={target:X,filePath:B};if(G.title!==void 0)Y.title=G.title;if(G.thread!==void 0)Y.thread=G.thread;if(G.comment!==void 0)Y.comment=G.comment;if(G.code!==void 0)Y.code=G.code;if(G["channel-id"]!==void 0)Y.channelId=G["channel-id"];if(G["user-id"]!==void 0)Y.userId=G["user-id"];await AQ(_,Y);return}case"dump":{let{values:G}=L({args:z,options:{days:{type:"string",short:"d",default:"7"},limit:{type:"string",short:"l",default:"200"},filter:{type:"string",short:"f"}},strict:!0});await jQ(_,Number(G.days),Number(G.limit),G.filter);return}default:P()}}CQ().catch((Q)=>{let J=Q instanceof Error?Q.message:String(Q);console.error(`Error: ${J}`),process.exit(1)});
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "slack-term",
3
+ "version": "1.7.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/snomiao/slack-cli.git"
7
+ },
8
+ "devDependencies": {
9
+ "@semantic-release/commit-analyzer": "^13",
10
+ "@semantic-release/github": "^12",
11
+ "@semantic-release/npm": "^13",
12
+ "@semantic-release/release-notes-generator": "^14",
13
+ "@types/bun": "latest",
14
+ "@types/node": "^22",
15
+ "@vitest/coverage-v8": "^4.1.4",
16
+ "semantic-release": "^24",
17
+ "typescript": "^5.6",
18
+ "vitest": "^4.1.4"
19
+ },
20
+ "bin": {
21
+ "slack": "./ts/cli.ts"
22
+ },
23
+ "description": "Slack CLI — read activity/news/mentions, search, send messages with a confirm-hash safety gate.",
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "files": [
28
+ "ts/",
29
+ "dist/",
30
+ "README.md",
31
+ "SKILL.md",
32
+ "LICENSE"
33
+ ],
34
+ "homepage": "https://github.com/snomiao/slack-cli",
35
+ "keywords": [
36
+ "slack",
37
+ "cli",
38
+ "messaging",
39
+ "agent",
40
+ "terminal"
41
+ ],
42
+ "license": "MIT",
43
+ "overrides": {
44
+ "@semantic-release/npm": "^13",
45
+ "@semantic-release/github": "^12"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "dev": "bun run ts/cli.ts",
52
+ "build": "bun build ts/cli.ts --target=node --outfile dist/cli.js --minify",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run --coverage",
55
+ "test:watch": "vitest",
56
+ "test:parity": "bash tests/parity.sh",
57
+ "record": "bun run tests/record.ts",
58
+ "anonymize": "bun run tests/anonymize.ts",
59
+ "prepublishOnly": "bun run typecheck && bun run build"
60
+ },
61
+ "type": "module"
62
+ }