slack-term 1.31.0 → 1.31.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.31.0",
3
+ "version": "1.31.1",
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,32 @@ 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
+ function askStripPill(line: string, i: number): string | null {
116
+ const k = ASK_KEYCAPS[i]!;
117
+ for (const pre of [`:${k.name}:`, k.glyph]) {
118
+ if (line.startsWith(`${pre} `)) return line.slice(pre.length + 1);
119
+ if (line === pre) return "";
120
+ }
121
+ return null;
122
+ }
123
+
124
+ /** True if the line opens with ANY keycap — used only to find where the choice
125
+ * block starts; WHICH keycap it is gets checked afterwards, in order. */
126
+ function askIsPillLine(line: string): boolean {
127
+ return ASK_KEYCAPS.some((_k, i) => askStripPill(line, i) !== null);
128
+ }
129
+
104
130
  /** One line, always: newlines inside a choice would break the line-based body. */
105
131
  export function askFlatten(s: string): string {
106
132
  return s.replace(/\s*\n\s*/g, " ");
@@ -154,6 +180,29 @@ export type AskParsed =
154
180
  * by a loose heuristic: it is the one part of the body this command wrote
155
181
  * verbatim, so matching it exactly is what keeps an unrelated message from
156
182
  * being polled as if it were an ask. */
183
+ /** Why `askParseMessage` rejected a body, in the reader's terms.
184
+ *
185
+ * The single generic "this is not an ask" line sent a real user re-checking
186
+ * permalinks that were fine, and cost ~20 minutes while pressed answers went
187
+ * uncollected (2026-08-31). A rejection has to say WHICH check failed, because
188
+ * the three causes have completely different fixes: wrong link, a body someone
189
+ * edited, or a bug here. */
190
+ export function askExplainReject(text: string): string {
191
+ if (!text.trim()) return "本文が空です (bot 投稿や添付のみのメッセージかもしれません)";
192
+ const lines = text.split("\n");
193
+ const last = lines[lines.length - 1] ?? "";
194
+ if (text.startsWith(ASK_RESOLVED_PREFIX)) {
195
+ return "回答済みマーク付きですが、引用された回答本文が見つかりません";
196
+ }
197
+ if (!/_$/.test(last)) {
198
+ return "末尾が `ask` の案内文ではありません — このメッセージは `slack ask` で投稿されたものではないか、本文が編集されています";
199
+ }
200
+ if (!lines[0]!.startsWith(ASK_MARKER_PREFIX) && !lines[0]!.startsWith("*")) {
201
+ return "先頭が質問文 (太字) ではありません — 本文が編集されている可能性があります";
202
+ }
203
+ return "選択肢の並びを読み取れません — 番号が 1 から連番になっていないか、本文が編集されています";
204
+ }
205
+
157
206
  export function askParseMessage(text: string): AskParsed {
158
207
  const lines = text.split("\n");
159
208
 
@@ -221,7 +270,7 @@ export function askParseMessage(text: string): AskParsed {
221
270
  const raw: string[] = [];
222
271
  while (i >= 0) {
223
272
  const line = lines[i]!;
224
- if (!ASK_KEYCAPS.some((k) => line.startsWith(`${k.glyph} `) || line === k.glyph)) break;
273
+ if (!askIsPillLine(line)) break;
225
274
  raw.unshift(line);
226
275
  i--;
227
276
  }
@@ -231,9 +280,9 @@ export function askParseMessage(text: string): AskParsed {
231
280
  // block starting at 2️⃣ would have it waiting on a pill nobody can press.
232
281
  if (!raw.length || raw.length > ASK_MAX_REACTION_CHOICES) return { kind: "other" };
233
282
  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));
283
+ const rest = askStripPill(raw[k]!, k);
284
+ if (rest === null) return { kind: "other" };
285
+ reactable.push(rest);
237
286
  }
238
287
  if (lines[i] !== "") return { kind: "other" };
239
288
  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
  }