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.
- package/README.md +56 -0
- package/dist/cli.js +289 -249
- package/locales/en.json +32 -0
- package/locales/ja.json +31 -0
- package/package.json +2 -1
- package/ts/ask.ts +47 -46
- package/ts/botIntent.ts +1 -1
- package/ts/choiceHint.ts +30 -0
- package/ts/cli.ts +739 -123
- package/ts/inbox.ts +191 -0
- package/ts/locales.ts +113 -0
- package/ts/readback.ts +102 -0
- package/ts/redact.ts +225 -0
- package/ts/scope.ts +472 -0
- package/ts/slack.ts +40 -2
- package/ts/stream.ts +3 -0
- package/ts/tick.ts +47 -0
- package/ts/vendor/gitleaks.LICENSE +21 -0
- package/ts/vendor/gitleaks.toml +3209 -0
package/locales/en.json
ADDED
|
@@ -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
|
+
}
|
package/locales/ja.json
ADDED
|
@@ -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.
|
|
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
|
-
* ❓ (
|
|
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. `
|
|
105
|
-
*
|
|
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: "
|
|
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
|
-
|
|
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",
|
package/ts/choiceHint.ts
ADDED
|
@@ -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
|
+
}
|