slack-term 1.31.0 → 1.31.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.31.0",
3
+ "version": "1.31.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
package/ts/ask.ts CHANGED
@@ -101,6 +101,38 @@ export function isInvalidNotice(line: string | undefined): boolean {
101
101
  export const ASK_RESOLVED_MARKER = "white_check_mark";
102
102
  export const ASK_RESOLVED_PREFIX = `:${ASK_RESOLVED_MARKER}: `;
103
103
 
104
+ /** How a keycap is WRITTEN into the body, and how it is read back.
105
+ *
106
+ * Slack NORMALISES a unicode keycap in the stored `text`: post `1️⃣` and
107
+ * `conversations.history` returns `:one:`. So a body built with glyphs never
108
+ * reads back as what was sent, and `--waitFor` cannot recognise its own
109
+ * question. `poll` already handled this; `ask` did not, which made every
110
+ * pill-answered question uncollectable (reported from real use 2026-08-31:
111
+ * four questions answered, nothing collected for ~20 minutes).
112
+ *
113
+ * Both spellings are accepted on the way in: the shortcode is what Slack
114
+ * stores, and the glyph is what a body hand-edited in the Slack UI can be. */
115
+ /** How a keycap is WRITTEN into the body: the shortcode, because that is the
116
+ * form Slack stores and therefore the form that reads back unchanged. */
117
+ function askPill(i: number): string {
118
+ return `:${ASK_KEYCAPS[i]!.name}:`;
119
+ }
120
+
121
+ function askStripPill(line: string, i: number): string | null {
122
+ const k = ASK_KEYCAPS[i]!;
123
+ for (const pre of [askPill(i), k.glyph]) {
124
+ if (line.startsWith(`${pre} `)) return line.slice(pre.length + 1);
125
+ if (line === pre) return "";
126
+ }
127
+ return null;
128
+ }
129
+
130
+ /** True if the line opens with ANY keycap — used only to find where the choice
131
+ * block starts; WHICH keycap it is gets checked afterwards, in order. */
132
+ function askIsPillLine(line: string): boolean {
133
+ return ASK_KEYCAPS.some((_k, i) => askStripPill(line, i) !== null);
134
+ }
135
+
104
136
  /** One line, always: newlines inside a choice would break the line-based body. */
105
137
  export function askFlatten(s: string): string {
106
138
  return s.replace(/\s*\n\s*/g, " ");
@@ -116,7 +148,12 @@ export function askBuildText(question: string, body: string, reactable: string[]
116
148
  lines.push("");
117
149
  // Flattened: a choice is a pill label, and a newline in one would split it
118
150
  // into a line the parser cannot tell from body text.
119
- lines.push(...reactable.map((s, i) => `${ASK_KEYCAPS[i]!.glyph} ${askFlatten(s)}`));
151
+ // Built with the SHORTCODE, matching what Slack stores. Building with the
152
+ // glyph worked only because the parser now accepts both — the body still
153
+ // came back as `:one:` and never matched what was sent. Writing the stored
154
+ // form keeps posted and read-back bytes identical, which is what the
155
+ // round-trip test can actually check. (`poll` already did this.)
156
+ lines.push(...reactable.map((s, i) => `${askPill(i)} ${askFlatten(s)}`));
120
157
  if (overflow.length) {
121
158
  lines.push("");
122
159
  lines.push(...overflow.map((s, i) => `(${i + 11}) ${askFlatten(s)}`));
@@ -154,6 +191,29 @@ export type AskParsed =
154
191
  * by a loose heuristic: it is the one part of the body this command wrote
155
192
  * verbatim, so matching it exactly is what keeps an unrelated message from
156
193
  * being polled as if it were an ask. */
194
+ /** Why `askParseMessage` rejected a body, in the reader's terms.
195
+ *
196
+ * The single generic "this is not an ask" line sent a real user re-checking
197
+ * permalinks that were fine, and cost ~20 minutes while pressed answers went
198
+ * uncollected (2026-08-31). A rejection has to say WHICH check failed, because
199
+ * the three causes have completely different fixes: wrong link, a body someone
200
+ * edited, or a bug here. */
201
+ export function askExplainReject(text: string): string {
202
+ if (!text.trim()) return "本文が空です (bot 投稿や添付のみのメッセージかもしれません)";
203
+ const lines = text.split("\n");
204
+ const last = lines[lines.length - 1] ?? "";
205
+ if (text.startsWith(ASK_RESOLVED_PREFIX)) {
206
+ return "回答済みマーク付きですが、引用された回答本文が見つかりません";
207
+ }
208
+ if (!/_$/.test(last)) {
209
+ return "末尾が `ask` の案内文ではありません — このメッセージは `slack ask` で投稿されたものではないか、本文が編集されています";
210
+ }
211
+ if (!lines[0]!.startsWith(ASK_MARKER_PREFIX) && !lines[0]!.startsWith("*")) {
212
+ return "先頭が質問文 (太字) ではありません — 本文が編集されている可能性があります";
213
+ }
214
+ return "選択肢の並びを読み取れません — 番号が 1 から連番になっていないか、本文が編集されています";
215
+ }
216
+
157
217
  export function askParseMessage(text: string): AskParsed {
158
218
  const lines = text.split("\n");
159
219
 
@@ -221,7 +281,7 @@ export function askParseMessage(text: string): AskParsed {
221
281
  const raw: string[] = [];
222
282
  while (i >= 0) {
223
283
  const line = lines[i]!;
224
- if (!ASK_KEYCAPS.some((k) => line.startsWith(`${k.glyph} `) || line === k.glyph)) break;
284
+ if (!askIsPillLine(line)) break;
225
285
  raw.unshift(line);
226
286
  i--;
227
287
  }
@@ -231,9 +291,9 @@ export function askParseMessage(text: string): AskParsed {
231
291
  // block starting at 2️⃣ would have it waiting on a pill nobody can press.
232
292
  if (!raw.length || raw.length > ASK_MAX_REACTION_CHOICES) return { kind: "other" };
233
293
  for (let k = 0; k < raw.length; k++) {
234
- const glyph = ASK_KEYCAPS[k]!.glyph;
235
- if (!(raw[k]!.startsWith(`${glyph} `) || raw[k] === glyph)) return { kind: "other" };
236
- reactable.push(raw[k]!.slice(glyph.length + 1));
294
+ const rest = askStripPill(raw[k]!, k);
295
+ if (rest === null) return { kind: "other" };
296
+ reactable.push(rest);
237
297
  }
238
298
  if (lines[i] !== "") return { kind: "other" };
239
299
  if (i < end + 1) return { kind: "other" };
package/ts/cli.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  askBuildText,
22
22
  askBuildResolvedText,
23
23
  askParseMessage,
24
+ askExplainReject,
24
25
  askFlatten,
25
26
  applyInvalidNotice,
26
27
  readInvalidNotice,
@@ -295,6 +296,17 @@ function slimMsg(m: Record<string, Json>): Record<string, Json> {
295
296
  // Thread stats, present only on thread parents. Without these a caller
296
297
  // reading top-level history cannot tell a post with replies from one
297
298
  // without, and silently misses everything said inside the thread.
299
+ // Reactions, when the message carries any. `ask` and `poll` record ANSWERS
300
+ // as reactions, so omitting them made `--json` useless as the fallback
301
+ // people reach for when a poller fails — the pressed pills were right there
302
+ // in the API response and the CLI dropped them (reported 2026-08-31).
303
+ ...(Array.isArray(m.reactions) && m.reactions.length
304
+ ? { reactions: asArray(m.reactions).map(asRecord).map((r) => ({
305
+ name: r.name ?? null,
306
+ count: r.count ?? null,
307
+ users: Array.isArray(r.users) ? r.users : [],
308
+ })) }
309
+ : {}),
298
310
  ...(m.reply_count !== undefined ? { reply_count: m.reply_count } : {}),
299
311
  ...(m.reply_users_count !== undefined ? { reply_users_count: m.reply_users_count } : {}),
300
312
  ...(m.latest_reply !== undefined ? { latest_reply: m.latest_reply } : {}),
@@ -2235,8 +2247,12 @@ async function cmdAskWaitFor(token: string, args: { link: string; timeout: numbe
2235
2247
  const parsed = askParseMessage(text);
2236
2248
  if (parsed.kind === "other") {
2237
2249
  console.error(
2238
- `Error: これは \`slack ask\` の質問ではありません (回答の読み取り方が分かりません)。\n` +
2239
- ` ${stripTerminalControls(args.link)}`,
2250
+ `Error: これは \`slack ask\` の質問として読み取れません。\n` +
2251
+ ` 理由: ${askExplainReject(text)}\n` +
2252
+ ` ${stripTerminalControls(args.link)}\n` +
2253
+ // The pills are still readable by hand, so a failure here is not a dead
2254
+ // end — say so, because the reported cost was not knowing that.
2255
+ ` (押されたリアクションは残っています: slack read '${stripTerminalControls(args.link)}' で確認できます)`,
2240
2256
  );
2241
2257
  process.exit(ASK_EXIT_ERROR);
2242
2258
  }