slack-term 1.42.0 → 1.43.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
@@ -380,6 +380,82 @@ slack tail "@alice" --exit-on-message --timeout 30m --interval 15000
380
380
  **Note:** Cross-channel mention streaming (`--me` without a target) is not yet
381
381
  supported — a target channel is required.
382
382
 
383
+ ### stream — every matching message, across all channels
384
+
385
+ `slack stream --grep <regex>` watches **every** conversation the identity is in —
386
+ new top-level posts *and* thread replies — and prints each message whose text
387
+ matches. It is the cross-channel counterpart of `tail`, built for a long-running
388
+ consumer (e.g. "wake an agent whenever someone @mentions the bot"):
389
+
390
+ ```sh
391
+ slack stream --grep '<@U00000001>|@mybot' --json # runs until stopped
392
+ slack stream --grep 'deploy' -i --channel '#dev' --once # one scan, then exit
393
+ slack stream --grep '<@U00000001>' --since 2h --json # replay the last 2 hours
394
+ slack stream --grep '<@U00000001>' --replies-to self --json # + every reply in threads the bot is in
395
+ ```
396
+
397
+ - **One JSON line per match** with `--json`:
398
+ `{type: "message"|"reply", channel: {id, name}, ts, thread_ts, user: {id, name}, text, permalink, match}`,
399
+ where `match` is `"grep"` or `"replies-to"`.
400
+ `--grep` runs on the raw text (mentions look like `<@U…>`), plus legacy attachment
401
+ text; the emitted `text` has `&lt; &gt; &amp;` decoded.
402
+ - **Privacy-safe defaults.** With a bot token (`SLACK_BOT_TOKEN`) it streams **as the
403
+ bot**, so it sees only channels the bot was invited to — never the user's DMs.
404
+ `--as-user` opts into the user identity. Matching happens before anything is
405
+ printed: a non-matching message is never printed, logged, or even name-resolved.
406
+ - **Replies in a user's threads.** `--replies-to <user-id>` (repeatable; `self` = the
407
+ streaming identity) also prints every reply in a thread that user started or has
408
+ replied in, whether or not it matches `--grep` — people answer a bot in its thread
409
+ without tagging it. It matches Slack's `parent_user_id`, the parent's
410
+ `reply_users`, and the id's own replies the stream has seen (kept in the state
411
+ file for 30 days). `reply_users` lists at most five people, so a thread cut short
412
+ there is read once in full (as is one whose list is missing). A bot can appear
413
+ as its user id (`U…`) or bot id (`B…`); `self`, or a `B…` id, covers both. Polling only reaches threads inside `--thread-window`;
414
+ with the relay, a reply in an older thread is caught too.
415
+ - **No self-echo.** Posts by the streaming identity are excluded by sender id (its user
416
+ id and bot id; with `--as-user`, the bot's posts too) — the same text from anyone
417
+ else still matches.
418
+ - **Resumable.** A per-channel cursor (plus one per active thread) is saved under
419
+ `$XDG_STATE_HOME/slack-term/stream/` (one file per identity + grep + channels + `--replies-to`;
420
+ override with `--state`). A restart continues where it stopped: nothing lost,
421
+ nothing repeated. The first run starts *now*; `--since` replays from a point,
422
+ ignoring the saved cursor. Delivery is at-least-once only in one corner — a crash
423
+ between printing a match and saving the cursor repeats that line, so dedupe on
424
+ `channel.id + ts`. A lock file keeps two streams off the same state.
425
+ - **Transport: the relay is the primary mode; polling is the fallback.** Deploy
426
+ [`worker/`](worker/) (a Cloudflare Worker on Slack's Events API — setup in
427
+ [worker/README.md](worker/README.md)) and set `SLACK_RELAY_URL` and
428
+ `SLACK_RELAY_TOKEN`: the stream then hears a *doorbell* — `{channel, ts, thread_ts}`,
429
+ no text — within a second of each post, reads that one message through the Web API
430
+ with its own token and runs it through the same `--grep`. While the relay is
431
+ connected the poll still runs, every `--reconcile` (default `5m`), as the safety net;
432
+ when the relay is down it goes back to `--interval`. Both paths share one record of
433
+ what was printed, so a message is never printed twice. The relay is used only when
434
+ streaming as the bot (it rings for the bot's channels) and never with `--once`.
435
+ - **Polling fallback** (no relay configured, `--as-user`, or `--no-relay`). The stream
436
+ prints a `WARNING: polling mode` line on stderr, pointing to the relay setup. It
437
+ polls every `--interval` (default `45s`), paced to stay inside Slack's rate limits:
438
+ - every cycle reads each channel's history **from its cursor only** (new posts);
439
+ - each channel's whole `--thread-window` (default `3d`) — which shows which threads
440
+ gained replies, and only those threads are read — is re-read once per
441
+ `--thread-scan` (default `3m`), spread across cycles rather than all at once, so a
442
+ reply under an older parent can take up to `--thread-scan` to appear;
443
+ - a channel with no post inside the window skips that re-read, and such an idle
444
+ DM/MPIM (`--as-user`) is not read at all between `--thread-scan` periods;
445
+ - all `conversations.history` / `conversations.replies` calls share one token
446
+ bucket, `--calls-per-min` (default `40`, under Tier 3's ~50/min; 10 at once).
447
+ A message is held back until it is 5 s old, so a cursor never skips one Slack has
448
+ not made visible yet. Rate limits honour `Retry-After`.
449
+ - **Not covered:** edits (a message edited *into* matching is not re-emitted), and —
450
+ without the relay — replies in a thread whose parent is older than `--thread-window`.
451
+ - **Failures are loud and distinguishable.** A network/API error retries the cycle
452
+ with φ backoff (`2s·1.618ⁿ`, capped at 5 min), one stderr line per attempt; after
453
+ 12 consecutive failures (≈16 min) it exits. A channel it cannot read is skipped with
454
+ one stderr line; if *every* channel fails, that is a failure, not "no matches".
455
+ - **Exit codes** (same contract as `ask`): `0` matches printed (`--once`) or stopped by
456
+ SIGINT/SIGTERM; `2` `--once` found nothing; `3` transport/auth failure (stderr says
457
+ which); `1` bad arguments (e.g. an invalid regex).
458
+
383
459
  ## Configuration
384
460
 
385
461
  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