slack-term 1.24.0 → 1.26.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 CHANGED
@@ -54,13 +54,23 @@ slack news --limit 5
54
54
  # Recent messages across joined channels
55
55
  slack msgs
56
56
 
57
+ # Channel/DM history. Thread parents are marked `[+N replies]`, so a line with
58
+ # replies is distinguishable from one without — open it with `slack thread`.
59
+ # --json adds reply_count / reply_users_count / latest_reply.
60
+ slack read "#general"
61
+ slack read "#general" --unreplied # only threads/posts whose last word isn't yours
62
+
57
63
  # Search messages
58
64
  slack search "deploy"
59
65
  slack search "deploy" --count 50
60
66
 
61
67
  # Send a message (two-step confirm — quote #channel)
62
68
  slack send "#general" "Hello team"
63
- # Prints a destination preview + confirm code; rerun with --code=<code> to actually send
69
+ # Prints who you're acting as (From: @handle (Uxxxx) — Workspace) + a destination
70
+ # preview + confirm code; rerun with --code=<code> to actually send. Every gated write
71
+ # (send/edit/delete/upload/schedule/channel create/drafts) shows that From: line and
72
+ # binds it into the code, so a profile switch mid-flow invalidates it instead of acting
73
+ # as the wrong account.
64
74
  slack send "#general" "Hello team" --code=<code>
65
75
 
66
76
  # Reply in a thread — #chan:<thread_ts>, or just paste a message permalink
@@ -71,7 +81,7 @@ slack send "https://acme.slack.com/archives/C0123456789/p1700000000000100" "Repl
71
81
  # Resolves via users.list, then the channel's members so Slack Connect guests work;
72
82
  # any handle that can't be resolved is left as plain text. The confirm preview shows
73
83
  # the converted message before sending. Use --no-mentions to keep @text literal.
74
- slack send "#general" "thanks @t.matsuda19790127 and @taku"
84
+ slack send "#general" "thanks @t.yamada19850101 and @alice"
75
85
  slack send "#general" "ping @ops on call" --no-mentions # leave @ops as plain text
76
86
 
77
87
  # Edit or delete a sent message (same confirm-code gate)
@@ -83,6 +93,14 @@ slack delete "<permalink>"
83
93
  slack react "#general:1700000000.000100" white_check_mark
84
94
  slack react "<permalink>" eyes --remove # take a reaction back
85
95
 
96
+ # Task tracking on top of reactions — :pushpin: marks a message as a task,
97
+ # a second reaction carries its progress (see "todo" below)
98
+ slack todo ls # my open tasks
99
+ slack todo ls --state untriaged --in "#general"
100
+ slack todo set "#general:1700000000.000100" doing
101
+ slack todo flag "<permalink>" blocked
102
+ slack todo doctor --in "#general" # find messages stuck in two states
103
+
86
104
  # Bulk export channel history
87
105
  slack dump --days 7 --filter eng
88
106
 
@@ -94,6 +112,98 @@ slack tail "#general" --me # only messages that mention you
94
112
  slack tail "@bob" --exit-on-message --timeout 30m # wait for a reply, then exit
