slack-term 1.42.0 → 1.44.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
@@ -125,6 +125,11 @@ slack react "<permalink>" eyes --remove # take a reaction back
125
125
  # one tap on an existing pill, no emoji picker. Same two-step confirm gate as send.
126
126
  # A ❓ "その他 (other)" pill always follows the choices: pressing it means "none of
127
127
  # these" and the answer comes as a reply (exit 5), not as the ❓ itself.
128
+ # --explain (or SLACK_TERM_ASK_EXPLAIN=1) adds an ℹ️ "説明して (explain)" pill after ❓:
129
+ # pressing it asks YOU to explain in the thread and is never an answer — the question
130
+ # stays open. --wait/--waitFor say so on stderr and may post ONE thread note (once per
131
+ # question) saying an explanation is coming; --explain-exit makes that exit 7.
132
+ # --pending only reports a ℹ️ press — it posts nothing.
128
133
  # The instructions are posted in ja or en: --lang / SLACK_TERM_LANG, else the
129
134
  # answerers' Slack locale, else the question's own language, else $LANG, else ja.
130
135
  slack ask "@bob" "本番に出してよい?" "出してよい" "待って"
@@ -380,6 +385,82 @@ slack tail "@alice" --exit-on-message --timeout 30m --interval 15000
380
385
  **Note:** Cross-channel mention streaming (`--me` without a target) is not yet
381
386
  supported — a target channel is required.
382
387
 
