slack-term 1.48.0 → 1.49.1

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.
@@ -0,0 +1,32 @@
1
+ {
2
+ "$comment": "slack-term language pack. Strings slack ask posts into Slack, and the patterns the choice-question hint looks for. Loaded at call time: edit and rerun, no rebuild. A .slack-term/locales/<lang>.json overrides keys here. ask.* marked WIRE are matched back exactly on questions already posted: never reword them.",
3
+ "lang": "en",
4
+ "locale_prefixes": [
5
+ "en"
6
+ ],
7
+ "ask": {
8
+ "instructionReactionThread": "_Press one of the reactions below to answer. If none fits, reply in this message's *thread* (ordinary channel posts are not picked up as answers)._",
9
+ "instructionReactionHere": "_Press one of the reactions below to answer. If none fits, reply to this message._",
10
+ "instructionTextThread": "_Reply in this message's *thread* — your reply is the answer (ordinary channel posts are not picked up as answers)._",
11
+ "instructionTextHere": "_Reply to this message — your reply is the answer._",
12
+ "overflowNote": "_Choices 11 and up have no reaction — answer them with a reply._",
13
+ "otherThread": "Other — reply in the thread",
14
+ "otherHere": "Other — reply to this message",
15
+ "explainLine (WIRE)": "Explain — press to ask the asker for more detail (not an answer)",
16
+ "explainNote (WIRE)": "Explanation requested — it will follow in this thread. The question stays open; answer it after that.",
17
+ "invalidPrefix (WIRE)": "_:warning: Several choices are selected, so this is not counted as an answer — keep only one: ",
18
+ "answeredVia": "_Answered by {how}{who}_",
19
+ "howReaction": "reaction {glyph}",
20
+ "howReply": "reply",
21
+ "howReplyN": "reply ({n})",
22
+ "voidStamp": "_Void{reason} — this question no longer takes answers._",
23
+ "supersededBy": "↪ Replaced by: {link}"
24
+ },
25
+ "choice_patterns": [
26
+ "\\((?:\\s*(?:yes|no)\\s*/){1,}\\s*(?:yes|no)\\s*\\)",
27
+ "\\(\\s*y\\s*/\\s*n\\s*\\)",
28
+ "\\(\\s*(?:true|false)\\s*/\\s*(?:true|false)\\s*\\)",
29
+ "(?:^|\\s)\\(?a\\)\\s.*\\(?b\\)\\s.*\\?\\s*$",
30
+ "(?:^|\\s)(?:option|choice)\\s*a\\b.*\\b(?:option|choice)?\\s*b\\b.*\\?\\s*$"
31
+ ]
32
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$comment": "slack-term language pack. Strings slack ask posts into Slack, and the patterns the choice-question hint looks for. Loaded at call time: edit and rerun, no rebuild. A .slack-term/locales/<lang>.json overrides keys here. ask.* marked WIRE are matched back exactly on questions already posted: never reword them.",
3
+ "lang": "ja",
4
+ "locale_prefixes": [
5
+ "ja"
6
+ ],
7
+ "ask": {
8
+ "instructionReactionThread": "_下のリアクションを 1 つ押すと回答になります。当てはまるものがなければ、このメッセージの *スレッド* で返信してください (チャンネルへの通常投稿は回答として拾いません)。_",
9
+ "instructionReactionHere": "_下のリアクションを 1 つ押すと回答になります。当てはまるものがなければ、このメッセージに返信してください。_",
10
+ "instructionTextThread": "_このメッセージの *スレッド* で返信してください。本文がそのまま回答になります (チャンネルへの通常投稿は回答として拾いません)。_",
11
+ "instructionTextHere": "_このメッセージに返信してください。本文がそのまま回答になります。_",
12
+ "overflowNote": "_11 番目以降はリアクションがないので、返信で答えてください。_",
13
+ "otherThread": "その他 — スレッドで返信してください",
14
+ "otherHere": "その他 — このメッセージに返信してください",
15
+ "explainLine (WIRE)": "説明して — 押すと質問者に説明を頼みます (回答にはなりません)",
16
+ "explainNote (WIRE)": "説明のリクエストを受け付けました — このスレッドで説明します。質問はまだ開いているので、回答はそのあとで大丈夫です。",
17
+ "invalidPrefix (WIRE)": "_:warning: 同時に複数選ばれているため回答として数えていません — どれか 1 つだけ残してください: ",
18
+ "answeredVia": "_{how}で回答済み{who}_",
19
+ "howReaction": "リアクション {glyph}",
20
+ "howReply": "返信",
21
+ "howReplyN": "返信 ({n})",
22
+ "voidStamp": "_無効{reason} — この質問への回答は受け付けません。_",
23
+ "supersededBy": "↪ 新しい質問: {link}"
24
+ },
25
+ "choice_patterns": [
26
+ "[((]\\s*(?:是|はい|する|あり|可)\\s*[//・|]\\s*(?:否|いいえ|しない|なし|不可)\\s*[))]",
27
+ "「[^」\\n]+?[//][^」\\n]+」.*[??]",
28
+ "(?:^|\\s)[((]?[aaAA11①][))]\\s.*[((]?[bbBB22②][))]\\s.*[??]\\s*$",
29
+ "(?:どちら|どれ)に(?:します|しましょう)か[??]?"
30
+ ]
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.48.0",
3
+ "version": "1.49.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
@@ -35,6 +35,7 @@
35
35
  },