95
113
  ```
96
114
 
115
+ ### todo — task tracking on reactions
116
+
117
+ Tasks are just messages. A **marker** reaction (📌 `:pushpin:`) says "this is a task";
118
+ a **progress** reaction says where it stands; **reason flags** stack on top:
119
+
120
+ | axis | reaction | meaning |
121
+ | --- | --- | --- |
122
+ | progress 1 | ✅ `white_check_mark` | done |
123
+ | progress 2 | 🚫 `no_entry_sign` | dropped |
124
+ | progress 3 | 👀 `eyes` | doing |
125
+ | progress 4 | ⏳ `hourglass_flowing_sand` | pending |
126
+ | progress 5 | *(marker only)* | undefined / untriaged |
127
+ | flag | ❗ `exclamation` | alert |
128
+ | flag | ❓ `question` | needs discussion |
129
+ | flag | 💬 `speech_balloon` | waiting — the other party in this conversation owes a reply |
130
+ | flag | 🔒 `lock` | blocked — stuck on something *outside* this conversation |
131
+
132
+ The invariant is **"at least one progress reaction, readers collapse by priority"** —
133
+ not "exactly one". A message carrying both ✅ and 👀 reads as *done*.
134
+
135
+ **Whose ball is it?** There are only three answers, and `pending` ⏳ already means *mine*.
136
+ The other two are the flags:
137
+
138
+ - 💬 **waiting** — I've done my part; someone **in this conversation** owes the next move
139
+ (I asked a question, I'm waiting on a review, I sent it and need a yes/no).
140
+ - 🔒 **blocked** — the hold-up is **outside this conversation**: another task, a third
141
+ party, an external dependency, a deploy window.
142
+
143
+ They stack with each other and with ❗/❓; `--state stuck` matches a task with any of them.
144
+
145
+ ```sh
146
+ # Two listings, split by whose reactions count — the distinction is the command,
147
+ # not a flag, so "everyone's list" and "my list" can't be confused at a glance.
148
+ slack todo ls # EVERYONE's tasks (has:)
149
+ slack mytodo ls # only tasks YOU reacted to (hasmy:)
150
+
151
+ slack todo ls --state untriaged # marked, no progress reaction yet
152
+ slack todo ls --state doing --in "#eng" # scoped to a channel
153
+ slack mytodo ls --state stuck # my unfinished tasks carrying a reason flag
154
+ slack todo ls --state stuck --from "@alice" # …and it was alice who wrote it
155
+ slack todo ls --from me # tasks on my own messages (any reactor)
156
+ slack todo ls --mine # same as `slack mytodo ls`
157
+ slack todo set "#eng:1700000000.000100" doing
158
+ slack todo flag "<permalink>" waiting # their ball now
159
+ slack todo flag "<permalink>" needs-discussion
160
+ slack todo flag "<permalink>" blocked --remove
161
+ slack todo doctor --in "#eng" # report messages in two progress states
162
+ slack todo doctor --in "#eng" --fix # keep the highest-priority one
163
+ ```
164
+
165
+ Notes:
166
+
167
+ - **`todo ls` vs `mytodo ls`** — `todo ls` matches anyone's reactions (`has:`), `mytodo ls`
168
+ only your own (`hasmy:`). Everything else about them is identical. Both print a
169
+ `From: @handle (Uxxxx) — Workspace` line first, because `hasmy:` resolves against
170
+ whichever account the token belongs to — "my tasks" means nothing until you know who
171
+ *my* is, and a stale profile otherwise lists someone else's list without saying so.
172
+ (`todo ls --mine` is a one-off shortcut to the narrow view.)
173
+ - Both are read-only and run **exactly one** `search.messages` call — priority is expressed
174
+ as negated `has:`/`hasmy:` terms in the query, not as multiple searches
175
+ (`search.messages` is Tier 2, ~20 req/min). Slack's search index lags a little
176
+ behind `reactions.add`, so a just-set task may take a moment to appear.
177
+ - `todo set` **adds the new reaction before removing the old ones**, and removes serially.
178
+ The reverse order would leave a window with no progress reaction at all — a crash or a
179
+ 429 there would drop the task out of every query with nothing left pointing at it.
180
+ Worst case here is a message with two progress reactions, which reads correctly and is
181
+ repairable with `todo doctor --fix`.
182
+ - `--from <@user|me>` maps to search's `from:` modifier (a missing `@` is supplied; `me`
183
+ passes through as-is). Reactions carry no reference to *whom* a task waits on, but the
184
+ sender is usually that person — so filtering by author answers most of the question for
185
+ free. It composes with `--in` in the same single search.
186
+ - **Not implemented, deliberately:** encoding the actual reference ("blocked on @alice" /
187
+ "blocked on task X") as a machine-readable token in a thread reply. Measured: Slack's
188
+ full-text index splits on punctuation, so a quoted search for `"blocked-on"` returns 0
189
+ hits, and `"1.91"` matches `1.91.0`. A token like `todo:v1 blocked-on=@alice` is therefore
190
+ unsearchable. Any future attempt needs a single alphanumeric marker word (e.g. `tdblock`)
191
+ plus a bare `@mention`.
192
+ - `todo doctor` pages history sequentially and scans at most `--limit` messages
193
+ (default 1000); if it hits the limit it says so rather than silently stopping.
194
+ - Emoji are configurable in `~/.config/slack-cli/todo.json` (same directory as
195
+ `profiles.json`); anything omitted falls back to the defaults above:
196
+
197
+ ```json
198
+ { "marker": "round_pushpin", "progress": { "doing": "construction" } }
199
+ ```
200
+
201
+ - Channel-name→ID and user-ID→handle lookups are cached in
202
+ `~/.config/slack-cli/cache.json` for 1 hour, namespaced by workspace (`team_id`) since
203
+ both are workspace-scoped; `--no-cache` bypasses it. Reaction state and search results
204
+ are **never** cached — they are exactly the values that change, and Slack's search index
205
+ already lags. A corrupt or unwritable cache is ignored and the command runs uncached.
206
+
97
207
  ### tail — real-time message stream
98
208
 
99
209
  `slack tail` polls a channel every 3 seconds (configurable via `--interval`) and
@@ -101,10 +211,10 @@ prints new messages as they arrive, in the same `[ts] @handle: text` format as
101
211
  the `read` command.
102
212
 
103
213
  ```sh