388
+ ### stream — every matching message, across all channels
389
+
390
+ `slack stream --grep <regex>` watches **every** conversation the identity is in —
391
+ new top-level posts *and* thread replies — and prints each message whose text
392
+ matches. It is the cross-channel counterpart of `tail`, built for a long-running
393
+ consumer (e.g. "wake an agent whenever someone @mentions the bot"):
394
+
395
+ ```sh
396
+ slack stream --grep '<@U00000001>|@mybot' --json # runs until stopped
397
+ slack stream --grep 'deploy' -i --channel '#dev' --once # one scan, then exit
398
+ slack stream --grep '<@U00000001>' --since 2h --json # replay the last 2 hours
399
+ slack stream --grep '<@U00000001>' --replies-to self --json # + every reply in threads the bot is in
400
+ ```
401
+
402
+ - **One JSON line per match** with `--json`:
403
+ `{type: "message"|"reply", channel: {id, name}, ts, thread_ts, user: {id, name}, text, permalink, match}`,
404
+ where `match` is `"grep"` or `"replies-to"`.
405
+ `--grep` runs on the raw text (mentions look like `<@U…>`), plus legacy attachment
406
+ text; the emitted `text` has `&lt; &gt; &amp;` decoded.
407
+ - **Privacy-safe defaults.** With a bot token (`SLACK_BOT_TOKEN`) it streams **as the
408
+ bot**, so it sees only channels the bot was invited to — never the user's DMs.
409
+ `--as-user` opts into the user identity. Matching happens before anything is
410
+ printed: a non-matching message is never printed, logged, or even name-resolved.
411
+ - **Replies in a user's threads.** `--replies-to <user-id>` (repeatable; `self` = the
412
+ streaming identity) also prints every reply in a thread that user started or has
413
+ replied in, whether or not it matches `--grep` — people answer a bot in its thread
414
+ without tagging it. It matches Slack's `parent_user_id`, the parent's
415
+ `reply_users`, and the id's own replies the stream has seen (kept in the state
416
+ file for 30 days). `reply_users` lists at most five people, so a thread cut short
417
+ there is read once in full (as is one whose list is missing). A bot can appear
418
+ as its user id (`U…`) or bot id (`B…`); `self`, or a `B…` id, covers both. Polling only reaches threads inside `--thread-window`;
419
+ with the relay, a reply in an older thread is caught too.
420
+ - **No self-echo.** Posts by the streaming identity are excluded by sender id (its user
421
+ id and bot id; with `--as-user`, the bot's posts too) — the same text from anyone
422
+ else still matches.
423
+ - **Resumable.** A per-channel cursor (plus one per active thread) is saved under
424
+ `$XDG_STATE_HOME/slack-term/stream/` (one file per identity + grep + channels + `--replies-to`;
425
+ override with `--state`). A restart continues where it stopped: nothing lost,
426
+ nothing repeated. The first run starts *now*; `--since` replays from a point,
427
+ ignoring the saved cursor. Delivery is at-least-once only in one corner — a crash
428
+ between printing a match and saving the cursor repeats that line, so dedupe on
429
+ `channel.id + ts`. A lock file keeps two streams off the same state.
430
+ - **Transport: the relay is the primary mode; polling is the fallback.** Deploy
431
+ [`worker/`](worker/) (a Cloudflare Worker on Slack's Events API — setup in
432
+ [worker/README.md](worker/README.md)) and set `SLACK_RELAY_URL` and
433
+ `SLACK_RELAY_TOKEN`: the stream then hears a *doorbell* — `{channel, ts, thread_ts}`,
434
+ no text — within a second of each post, reads that one message through the Web API
435
+ with its own token and runs it through the same `--grep`. While the relay is
436
+ connected the poll still runs, every `--reconcile` (default `5m`), as the safety net;
437
+ when the relay is down it goes back to `--interval`. Both paths share one record of
438
+ what was printed, so a message is never printed twice. The relay is used only when
439
+ streaming as the bot (it rings for the bot's channels) and never with `--once`.
440
+ - **Polling fallback** (no relay configured, `--as-user`, or `--no-relay`). The stream
441
+ prints a `WARNING: polling mode` line on stderr, pointing to the relay setup. It
442
+ polls every `--interval` (default `45s`), paced to stay inside Slack's rate limits:
443
+ - every cycle reads each channel's history **from its cursor only** (new posts);
444
+ - each channel's whole `--thread-window` (default `3d`) — which shows which threads
445
+ gained replies, and only those threads are read — is re-read once per
446
+ `--thread-scan` (default `3m`), spread across cycles rather than all at once, so a
447
+ reply under an older parent can take up to `--thread-scan` to appear;
448
+ - a channel with no post inside the window skips that re-read, and such an idle
449
+ DM/MPIM (`--as-user`) is not read at all between `--thread-scan` periods;
450
+ - all `conversations.history` / `conversations.replies` calls share one token
451
+ bucket, `--calls-per-min` (default `40`, under Tier 3's ~50/min; 10 at once).
452
+ A message is held back until it is 5 s old, so a cursor never skips one Slack has
453
+ not made visible yet. Rate limits honour `Retry-After`.
454
+ - **Not covered:** edits (a message edited *into* matching is not re-emitted), and —
455
+ without the relay — replies in a thread whose parent is older than `--thread-window`.
456
+ - **Failures are loud and distinguishable.** A network/API error retries the cycle
457
+ with φ backoff (`2s·1.618ⁿ`, capped at 5 min), one stderr line per attempt; after
458
+ 12 consecutive failures (≈16 min) it exits. A channel it cannot read is skipped with
459
+ one stderr line; if *every* channel fails, that is a failure, not "no matches".
460
+ - **Exit codes** (same contract as `ask`): `0` matches printed (`--once`) or stopped by
461
+ SIGINT/SIGTERM; `2` `--once` found nothing; `3` transport/auth failure (stderr says
462
+ which); `1` bad arguments (e.g. an invalid regex).
463
+
383
464
  ## Configuration
384
465
 
385
466
  Requires a Slack user token (`xoxp-...`) with the following scopes:
package/SKILL.md CHANGED
@@ -52,6 +52,15 @@ slack news --limit 5
52
52
  # Recent messages across joined channels
53
53
  slack msgs
54
54
 
55
+ # Every new message matching a regex, across all channels + threads (long-running;
56
+ # resumable cursor; as the bot when SLACK_BOT_TOKEN is set). Exit 0 match / 2 none / 3 failure.
57
+ # Real time = the relay (primary mode): deploy worker/ (see worker/README.md) and set
58
+ # SLACK_RELAY_URL + SLACK_RELAY_TOKEN. Without it the stream falls back to POLLING and
59
+ # warns on stderr ("WARNING: polling mode") — a fallback only: paced (--calls-per-min 40),
60
+ # cursor-only reads each --interval (45s), full --thread-window re-read every --thread-scan (3m).
61
+ slack stream --grep '<@U00000001>|@mybot' --json
62
+ slack stream --grep 'deploy' -i --once
63
+
55
64
  # Full-text search
56
65
  slack search "deploy"
57
66
  slack search "deploy" --count 50
@@ -169,6 +178,16 @@ without losing clicks. A reaction is durable state anyone can read back later.
169
178
  (`1️⃣ … 2️⃣ … 3️⃣ … ❓ その他 — スレッドで返信`) and seeds it after the keycaps. Pressing it
170
179
  is not an answer — `--wait` notes it on stderr and keeps waiting for the reply, which
171
180
  arrives as free text (exit `5`).
181
+ - **ℹ️ is the "explain first" pill** — offered with `--explain` (or
182
+ `SLACK_TERM_ASK_EXPLAIN=1`; `--no-explain` overrides), listed under ❓ and seeded after
183
+ it. ❓ means "none fit, my answer follows"; ℹ️ means "I can't answer yet — explain".
184
+ It **never answers or closes** the question: no ✅, the pills stay. `--wait`/`--waitFor`
185
+ print who pressed it on stderr (`--json`: `explain: [user ids]`) and may post ONE thread
186
+ note per question saying an explanation is coming (the thread is the record — re-presses
187
+ and re-runs post nothing more). Then explain in the thread and keep collecting.
188
+ `--explain-exit` stops with exit `7` (stdout empty; `--json` `status: "explain"`) on the
189
+ run that posts that note, so a `--timeout 0` poller hears about it once. Only the
190
+ question's audience counts; bots and the asker are ignored.
172
191
  - **The instructions are posted in `ja` or `en`.** `--lang` (or `SLACK_TERM_LANG`) decides;
173
192
  otherwise the answerers' Slack locale when they all share one, else the language the
174
193
  question is written in, else the system locale, else `ja`. The confirm gate prints the
@@ -178,7 +197,7 @@ without losing clicks. A reaction is durable state anyone can read back later.
178
197
  - **Exit codes** are the contract: `0` answered (the answer alone on stdout), `2` nobody
179
198
  replied, `3` transport/config failure, `4` a reply matched several choices (stdout
180
199
  empty), `6` the question was **voided** (stop waiting), `5` a **free-text reply** that picked none of the offered choices — the reply
181
- text is on stdout. Everything human-facing goes to stderr, so
200
+ text is on stdout. `7` (only with `--explain-exit`) — ℹ️ pressed, explain and collect again. Everything human-facing goes to stderr, so
182
201
  `ANS=$(slack ask … --wait)` is safe.
183
202
  - **A reply that chooses nothing is delivered, but is not a decision.** When choices were
184
203
  offered, only a reaction, the choice text, or its number is a decision (`0`). A reply
@@ -245,7 +264,8 @@ an answer can be pressed and nobody ever hears about it.
245
264
  collected, and lists the ones somebody answered — grouped by the asking session, with
246
265
  the agent pid and whether it is still alive (pid running, started before the ask, same
247
266
  CLI), and per item the exact `collect:` line to run (cursor included). It is read-only —
248
- nothing is posted, edited, stamped or relayed. Collecting is that `slack ask --waitFor=…`
267
+ nothing is posted, edited, stamped or relayed (an ℹ️ explain request is reported on
268
+ stderr, but its thread note is left to `--wait`/`--waitFor`). Collecting is that `slack ask --waitFor=…`
249
269
  line: an answer it hands over (exit 0) takes the ask off the list; a free-text reply it
250
270
  hands over (exit 5) is not reported again, but a pill pressed after it is.
251
271