slack-term 1.44.0 → 1.46.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.44.0",
3
+ "version": "1.46.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/slack-term.git"
package/ts/ask.ts CHANGED
@@ -34,17 +34,40 @@ export type AskFound = { answer: string; how: string; who?: string; ts?: string;
34
34
  * in-flight question unparseable. The marker carries identity now, and the
35
35
  * instruction is free to be translated.
36
36
  *
37
- * It is also seeded as a REACTION, which is what makes `has:question` list
38
- * every question the way `has:pushpin` lists every todo. `todo` gave this
39
- * emoji up (its needs-discussion flag moved to :thinking_face:) precisely so
40
- * the two would not collide in search. */
37
+ * It is also seeded as a REACTION on free-text questions and on pills posted
38
+ * before ➕; a question with choices now seeds ➕ (`ASK_OTHER_MARKER`) in its
39
+ * place. So `has::question: OR has::heavy_plus_sign:`-style search lists the
40
+ * open questions the way `has::pushpin:` lists every todo — with the trade-off
41
+ * that ➕ is a common emoji, so that search can also match unrelated messages
42
+ * (`slack ask --ls` reads the local sent-log and is unaffected). `todo` gave
43
+ * ❓ up (its needs-discussion flag moved to :thinking_face:) precisely so the
44
+ * two would not collide in search. */
41
45
  export const ASK_MARKER = "question";
42
46
  export const ASK_MARKER_PREFIX = `:${ASK_MARKER}: `;
43
47
 
48
+ /** The "other" pill — "none of these fit, my answer follows as a reply" —
49
+ * 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".
51
+ *
52
+ * ❓ stays the HEAD marker (`ASK_MARKER_PREFIX` on the first line): that is the
53
+ * wire format that identifies an ask, and every reader keys on it. Only the
54
+ * pill moved. A free-text question has no other line and still seeds ❓.
55
+ *
56
+ * Pills posted before this carry ❓ as the other pill, both as the seed and
57
+ * as the body line. They stay fully working: the parser accepts either line
58
+ * (`askSeedMarker(text)` says which one a question was seeded with), a press
59
+ * on either counts as "other", and void / edit / resolve take off exactly the
60
+ * seed that is there. */
61
+ export const ASK_OTHER_MARKER = "heavy_plus_sign";
62
+ export const ASK_OTHER_PREFIX = `:${ASK_OTHER_MARKER}: `;
63
+ /** Every reaction that means "other" on some open pill: ➕ now, ❓ on the
64
+ * pills posted before ➕. */
65
+ export const ASK_OTHER_MARKERS: readonly string[] = [ASK_OTHER_MARKER, ASK_MARKER];
66
+
44
67
  /** The "explain" pill (`--explain`): pressing it asks the asker to explain the
45
68
  * question, and is NEVER an answer. Reported 2026-10-14: an "explain" offered
46
69
  * as a numbered choice was collected as the decision and closed the question.
47
- * Not ❓: ❓ means "none of these fit — my answer follows as a reply", and that
70
+ * Not ➕: ➕ means "none of these fit — my answer follows as a reply", and that
48
71
  * reply resolves the question; ℹ️ means "I cannot answer yet", and the question
49
72
  * stays open for a real answer after the explanation. */
50
73
  export const ASK_EXPLAIN_MARKER = "information_source";
@@ -145,10 +168,10 @@ interface AskCopy {
145
168
  }
146
169
 
147
170
  /** The standing "none of these" choice (`otherThread` / `otherHere`), listed
148
- * after the numbered ones. It is the ❓ marker pill, which is seeded LAST so
149
- * the reaction row reads the same as the body: 1️⃣ 2️⃣ 3️⃣ ❓. People already
150
- * pressed ❓ to mean "none of these fit" — the line makes that an explicit
151
- * choice, and says where the actual answer has to go: ❓ itself carries no
171
+ * after the numbered ones. It is the ➕ pill (`ASK_OTHER_MARKER`; ❓ on pills
172
+ * posted before 2026-10-14), seeded LAST so the reaction row reads the same as
173
+ * the body: 1️⃣ 2️⃣ 3️⃣ ➕. The line makes "none of these fit" an explicit
174
+ * choice, and says where the actual answer has to go: ➕ itself carries no
152
175
  * answer text, so it never resolves the question; the reply that follows it
153
176
  * does (exit 5, delivered as free text). Optional on the way in: questions
154
177
  * posted before it existed have no such line and must stay collectable.
@@ -164,8 +187,8 @@ export const ASK_COPY: Record<AskLang, AskCopy> = {
164
187
  "_このメッセージの *スレッド* で返信してください。本文がそのまま回答になります (チャンネルへの通常投稿は回答として拾いません)。_",
165
188
  instructionTextHere: "_このメッセージに返信してください。本文がそのまま回答になります。_",
166
189
  overflowNote: "_11 番目以降はリアクションがないので、返信で答えてください。_",
167
- otherThread: `${ASK_MARKER_PREFIX}その他 — スレッドで返信してください`,
168
- otherHere: `${ASK_MARKER_PREFIX}その他 — このメッセージに返信してください`,
190
+ otherThread: `${ASK_OTHER_PREFIX}その他 — スレッドで返信してください`,
191
+ otherHere: `${ASK_OTHER_PREFIX}その他 — このメッセージに返信してください`,
169
192
  explainLine: `${ASK_EXPLAIN_PREFIX}説明して — 押すと質問者に説明を頼みます (回答にはなりません)`,
170
193
  explainNote: `${ASK_EXPLAIN_PREFIX}説明のリクエストを受け付けました — このスレッドで説明します。質問はまだ開いているので、回答はそのあとで大丈夫です。`,
171
194
  invalidPrefix: "_:warning: 同時に複数選ばれているため回答として数えていません — どれか 1 つだけ残してください: ",
@@ -185,8 +208,8 @@ export const ASK_COPY: Record<AskLang, AskCopy> = {
185
208
  "_Reply in this message's *thread* — your reply is the answer (ordinary channel posts are not picked up as answers)._",
186
209
  instructionTextHere: "_Reply to this message — your reply is the answer._",
187
210
  overflowNote: "_Choices 11 and up have no reaction — answer them with a reply._",
188
- otherThread: `${ASK_MARKER_PREFIX}Other — reply in the thread`,
189
- otherHere: `${ASK_MARKER_PREFIX}Other — reply to this message`,
211
+ otherThread: `${ASK_OTHER_PREFIX}Other — reply in the thread`,
212
+ otherHere: `${ASK_OTHER_PREFIX}Other — reply to this message`,
190
213
  explainLine: `${ASK_EXPLAIN_PREFIX}Explain — press to ask the asker for more detail (not an answer)`,
191
214
  explainNote: `${ASK_EXPLAIN_PREFIX}Explanation requested — it will follow in this thread. The question stays open; answer it after that.`,
192
215
  invalidPrefix: "_:warning: Several choices are selected, so this is not counted as an answer — keep only one: ",
@@ -199,11 +222,20 @@ export const ASK_COPY: Record<AskLang, AskCopy> = {
199
222
  },
200
223
  };
201
224
 
202
- /** True for the "other" line in either spelling — the shortcode we write, or the
203
- * glyph a body hand-edited in the Slack UI can carry. Matched by prefix, not
204
- * verbatim, so the copy stays free to change. */
205
- function askIsOtherLine(line: string | undefined): boolean {
206
- return line !== undefined && (line.startsWith(ASK_MARKER_PREFIX) || line.startsWith("❓ "));
225
+ /** The "other" copy pills posted before ➕ carried — the same words under ❓.
226
+ * Kept so `askSalvageUnreadable` can still strip it from an old flattened body. */
227
+ const ASK_LEGACY_OTHER_LINES: string[] = ASK_LANGS.flatMap((l) =>
228
+ [ASK_COPY[l].otherThread, ASK_COPY[l].otherHere].map((s) => ASK_MARKER_PREFIX + s.slice(ASK_OTHER_PREFIX.length)));
229
+
230
+ /** Which reaction the "other" line names, or null if the line is not one: ➕ or
231
+ * ❓ (pills posted before ➕), each as the shortcode we write or the glyph a
232
+ * body hand-edited in the Slack UI can carry. Matched by prefix, not verbatim,
233
+ * so the copy stays free to change. */
234
+ function askOtherLineMarker(line: string | undefined): string | null {
235
+ if (line === undefined) return null;
236
+ if (line.startsWith(ASK_OTHER_PREFIX) || line.startsWith("➕ ")) return ASK_OTHER_MARKER;
237
+ if (line.startsWith(ASK_MARKER_PREFIX) || line.startsWith("❓ ")) return ASK_MARKER;
238
+ return null;
207
239
  }
208
240
 
209
241
  /** True for the explain line, in either spelling. Matched by prefix, like the
@@ -287,7 +319,7 @@ export const ASK_RESOLVED_PREFIX = `:${ASK_RESOLVED_MARKER}: `;
287
319
  /** Prefix `--void` stamps on a question that should no longer be answered —
288
320
  * expired, superseded, or overtaken by events (無効化). A THIRD state beside open
289
321
  * (`:question:`) and answered (`:white_check_mark:`), on purpose: anything that
290
- * keys on "starts with :question:" (the stale-pill reminder, `has::question:`)
322
+ * keys on "starts with :question:" (the stale-pill reminder, `has::question: OR has::heavy_plus_sign:`)
291
323
  * stops counting it the moment it is voided, and nothing reads it as answered.
292
324
  * Also seeded as a reaction, so `has::no_entry_sign:` lists every void. */
293
325
  export const ASK_VOID_MARKER = "no_entry_sign";
@@ -352,9 +384,9 @@ export function askBuildText(question: string, body: string, reactable: string[]
352
384
  // form keeps posted and read-back bytes identical, which is what the
353
385
  // round-trip test can actually check. (`poll` already did this.)
354
386
  lines.push(...reactable.map((s, i) => `${askPill(i)} ${askFlatten(s)}`));
355
- // Directly under the pills, matching the ❓ seeded right after them.
387
+ // Directly under the pills, matching the ➕ seeded right after them.
356
388
  lines.push(threadOnly ? c.otherThread : c.otherHere);
357
- // Under ❓, matching the ℹ️ seeded right after it.
389
+ // Under ➕, matching the ℹ️ seeded right after it.
358
390
  if (explain) lines.push(c.explainLine);
359
391
  if (overflow.length) {
360
392
  lines.push("");
@@ -382,7 +414,7 @@ export function askBuildText(question: string, body: string, reactable: string[]
382
414
  * decision was made against) and the option line that was chosen. Reported
383
415
  * from real use 2026-10-05: collecting an answer replaced the whole message
384
416
  * with question + answer, and the context of the decision was gone. Only what
385
- * no longer applies goes — the options NOT chosen, the ❓ line, the answering
417
+ * no longer applies goes — the options NOT chosen, the ➕ (or ❓) line, the answering
386
418
  * instructions.
387
419
  *
388
420
  * The head is unchanged on purpose: ✅ prefix, question, stamp, blank, quoted
@@ -453,7 +485,7 @@ export function askSalvageUnreadable(text: string): { question: string; body: st
453
485
  for (const instr of [c.instructionReactionThread, c.instructionReactionHere, c.instructionTextThread, c.instructionTextHere]) {
454
486
  if (body.includes(instr)) wasAsk = true;
455
487
  }
456
- for (const fixed of [c.instructionReactionThread, c.instructionReactionHere, c.instructionTextThread, c.instructionTextHere, c.overflowNote, c.otherThread, c.otherHere, c.explainLine]) {
488
+ for (const fixed of [c.instructionReactionThread, c.instructionReactionHere, c.instructionTextThread, c.instructionTextHere, c.overflowNote, c.otherThread, c.otherHere, ...ASK_LEGACY_OTHER_LINES, c.explainLine]) {
457
489
  body = body.split(fixed).join(" ");
458
490
  }
459
491
  const at = body.indexOf(c.invalidPrefix);
@@ -660,7 +692,7 @@ export function askParseMessage(text: string): AskParsed {
660
692
  i--;
661
693
  }
662
694
  if (askIsExplainLine(lines[i])) { explain = true; i--; }
663
- if (askIsOtherLine(lines[i])) i--;
695
+ if (askOtherLineMarker(lines[i])) i--;
664
696
  const raw: string[] = [];
665
697
  while (i >= 0) {
666
698
  const line = lines[i]!;
@@ -697,6 +729,29 @@ export function askParseMessage(text: string): AskParsed {
697
729
  return { kind: "open", question, body, reactable, overflow, threadOnly, lang, explain };
698
730
  }
699
731
 
732
+ /** The marker reaction `ask` seeded on an open question: ➕ when its other line
733
+ * says ➕, else ❓ (a free-text question, or a pill posted before ➕ — with a ❓
734
+ * other line or none at all). What void / edit / the ✅ swap take off. Read
735
+ * from the line directly under the last numbered pill, the one place the
736
+ * other line can be; null when the text is not an open ask.
737
+ *
738
+ * Separate from `askParseMessage` so the parse result keeps its shape. */
739
+ export function askSeedMarker(text: string): string | null {
740
+ const p = askParseMessage(text);
741
+ if (p.kind !== "open") return null;
742
+ if (!p.reactable.length) return ASK_MARKER;
743
+ const lines = text.split("\n");
744
+ const last = askPill(p.reactable.length - 1);
745
+ const k = ASK_KEYCAPS[p.reactable.length - 1]!.glyph;
746
+ // The last pill line is the LAST line opening with that keycap: the block is
747
+ // anchored to the bottom, and a body line can start with the same keycap.
748
+ let at = -1;
749
+ for (let i = lines.length - 1; i >= 0; i--) {
750
+ if (lines[i]!.startsWith(`${last} `) || lines[i] === last || lines[i]!.startsWith(`${k} `) || lines[i] === k) { at = i; break; }
751
+ }
752
+ return (at >= 0 ? askOtherLineMarker(lines[at + 1]) : null) ?? ASK_MARKER;
753
+ }
754
+
700
755
  /** Did a free-text reply actually PICK one of the offered choices?
701
756
  *
702
757
  * Reported from real use 2026-09-04: three options were offered, the answerer
package/ts/botIntent.ts CHANGED
@@ -15,6 +15,7 @@ const WRITES = new Set([
15
15
  "schedule send", "schedule rm",
16
16
  "pinlog new", "pinlog set", "pinlog note", "pinlog pin",
17
17
  "todo set", "todo flag",
18
+ "todos add", "todos done", "todos complete", "todos edit", "todos rm", "todos remove", "todos delete",
18
19
  "lists create", "lists add", "lists set", "lists share",
19
20
  "drafts new", "drafts save", "drafts edit", "drafts update", "drafts delete", "drafts rm",
20
21
  "channel create", "channel new", "channel rename", "channel mv",