104
- slack tail "#symval" # follow new messages from now
105
- slack tail "#symval" --since=30m # backfill 30 minutes, then stream
106
- slack tail "#symval" --thread=1700000000.000100 # one thread only
107
- slack tail "#symval" --me # only messages mentioning you
214
+ slack tail "#general" # follow new messages from now
215
+ slack tail "#general" --since=30m # backfill 30 minutes, then stream
216
+ slack tail "#general" --thread=1700000000.000100 # one thread only
217
+ slack tail "#general" --me # only messages mentioning you
108
218
  ```
109
219
 
110
220
  For automation, `--exit-on-message` stops as soon as the first message from
@@ -113,7 +223,7 @@ For automation, `--exit-on-message` stops as soon as the first message from
113
223
  make a "wait for a reply, then act" primitive that won't hang:
114
224
 
115
225
  ```sh
116
- slack tail "@matsuda" --exit-on-message --timeout 30m --interval 15000
226
+ slack tail "@alice" --exit-on-message --timeout 30m --interval 15000
117
227
  ```
118
228
 
119
229
  **Note:** Cross-channel mention streaming (`--me` without a target) is not yet
@@ -186,6 +296,19 @@ delivery (3-second interval). Supports `--since=<duration>` for backfill,
186
296
  that mention you. Uses `conversations.history?oldest=<ts>` as a cursor so
187
297
  already-seen messages are never re-printed, even across reconnects.
188
298
 
299
+ ## Ecosystem
300
+
301
+ This CLI is the human-comms surface of the
302
+ [agent-yes](https://github.com/snomiao/agent-yes) fleet: agent-yes runs, watches and
303
+ delegates to AI coding agents, and `slack` is how they read and answer the humans they
304
+ work with. Neither depends on the other — they share a stance rather than a library:
305
+ file-based state over daemons, one command surface across two runtimes, gates on the
306
+ irreversible and warnings on the merely unwise, and no real operational data in a public
307
+ repo.
308
+
309
+ A write-up of the design decisions behind the confirm gate lives on the agent-yes lab:
310
+ [A confirm code that goes stale](https://lab.agent-yes.com/2026-08-11-slack-term-safe-writes).
311
+
189
312
  ## Related / prior art
190
313
 
191
314
  - [`slkcli`](https://www.npmjs.com/package/slkcli) by
package/SKILL.md CHANGED
@@ -8,11 +8,15 @@ description: "Terminal Slack CLI — read news/mentions/DMs/channel history, ful
8
8
  A lightweight Slack CLI for quick workspace interaction from the terminal.
9
9
  Two implementations share one command surface (parity-tested):
10
10
 
11
- - **TypeScript** (bun-first), published as `@snomiao/slack` on npm.
11
+ - **TypeScript** (bun-first), published as `slack-term` on npm.
12
12
  - **Rust** native binary, installable via `cargo install`.
13
13
 
14
14
  Binary name: `slack` (also aliased as `sl` / invoked via `sc sl ...` in some setups).
15
15
 
16
+ Part of the [agent-yes](https://github.com/snomiao/agent-yes) ecosystem — this is the CLI an
17
+ agent uses to talk to humans. Design notes:
18
+ [lab.agent-yes.com](https://lab.agent-yes.com/2026-08-11-slack-term-safe-writes).
19
+
16
20
  ## When to use
17
21
 
18
22
  - User wants to read Slack activity, mentions, DMs, or channel history without opening the Slack app.
@@ -25,10 +29,13 @@ Binary name: `slack` (also aliased as `sl` / invoked via `sc sl ...` in some set
25
29
  TypeScript (recommended — any platform with Node 18+):
26
30
 
27
31
  ```sh