36
36
  "files": [
37
37
  "ts/",
38
+ "locales/",
38
39
  "dist/",
39
40
  "worker/src/",
40
41
  "README.md",
package/ts/ask.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { askString } from "./locales.ts";
2
+
1
3
  // The `ask` message format — builder and parser in one place, on purpose.
2
4
  //
3
5
  // `ask` keeps no local record of a question, so `--waitFor` recovers everything
@@ -47,7 +49,7 @@ export const ASK_MARKER_PREFIX = `:${ASK_MARKER}: `;
47
49
 
48
50
  /** The "other" pill — "none of these fit, my answer follows as a reply" —
49
51
  * seeded after the numbered pills and listed under them in the body. ➕, not
50
- * ❓ (taku 2026-10-14): ❓ read as "I have a question", not "add my own answer".
52
+ * ❓ (2026-10-14): ❓ read as "I have a question", not "add my own answer".
51
53
  *
52
54
  * ❓ stays the HEAD marker (`ASK_MARKER_PREFIX` on the first line): that is the
53
55
  * wire format that identifies an ask, and every reader keys on it. Only the
@@ -101,8 +103,8 @@ export type AskLangSource = "flag" | "env" | "readers" | "content" | "system" |
101
103
  * in a different language from the question they sit under read as noise.
102
104
  * 4. The system locale (`LC_ALL` → `LC_MESSAGES` → `LANG`), for content with no
103
105
  * letters at all (emoji, numbers).
104
- * 5. `ja`, the copy `ask` has always posted — so a machine nobody configured,
105
- * asking someone whose locale cannot be read, keeps doing what it did. */
106
+ * 5. `en` (11-02: English is the default; the readers' language, then
107
+ * the content, win over it). */
106
108
  export function askResolveLang(
107
109
  opts: { flag?: string | undefined; readerLocales?: (string | undefined)[]; content?: string[] },
108
110
  env: NodeJS.ProcessEnv = process.env,
@@ -118,7 +120,7 @@ export function askResolveLang(
118
120
  if (fromContent) return { lang: fromContent, source: "content" };
119
121
  const sys = askLangOfLocale(env.LC_ALL || env.LC_MESSAGES || env.LANG);
120
122
  if (sys) return { lang: sys, source: "system" };
121
- return { lang: "ja", source: "default" };
123
+ return { lang: "en", source: "default" };
122
124
  }
123
125
 
124
126
  /** `ja-JP` (Slack), `ja_JP.UTF-8` (POSIX), `en` → the language we have copy
@@ -178,50 +180,34 @@ interface AskCopy {
178
180
  *
179
181
  * The invalid-ballot prefix is shared with `poll`, which posts it in Japanese. */
180
182
  export const ASK_COPY: Record<AskLang, AskCopy> = {
181
- ja: {
182
- instructionReactionThread:
183
- "_下のリアクションを 1 つ押すと回答になります。当てはまるものがなければ、このメッセージの *スレッド* で返信してください (チャンネルへの通常投稿は回答として拾いません)。_",
184
- instructionReactionHere:
185
- "_下のリアクションを 1 つ押すと回答になります。当てはまるものがなければ、このメッセージに返信してください。_",
186
- instructionTextThread:
187
- "_このメッセージの *スレッド* で返信してください。本文がそのまま回答になります (チャンネルへの通常投稿は回答として拾いません)。_",
188
- instructionTextHere: "_このメッセージに返信してください。本文がそのまま回答になります。_",
189
- overflowNote: "_11 番目以降はリアクションがないので、返信で答えてください。_",
190
- otherThread: `${ASK_OTHER_PREFIX}その他 — スレッドで返信してください`,
191
- otherHere: `${ASK_OTHER_PREFIX}その他 — このメッセージに返信してください`,
192
- explainLine: `${ASK_EXPLAIN_PREFIX}説明して — 押すと質問者に説明を頼みます (回答にはなりません)`,
193
- explainNote: `${ASK_EXPLAIN_PREFIX}説明のリクエストを受け付けました — このスレッドで説明します。質問はまだ開いているので、回答はそのあとで大丈夫です。`,
194
- invalidPrefix: "_:warning: 同時に複数選ばれているため回答として数えていません — どれか 1 つだけ残してください: ",
195
- answeredVia: (how, who) => `_${how}で回答済み${who ? ` (${who})` : ""}_`,
196
- howReaction: (glyph) => `リアクション ${glyph}`,
197
- howReply: "返信",
198
- howReplyN: (n) => `返信 (${n})`,
199
- voidStamp: (reason) => `_無効${reason ? `: ${reason}` : ""} — この質問への回答は受け付けません。_`,
200
- supersededBy: (link) => `↪ 新しい質問: ${link}`,
201
- },
202
- en: {
203
- instructionReactionThread:
204
- "_Press one of the reactions below to answer. If none fits, reply in this message's *thread* (ordinary channel posts are not picked up as answers)._",
205
- instructionReactionHere:
206
- "_Press one of the reactions below to answer. If none fits, reply to this message._",
207
- instructionTextThread:
208
- "_Reply in this message's *thread* — your reply is the answer (ordinary channel posts are not picked up as answers)._",
209
- instructionTextHere: "_Reply to this message — your reply is the answer._",
210
- overflowNote: "_Choices 11 and up have no reaction — answer them with a reply._",
211
- otherThread: `${ASK_OTHER_PREFIX}Other — reply in the thread`,
212
- otherHere: `${ASK_OTHER_PREFIX}Other — reply to this message`,
213
- explainLine: `${ASK_EXPLAIN_PREFIX}Explain — press to ask the asker for more detail (not an answer)`,
214
- explainNote: `${ASK_EXPLAIN_PREFIX}Explanation requested — it will follow in this thread. The question stays open; answer it after that.`,
215
- invalidPrefix: "_:warning: Several choices are selected, so this is not counted as an answer — keep only one: ",
216
- answeredVia: (how, who) => `_Answered by ${how}${who ? ` (${who})` : ""}_`,
217
- howReaction: (glyph) => `reaction ${glyph}`,
218
- howReply: "reply",
219
- howReplyN: (n) => `reply (${n})`,
220
- voidStamp: (reason) => `_Void${reason ? `: ${reason}` : ""} — this question no longer takes answers._`,
221
- supersededBy: (link) => `↪ Replaced by: ${link}`,
222
- },
183
+ get ja() { return askCopyOf("ja"); },
184
+ get en() { return askCopyOf("en"); },
223
185
  };
224
186
 
187
+ /** One language's copy, from its pack (locales/<lang>.json) at access time —
188
+ * the words live there, the structure (markers, which piece goes where) here. */
189
+ function askCopyOf(lang: AskLang): AskCopy {
190
+ const s = (key: string, vars?: Record<string, string | number>): string => askString(lang, key, vars);
191
+ return {
192
+ instructionReactionThread: s("instructionReactionThread"),
193
+ instructionReactionHere: s("instructionReactionHere"),
194
+ instructionTextThread: s("instructionTextThread"),
195
+ instructionTextHere: s("instructionTextHere"),
196
+ overflowNote: s("overflowNote"),
197
+ otherThread: `${ASK_OTHER_PREFIX}${s("otherThread")}`,
198
+ otherHere: `${ASK_OTHER_PREFIX}${s("otherHere")}`,
199
+ explainLine: `${ASK_EXPLAIN_PREFIX}${s("explainLine")}`,
200
+ explainNote: `${ASK_EXPLAIN_PREFIX}${s("explainNote")}`,
201
+ invalidPrefix: s("invalidPrefix"),
202
+ answeredVia: (how, who) => s("answeredVia", { how, who: who ? ` (${who})` : "" }),
203
+ howReaction: (glyph) => s("howReaction", { glyph }),
204
+ howReply: s("howReply"),
205
+ howReplyN: (n) => s("howReplyN", { n }),
206
+ voidStamp: (reason) => s("voidStamp", { reason: reason ? `: ${reason}` : "" }),
207
+ supersededBy: (link) => s("supersededBy", { link }),
208
+ };
209
+ }
210
+
225
211
  /** The "other" copy pills posted before ➕ carried — the same words under ❓.
226
212
  * Kept so `askSalvageUnreadable` can still strip it from an old flattened body. */
227
213
  const ASK_LEGACY_OTHER_LINES: string[] = ASK_LANGS.flatMap((l) =>
@@ -866,3 +852,18 @@ export function askMatchChoice(reply: string, choices: string[]): AskChoiceMatch
866
852
  if (indexes.length > 1) return { kind: "ambiguous", indexes };
867
853
  return { kind: "chosen", index: indexes[0]! };
868
854
  }
855
+
856
+ /** `--waitFor` polls `conversations.replies` (or history) on a φ-growing
857
+ * delay. That method's rate limit is per app, shared by every waiter on the
858
+ * same token and by `slack stream`'s thread reads: eight waiters held at 5 s
859
+ * made ~96 calls a minute against Tier 3's ~50 and kept the bucket at 429, so
860
+ * every thread-reply doorbell waited out a Retry-After. A question answered
861
+ * within its first minutes still gets the fast poll; a long wait settles at
862
+ * `slowMaxMs` instead. */
863
+ export const ASK_POLL = { phi: 1.618033988749895, minMs: 1000, fastMaxMs: 5000, fastWindowMs: 120_000, slowMaxMs: 20_000 } as const;
864
+
865
+ /** The next poll delay, `elapsedMs` into the wait. */
866
+ export function askNextPollDelay(prevMs: number, elapsedMs: number): number {
867
+ const cap = elapsedMs < ASK_POLL.fastWindowMs ? ASK_POLL.fastMaxMs : ASK_POLL.slowMaxMs;
868
+ return Math.min(Math.max(prevMs, ASK_POLL.minMs) * ASK_POLL.phi, cap);
869
+ }
package/ts/botIntent.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  /** Write commands, as `argv._` spells them (aliases included). Read-only
12
12
  * commands are never refused — reading as the user impersonates no one. */
13
13
  const WRITES = new Set([
14
- "send", "ask", "poll", "vote", "edit", "delete", "react", "upload",
14
+ "send", "reply", "outbox flush", "ask", "poll", "vote", "edit", "tick", "delete", "react", "upload",
15
15
  "schedule send", "schedule rm",
16
16
  "pinlog new", "pinlog set", "pinlog note", "pinlog pin",
17
17
  "todo set", "todo flag",
@@ -0,0 +1,30 @@
1
+ // The choice-question hint on send/reply: a message that asks
2
+ // "(yes/no)?" or lists (a)/(b) options is better posted with `slack ask`, so
3
+ // the reader taps an answer instead of typing one. Hint only — never blocks.
4
+ // The patterns are per-language packs (locales/*.json `choice_patterns`), all
5
+ // of them applied: the question is spotted in any language.
6
+ import { choicePatterns } from "./locales.ts";
7
+
8
+ /** Text with what cannot be part of the question removed: code, URLs,
9
+ * Slack's <…> tokens (links, mentions) — `?a=1` in a link is not a question. */
10
+ function prose(text: string): string {
11
+ return text
12
+ .replace(/```[\s\S]*?```/g, " ")
13
+ .replace(/`[^`\n]*`/g, " ")
14
+ .replace(/<[^>\s]*>/g, " ")
15
+ .replace(/https?:\/\/\S+/g, " ");
16
+ }
17
+
18
+ /** The language pack whose pattern matched, or undefined. Line by line, so a
19
+ * pattern anchored at the end of a line sees each question. */
20
+ export function choiceQuestionLang(text: string): string | undefined {
21
+ const pats = choicePatterns();
22
+ for (const line of prose(text).split("\n")) {
23
+ for (const p of pats) if (p.re.test(line)) return p.lang;
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ export function choiceHint(target: string): string {
29
+ return `hint: this looks like a choice question — use slack ask '${target}' "<question>" <choice>… so the reader can tap an answer instead of typing it`;
30
+ }