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 +81 -0
- package/SKILL.md +22 -2
- package/dist/cli.js +223 -209
- package/package.json +2 -1
- package/ts/ask.ts +61 -4
- package/ts/cli.ts +336 -20
- package/ts/newlines.ts +158 -0
- package/ts/relay.ts +105 -0
- package/ts/slack.ts +52 -4
- package/ts/stream.ts +1048 -0
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 `< > &` 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
|
|
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
|
|