28
- npm install -g @snomiao/slack
29
- # or: bun add -g @snomiao/slack / pnpm add -g @snomiao/slack
32
+ npm install -g slack-term
33
+ # or: bun add -g slack-term / pnpm add -g slack-term
30
34
  ```
31
35
 
36
+ > Previously published as `@snomiao/slack`; that name is deprecated on npm and
37
+ > no longer updated. Install `slack-term`.
38
+
32
39
  Rust (native binary):
33
40
 
34
41
  ```sh
@@ -55,6 +62,10 @@ slack search "deploy" --count 50
55
62
  # Read a specific channel or DM (quote #channel — unquoted # is a shell comment)
56
63
  slack read "#general"
57
64
  slack read @username
65
+ # Thread parents are marked `[+N replies]` — a marked line means there is more to
66
+ # read inside the thread (`slack thread "#general" <ts>`); top-level output alone
67
+ # never shows thread replies. --json carries reply_count/reply_users_count/latest_reply.
68
+ slack read "#general" --unreplied # only what's still awaiting your answer
58
69
 
59
70
  # Send a message (two-step confirm)
60
71
  slack send "#general" "Hello team"
@@ -74,6 +85,17 @@ slack delete "<permalink>"
74
85
  slack react "#general:1700000000.000100" white_check_mark
75
86
  slack react "<permalink>" eyes --remove # take it back
76
87
 
88
+ # Task tracking on reactions — 📌 marks a task, a second reaction holds progress
89
+ slack todo ls # EVERYONE's open tasks (has:)
90
+ slack mytodo ls # only tasks YOU reacted to (hasmy:)
91
+ slack todo ls --state untriaged --in "#general" # marked but no progress yet
92
+ slack mytodo ls --state stuck # my unfinished tasks + a reason flag
93
+ slack todo set "#general:1700000000.000100" doing # pending|doing|done|dropped
94
+ slack todo ls --state stuck --from "@alice" # …and alice wrote it
95
+ slack todo flag "<permalink>" waiting # alert|needs-discussion|waiting|blocked
96
+ # waiting = their ball, blocked = outside
97
+ slack todo doctor --in "#general" [--fix] # messages stuck in two states
98
+
77
99
  # Bulk export a channel's history
78
100
  slack dump "#channel-name"
79
101
 
@@ -85,11 +107,24 @@ slack download "https://acme.slack.com/files/U1/F0123ABC/invoice.pdf"
85
107
 
86
108
  Targets for `send` are `#channel`, `@user`, `#channel:<thread_ts>`, or a Slack URL —
87
109
  a message permalink replies in that message's thread; a channel-only URL posts top-level.
