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/README.md +14 -7
- package/SKILL.md +28 -10
- package/dist/cli.js +201 -199
- package/package.json +1 -1
- package/ts/ask.ts +79 -24
- package/ts/botIntent.ts +1 -0
- package/ts/cli.ts +280 -37
- package/ts/slack.ts +32 -0
- package/ts/slackTodos.ts +4 -3
package/package.json
CHANGED
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* the
|
|
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
|
|
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
|
|
149
|
-
* the reaction row reads the same as
|
|
150
|
-
*
|
|
151
|
-
* choice, and says where the actual answer has to go:
|
|
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: `${
|
|
168
|
-
otherHere: `${
|
|
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: `${
|
|
189
|
-
otherHere: `${
|
|
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
|
-
/**
|
|
203
|
-
*
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
387
|
+
// Directly under the pills, matching the ➕ seeded right after them.
|
|
356
388
|
lines.push(threadOnly ? c.otherThread : c.otherHere);
|
|
357
|
-
// Under
|
|
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 (
|
|
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",
|