88
- The confirm preview prints the resolved destination (`→ ... — THREAD REPLY` or
89
- `→ ... — NEW top-level message`); verify it before re-running with `--code`.
110
+ The confirm preview prints who it will be sent **as** (`From: @handle (Uxxxx) — Workspace`,
111
+ plus `[as bot]` under `--as-bot`) and the resolved destination (`→ ... — THREAD REPLY` or
112
+ `→ ... — NEW top-level message`); verify both before re-running with `--code`.
113
+
114
+ **Every** gated write opens its preview with that same `From:` line — `send`, `edit`,
115
+ `delete`, `upload`, `schedule send`, `schedule rm`, `channel create`, `drafts edit`,
116
+ `drafts delete` — and the identity is part of the confirm hash: switching
117
+ profile/token between preview and confirm invalidates the code instead of acting as
118
+ someone else. It matters most on `schedule send`, where the message goes out later,
119
+ unattended, as whoever the token was at schedule time.
120
+
121
+ `schedule send` prints its fire time in both zones —
122
+ `At: 2026-08-10 18:00:00 GMT+9 (local) / 2026-08-10T09:00:00.000Z (Unix: 1786352400)` —
123
+ because `--at` accepts both bare local times (`"2026-08-10 09:00"`) and `Z`-suffixed
124
+ UTC, and the two are easy to mix up. Check the side you meant before confirming.
90
125
 
91
126
  `send` also runs a **warn-only untagged-mention lint**: if the body names a workspace
92
- member in plain text (honorifics like `松田さん` / `張老师`, or `Hi Dave`) without a
127
+ member in plain text (honorifics like `山田さん` / `王老师`, or `Hi Dave`) without a
93
128
  matching `<@USERID>` tag, it prints `⚠ possible untagged mention: …` alongside the
94
129
  confirm code. It never blocks — third-party/external references are expected to trip it.
95
130
  To actually notify someone, write `@handle` (auto-converted to `<@USERID>`; see below).
@@ -103,8 +138,57 @@ and no one has replied since — `⚠ 相手はまだ返信していません…
103
138
  on that message instead of piling on a new one. Reply-status based, not time-based; opt
104
139
  out with `SLACK_UNREPLIED_WARN=0`.
105
140
 
141
+ ### todo — tasks as reactions
142
+
143
+ A task is any message carrying the marker reaction 📌 `:pushpin:`. Progress lives in a
144
+ second reaction, ranked by priority — **1** ✅ `white_check_mark` done, **2** 🚫
145
+ `no_entry_sign` dropped, **3** 👀 `eyes` doing, **4** ⏳ `hourglass_flowing_sand` pending, **5** marker
146
+ only = undefined. A message may carry more than one progress reaction — readers collapse
147
+ to the highest priority, so ✅+👀 reads as *done*.
148
+
149
+ Reason flags stack independently of progress and of each other: ❗ `exclamation` alert,
150
+ ❓ `question` needs-discussion, and two that say **whose ball it is** —
151
+
152
+ - 💬 `speech_balloon` **waiting** — someone **in this conversation** owes the next move
153
+ (asked a question, waiting on a review/approval). Their ball.
154
+ - 🔒 `lock` **blocked** — stuck on something **outside** it: another task, a third party,
155
+ an external dependency. Nobody here can unstick it by replying.
156
+
157
+ (⏳ `hourglass_flowing_sand` pending already means "my ball", which is why only these two are needed.)
158
+
159
+ - **`todo ls` = anyone's reactions (`has:`), `mytodo ls` = only yours (`hasmy:`).** Two
160
+ commands rather than a flag, so the two lists can't be mistaken for each other. Both
161
+ print `From: @handle (Uxxxx) — Workspace` first: `hasmy:` resolves against whoever the
162
+ token is, so "my tasks" is only meaningful once you know who *my* is. `todo ls --mine`
163
+ is a shortcut to the narrow view; `--shared` is accepted but is now the default.
164
+ - Both are read-only and issue **one** `search.messages` call; priority is encoded as
165
+ negated `has:`/`hasmy:` terms.
166
+ `search.messages` returns no reaction data, so this filtering can only happen in the
167
+ query. The search index lags slightly behind `reactions.add`.
168
+ - `todo set` adds the target reaction **first**, then removes the stale ones one at a time.
169
+ Never remove-then-add: an interruption in between leaves the task with no progress
170
+ reaction and it vanishes from every query. Two reactions is the safe failure mode, and
171
+ `todo doctor --fix` repairs it (also add-first).
172
+ - `todo ls --from <@user|me>` adds search's `from:` modifier (missing `@` supplied, `me`
173
+ left bare) and composes with `--in` in the same single search. Reactions can't point at
174
+ *who* a task waits on, but the sender usually is that person.
175
+ - Deliberately NOT implemented: a machine-readable token in a thread reply to carry the
176
+ actual reference. Measured — Slack's index splits on punctuation, so quoted `"blocked-on"`
177
+ gets 0 hits and `"1.91"` matches `1.91.0`; `todo:v1 blocked-on=@alice` is unsearchable.
178
+ A future attempt needs one alphanumeric marker word (e.g. `tdblock`) + a bare `@mention`.
179
+ - `todo doctor` scans at most `--limit` messages (default 1000), pages sequentially, and
180
+ explicitly reports when it hits the limit.
181
+ - Emoji overrides: `~/.config/slack-cli/todo.json`, e.g.
182
+ `{ "marker": "round_pushpin", "progress": { "doing": "construction" } }`.
183
+ - Channel/user lookups are cached for 1h in `~/.config/slack-cli/cache.json`, keyed by
184
+ workspace `team_id` (`--no-cache` to bypass). Reaction state and search results are never
185
+ cached; a broken cache is ignored. Message listings read this cache too, behind their
186
+ in-process map.
187
+ - Rate limits: `todo` commands catch Slack's 429, wait `Retry-After`, and retry up to 3
188
+ times before failing loudly.
189
+
106
190
  **Etiquette:** prefer a `react` over a reply for simple acks (了解 → `eyes`, 完了 →
107
- `white_check_mark`, 処理中 → `hourglass`) so threads stay short; read the thread with
191
+ `white_check_mark`, 処理中 → `hourglass_flowing_sand`) so threads stay short; read the thread with
108
192
  `slack thread`/`slack read` before replying to avoid duplicates; consolidate multiple
109
193
  points into one message.
110
194
 
@@ -177,7 +261,7 @@ slack news --limit 1
177
261
  - **`xoxc-` desktop session token** — accepted by the public Slack API (send/edit/delete/react/upload/channel create+invite included) as long as its `xoxd` session cookie is attached (`slack auth chrome`/`slack auth firefox`, or `SLACK_COOKIE=...`). Without the cookie it still fails with a clear "needs its session cookie" error.
178
262
  - **`conversations.invite` reports success but the member never shows up** — the invited user is likely a single-channel guest (`is_ultra_restricted`); Slack silently no-ops the invite since that account type can only ever belong to the one channel it was created in. `channel create --invite` now warns about this up front.
179
263
  - **Send is rejected with "use #channel or @user"** — the CLI enforces human-readable targets. Use `#channel-name` or `@display-name`, not raw IDs.
180
- - **Confirm code mismatch on `send`** — the message text or destination changed between preview and confirm. Re-run without `--code` for a fresh preview, and re-check the `→` destination line (THREAD REPLY vs NEW top-level message).
264
+ - **Confirm code mismatch on any gated write** — the content, target or acting identity changed between preview and confirm (a different profile/`SLACK_TOKEN`, or `--as-bot` added or dropped). Re-run without `--code` for a fresh preview, and re-check the `From:` line and the `→` destination line (THREAD REPLY vs NEW top-level message).
181
265
  - **Enterprise Grid / admin-locked workspace** — custom app installation may need admin approval or be disabled outright.
182
266
 
183
267
  ## Safety