slack-term 1.32.0 → 1.32.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/README.md +2 -1
- package/SKILL.md +9 -2
- package/dist/cli.js +118 -118
- package/package.json +1 -1
- package/ts/ask.ts +106 -0
- package/ts/cli.ts +107 -6
package/package.json
CHANGED
package/ts/ask.ts
CHANGED
|
@@ -305,3 +305,109 @@ export function askParseMessage(text: string): AskParsed {
|
|
|
305
305
|
|
|
306
306
|
return { kind: "open", question, reactable, overflow, threadOnly };
|
|
307
307
|
}
|
|
308
|
+
|
|
309
|
+
/** Did a free-text reply actually PICK one of the offered choices?
|
|
310
|
+
*
|
|
311
|
+
* Reported from real use 2026-09-04: three options were offered, the answerer
|
|
312
|
+
* replied 「没懂,能给我讲前因后果吗」 ("I don't follow — can you explain the
|
|
313
|
+
* background?"), and `--waitFor` exited 0 with that stored AS the decision. Any
|
|
314
|
+
* reply from an addressed person counted, because nothing compared the reply to
|
|
315
|
+
* the candidates. A lane harvesting that unparks the work and proceeds with
|
|
316
|
+
* nothing behind it, and the recorded answer reads as though he had chosen.
|
|
317
|
+
*
|
|
318
|
+
* Deliberately NARROW. Every loosening here re-creates the same defect one step
|
|
319
|
+
* further out, and the two error directions are not symmetric: calling a
|
|
320
|
+
* non-answer an answer makes an automated flow act on a decision nobody took,
|
|
321
|
+
* while calling an answer a non-answer parks it in front of a human who can
|
|
322
|
+
* settle it with one reaction. So: an exact choice, a bare number, a keycap, or
|
|
323
|
+
* a number introducing its own choice text. Nothing fuzzy, nothing partial —
|
|
324
|
+
* "the second one" and "option B" are NOT matches, on purpose.
|
|
325
|
+
*
|
|
326
|
+
* `choices` is the full candidate list, reactable and overflow together, in
|
|
327
|
+
* presentation order. The overflow ones (11+) can only ever be answered as
|
|
328
|
+
* text, so a matcher that ignored them would call every legitimate answer to a
|
|
329
|
+
* long question "not chosen" — the failure the caller cannot see. */
|
|
330
|
+
export type AskChoiceMatch =
|
|
331
|
+
| { kind: "chosen"; index: number } // 1-based, matching the printed numbering
|
|
332
|
+
| { kind: "none" }
|
|
333
|
+
| { kind: "ambiguous"; indexes: number[] };
|
|
334
|
+
|
|
335
|
+
/** Lowercase, NFKC-fold (full-width 7 and 7.become 7), strip decoration and
|
|
336
|
+
* one trailing sentence mark, collapse runs of space. NFKC is what lets a reply
|
|
337
|
+
* typed on a Japanese IME match a choice typed on an ASCII keyboard. */
|
|
338
|
+
function askNormalizeChoice(s: string): string {
|
|
339
|
+
return s
|
|
340
|
+
.normalize("NFKC")
|
|
341
|
+
.replace(/[*_`~]/g, "")
|
|
342
|
+
.trim()
|
|
343
|
+
// Brackets and sentence marks are stripped in ONE class at each end rather
|
|
344
|
+
// than in two passes: 「中止」。 puts the full stop OUTSIDE the quote, so
|
|
345
|
+
// stripping quotes and then punctuation leaves the closing 」 behind and the
|
|
346
|
+
// reply stops matching a choice it plainly names.
|
|
347
|
+
.replace(/^[\s"'`「『((\[]+/, "")
|
|
348
|
+
.replace(/[\s"'`」』))\]。..!!??、,]+$/, "")
|
|
349
|
+
.replace(/\s+/g, " ")
|
|
350
|
+
.trim()
|
|
351
|
+
.toLowerCase();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function askMatchChoice(reply: string, choices: string[]): AskChoiceMatch {
|
|
355
|
+
const norm = askNormalizeChoice(reply);
|
|
356
|
+
if (!norm) return { kind: "none" };
|
|
357
|
+
const normChoices = choices.map(askNormalizeChoice);
|
|
358
|
+
|
|
359
|
+
const hits = new Set<number>();
|
|
360
|
+
|
|
361
|
+
// 1. The reply IS the choice text.
|
|
362
|
+
normChoices.forEach((c, i) => { if (c && c === norm) hits.add(i + 1); });
|
|
363
|
+
|
|
364
|
+
// 2. A keycap glyph, which is what someone copies out of the message when they
|
|
365
|
+
// cannot react (a thread on mobile, or a choice past the tenth).
|
|
366
|
+
ASK_KEYCAPS.forEach((k, i) => {
|
|
367
|
+
if (i < choices.length && reply.trim() === k.glyph) hits.add(i + 1);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// 3. A number: bare, or introducing the choice it names. `1` and `1. foo` are
|
|
371
|
+
// the same act. A number followed by DIFFERENT text is not a match — that
|
|
372
|
+
// is someone writing prose that happens to start with a digit.
|
|
373
|
+
//
|
|
374
|
+
// The separator is REQUIRED once text follows, and matched as its own
|
|
375
|
+
// alternative rather than as an optional group. With it optional, `100`
|
|
376
|
+
// parsed as choice 10 followed by "0" — so a ten-option question whose
|
|
377
|
+
// tenth choice is "0" matched a reply nobody meant as a choice. That is the
|
|
378
|
+
// expensive direction of the error, invented while fixing the same one.
|
|
379
|
+
// (cross-vendor review, 2026-09-05)
|
|
380
|
+
let n = 0;
|
|
381
|
+
let rest = "";
|
|
382
|
+
let m = norm.match(/^(\d{1,2})$/);
|
|
383
|
+
if (m) {
|
|
384
|
+
n = Number(m[1]);
|
|
385
|
+
} else if ((m = norm.match(/^(\d{1,2})(?:\s*[.)::、。\-\]]\s*|\s+)(.*)$/))) {
|
|
386
|
+
n = Number(m[1]);
|
|
387
|
+
// Normalised AGAIN, not merely trimmed: `1. (Release)` reaches here as
|
|
388
|
+
// `(release` — the number kept the opening bracket from being stripped at
|
|
389
|
+
// the head of the string while the closing one went at the tail, leaving an
|
|
390
|
+
// unbalanced remainder that matched nothing. Re-running the same normaliser
|
|
391
|
+
// on the remainder is what makes a bracketed choice answerable by number.
|
|
392
|
+
rest = askNormalizeChoice(m[2]!);
|
|
393
|
+
// A separator with NOTHING after it is not a choice: `1 -` is someone who
|
|
394
|
+
// started typing and stopped, and selecting option 1 for them is the
|
|
395
|
+
// expensive error. `1.` and `1)` still work — their punctuation is stripped
|
|
396
|
+
// at the tail, so they arrive at the bare-number branch above.
|
|
397
|
+
if (!rest) return { kind: "none" };
|
|
398
|
+
// An all-digit remainder is a range or a score (`1-2`), not a numbered
|
|
399
|
+
// choice. Without this, a question whose options are themselves numbers
|
|
400
|
+
// matched `1-2` as "option 1, whose text is 2". Rejecting it fails to the
|
|
401
|
+
// CHEAP side: a genuine `1. 2` re-asks, a false selection does not.
|
|
402
|
+
if (/^\d+$/.test(rest)) return { kind: "none" };
|
|
403
|
+
}
|
|
404
|
+
if (n >= 1 && n <= choices.length && (rest === "" || rest === normChoices[n - 1])) hits.add(n);
|
|
405
|
+
|
|
406
|
+
const indexes = [...hits].sort((a, b) => a - b);
|
|
407
|
+
if (!indexes.length) return { kind: "none" };
|
|
408
|
+
// Two distinct choices matched — the same refusal-to-guess the poller already
|
|
409
|
+
// applies when someone presses two pills. Identical choice TEXT is the usual
|
|
410
|
+
// cause, and picking the lower index would answer with a coin flip.
|
|
411
|
+
if (indexes.length > 1) return { kind: "ambiguous", indexes };
|
|
412
|
+
return { kind: "chosen", index: indexes[0]! };
|
|
413
|
+
}
|
package/ts/cli.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
askParseMessage,
|
|
24
24
|
askExplainReject,
|
|
25
25
|
askFlatten,
|
|
26
|
+
askMatchChoice,
|
|
26
27
|
applyInvalidNotice,
|
|
27
28
|
readInvalidNotice,
|
|
28
29
|
type AskFound,
|
|
@@ -929,14 +930,27 @@ async function recipientTzFor(token: string, channelId: string, cookie?: string)
|
|
|
929
930
|
}
|
|
930
931
|
}
|
|
931
932
|
|
|
932
|
-
|
|
933
|
+
/** `when` is the moment the message will ACTUALLY reach the person — which is
|
|
934
|
+
* now for a send, and `post_at` for a schedule. Judging a scheduled message by
|
|
935
|
+
* the clock at schedule time answers a question nobody asked: `schedule send`
|
|
936
|
+
* is the one command where the delivery time is stated outright, and reading
|
|
937
|
+
* it off `Date.now()` warned about a message set for December because it was
|
|
938
|
+
* scheduled after 23:00. A warning that is false on the command where the
|
|
939
|
+
* answer is written in the arguments teaches people to ignore the warning. */
|
|
940
|
+
function requireCode(
|
|
941
|
+
provided: string | undefined,
|
|
942
|
+
expected: string,
|
|
943
|
+
contextLines: string[],
|
|
944
|
+
recipientTz?: string | null,
|
|
945
|
+
when: Date = new Date(),
|
|
946
|
+
): void {
|
|
933
947
|
// The clock goes to stdout with the rest of the preview, not to stderr with
|
|
934
948
|
// the code. The preview is what you are asked to LOOK at before committing,
|
|
935
949
|
// and "what time is it where they are" is part of that picture — a caller
|
|
936
950
|
// doing `send ... 2>/dev/null` to read the preview would otherwise drop the
|
|
937
951
|
// one line added to stop a 01:00 send.
|
|
938
952
|
for (const line of contextLines) console.log(line);
|
|
939
|
-
const clock = quietHoursNotice(
|
|
953
|
+
const clock = quietHoursNotice(when, recipientTz);
|
|
940
954
|
if (clock) console.log(clock);
|
|
941
955
|
if (provided !== undefined) {
|
|
942
956
|
console.error(`Code mismatch (got ${provided}, expected ${expected})`);
|
|
@@ -1629,12 +1643,18 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1629
1643
|
// Exit codes are the contract for `--wait`, and a non-zero one must never be
|
|
1630
1644
|
// mistakable for "an answer arrived":
|
|
1631
1645
|
// 0 answered (the answer text is on stdout, and nothing else is)
|
|
1632
|
-
// 2 timed out
|
|
1646
|
+
// 2 timed out (nobody replied)
|
|
1633
1647
|
// 3 transport/config failure
|
|
1648
|
+
// 4 replied, but chose none of the offered choices — stdout stays EMPTY
|
|
1634
1649
|
// Everything human-facing goes to stderr, so `ANS=$(slack ask ... --wait)` is safe.
|
|
1635
1650
|
|
|
1636
1651
|
const ASK_EXIT_TIMEOUT = 2;
|
|
1637
1652
|
const ASK_EXIT_ERROR = 3;
|
|
1653
|
+
/** Somebody replied, and what they wrote picks none of the offered choices.
|
|
1654
|
+
* A THIRD state, distinct from both 0 and 2 on purpose: exit 0 would have an
|
|
1655
|
+
* automated caller act on a decision nobody took, and exit 2 ("nobody
|
|
1656
|
+
* answered") would hide that a human is standing there waiting for something. */
|
|
1657
|
+
const ASK_EXIT_UNCHOSEN = 4;
|
|
1638
1658
|
|
|
1639
1659
|
const ASK_PHI = 1.618033988749895;
|
|
1640
1660
|
const ASK_POLL_MIN_MS = 1000;
|
|
@@ -1717,6 +1737,11 @@ interface AskWaitCtx {
|
|
|
1717
1737
|
threadParentTs?: string;
|
|
1718
1738
|
question: string;
|
|
1719
1739
|
reactable: string[];
|
|
1740
|
+
/** Choices past the tenth. They carry NO reaction — text is the only way to
|
|
1741
|
+
* answer them — so they must be in the candidate list the reply is matched
|
|
1742
|
+
* against, or every legitimate answer to a long question reads as "chose
|
|
1743
|
+
* nothing". */
|
|
1744
|
+
overflow: string[];
|
|
1720
1745
|
threadOnly: boolean;
|
|
1721
1746
|
/** Who may answer. Empty + broadcast=false would accept nobody, which is why
|
|
1722
1747
|
* posting an unaddressed question is refused up front. */
|
|
@@ -1750,6 +1775,12 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
1750
1775
|
const isAnswerer = (user: unknown): user is string =>
|
|
1751
1776
|
typeof user === "string" && !!user && user !== askerUserId && (broadcast || audience.has(user));
|
|
1752
1777
|
|
|
1778
|
+
/** The earliest reply from an answerer that picked none of the choices. Kept
|
|
1779
|
+
* across polls so the timeout can report it, and so the operator is told once
|
|
1780
|
+
* rather than on every tick. */
|
|
1781
|
+
let unchosen: { text: string; who: string; ts: string; ambiguous: boolean } | undefined;
|
|
1782
|
+
let unchosenReported = "";
|
|
1783
|
+
|
|
1753
1784
|
/** Every seed an answerer is on — not just the first. Changing your mind
|
|
1754
1785
|
* leaves BOTH reactions in place (Slack only drops one when you actively
|
|
1755
1786
|
* un-react), so taking the lowest index would answer with the abandoned
|
|
@@ -1809,6 +1840,10 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
1809
1840
|
return null;
|
|
1810
1841
|
}
|
|
1811
1842
|
|
|
1843
|
+
/** The full candidate list — reactable pills AND the text-only overflow —
|
|
1844
|
+
* empty when the question was asked free-text. */
|
|
1845
|
+
const candidates = [...reactable, ...ctx.overflow];
|
|
1846
|
+
|
|
1812
1847
|
function answerFromMessages(messages: Record<string, Json>[], before = Infinity): AskFound | null {
|
|
1813
1848
|
// Oldest first: if someone wrote twice, the first reply is the answer.
|
|
1814
1849
|
const ordered = [...messages].sort((a, b) => Number(a.ts) - Number(b.ts));
|
|
@@ -1820,12 +1855,35 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
1820
1855
|
if (typeof m.subtype === "string" && !ASK_ANSWERABLE_SUBTYPES.has(m.subtype)) continue;
|
|
1821
1856
|
if (Number(m.ts) >= before) break;
|
|
1822
1857
|
const t = typeof m.text === "string" ? m.text.trim() : "";
|
|
1823
|
-
if (t)
|
|
1858
|
+
if (!t) continue;
|
|
1859
|
+
// A question asked WITHOUT choices is answered by whatever comes back —
|
|
1860
|
+
// there is nothing to match against, and every reply is the answer.
|
|
1861
|
+
if (!candidates.length) return { answer: t, how: "返信", who: m.user };
|
|
1862
|
+
const match = askMatchChoice(t, candidates);
|
|
1863
|
+
if (match.kind === "chosen") {
|
|
1864
|
+
// Answer with the CHOICE, not with the reply that selected it: "2" and
|
|
1865
|
+
// "2. 中止" have to reach the caller as the same decision a pill would
|
|
1866
|
+
// have produced, or the same answer arrives in three spellings.
|
|
1867
|
+
return { answer: candidates[match.index - 1]!, how: `返信 (${match.index})`, who: m.user };
|
|
1868
|
+
}
|
|
1869
|
+
// Replied, but picked nothing. Recorded rather than returned: a later
|
|
1870
|
+
// reply may still choose, and the first one is what the operator needs to
|
|
1871
|
+
// see — it is usually a question back.
|
|
1872
|
+
if (!unchosen || Number(m.ts) < Number(unchosen.ts)) {
|
|
1873
|
+
unchosen = { text: t, who: typeof m.user === "string" ? m.user : "", ts: String(m.ts), ambiguous: match.kind === "ambiguous" };
|
|
1874
|
+
}
|
|
1824
1875
|
}
|
|
1825
1876
|
return null;
|
|
1826
1877
|
}
|
|
1827
1878
|
|
|
1828
1879
|
async function poll(): Promise<AskFound | null> {
|
|
1880
|
+
// Recomputed every pass rather than accumulated. A reply that has since been
|
|
1881
|
+
// DELETED must stop counting: carrying it forward would let the timeout
|
|
1882
|
+
// report "somebody replied and chose nothing" about a message that is no
|
|
1883
|
+
// longer there, and exit 4 where exit 2 is the truth. `unchosenReported`
|
|
1884
|
+
// persists on purpose — it is what keeps the operator from being told the
|
|
1885
|
+
// same thing on every tick.
|
|
1886
|
+
unchosen = undefined;
|
|
1829
1887
|
// A question that is itself a thread reply is invisible to
|
|
1830
1888
|
// `conversations.history`, so the entire poll — reactions included — has to
|
|
1831
1889
|
// come from the thread.
|
|
@@ -1942,6 +2000,25 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
1942
2000
|
console.log(found.answer);
|
|
1943
2001
|
process.exit(0);
|
|
1944
2002
|
}
|
|
2003
|
+
// Said as soon as it is seen, not only at the timeout: the reply is usually
|
|
2004
|
+
// a question BACK, and the person who can unblock it is the one watching
|
|
2005
|
+
// this command. Once per reply — a poller that repeated itself every tick
|
|
2006
|
+
// would be noise nobody reads.
|
|
2007
|
+
if (unchosen && unchosen.ts !== unchosenReported) {
|
|
2008
|
+
unchosenReported = unchosen.ts;
|
|
2009
|
+
let who = unchosen.who;
|
|
2010
|
+
try {
|
|
2011
|
+
if (who) who = await userName(token, who, cookie);
|
|
2012
|
+
} catch {
|
|
2013
|
+
// the id is still a usable answer to "who"
|
|
2014
|
+
}
|
|
2015
|
+
console.error(
|
|
2016
|
+
unchosen.ambiguous
|
|
2017
|
+
? ` (${stripTerminalControls(who)} の返信は複数の選択肢に一致します。1 つに絞ってもらうまで待ちます)`
|
|
2018
|
+
: ` (${stripTerminalControls(who)} が返信しましたが、選択肢のどれでもありません。待機を続けます)`,
|
|
2019
|
+
);
|
|
2020
|
+
console.error(` 「${stripTerminalControls(unchosen.text)}」`);
|
|
2021
|
+
}
|
|
1945
2022
|
// Checked after the first poll, never before it: `--timeout 0` means one
|
|
1946
2023
|
// look, and one look must still be a real look.
|
|
1947
2024
|
if (Date.now() >= deadline) break;
|
|
@@ -1953,6 +2030,23 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
1953
2030
|
// question is still standing, and they have just decided not to block on it.
|
|
1954
2031
|
// Printing only the permalink here leaves them where `ask` used to: holding a
|
|
1955
2032
|
// link with nothing that reads a pressed pill back.
|
|
2033
|
+
// Three states, not two. "Nobody replied" and "somebody replied and chose
|
|
2034
|
+
// nothing" need different reactions from whoever reads this — the first waits,
|
|
2035
|
+
// the second answers a person — and collapsing them into exit 2 hides a human
|
|
2036
|
+
// standing there. stdout stays empty in both: the caller must not be handed
|
|
2037
|
+
// something to act on.
|
|
2038
|
+
if (unchosen) {
|
|
2039
|
+
console.error(
|
|
2040
|
+
unchosen.ambiguous
|
|
2041
|
+
? `Error: 返信はありましたが、複数の選択肢に一致するため確定できません (メッセージはそのまま残っています)`
|
|
2042
|
+
: `Error: 返信はありましたが、選択肢のどれも選ばれていません (メッセージはそのまま残っています)`,
|
|
2043
|
+
);
|
|
2044
|
+
console.error(` 返信: 「${stripTerminalControls(unchosen.text)}」`);
|
|
2045
|
+
console.error(` 選択肢: ${candidates.map((c, i) => `${i + 1}. ${stripTerminalControls(askFlatten(c))}`).join(" ")}`);
|
|
2046
|
+
console.error(` ${shown}`);
|
|
2047
|
+
console.error(` 返答してから回収する: ${askResumeCommand(shown, ctx.asBot)}`);
|
|
2048
|
+
process.exit(ASK_EXIT_UNCHOSEN);
|
|
2049
|
+
}
|
|
1956
2050
|
if (timeout === 0) {
|
|
1957
2051
|
console.error(`まだ回答がありません: ${shown}`);
|
|
1958
2052
|
} else {
|
|
@@ -2177,6 +2271,7 @@ async function cmdAsk(token: string, args: AskArgs): Promise<void> {
|
|
|
2177
2271
|
ts,
|
|
2178
2272
|
question,
|
|
2179
2273
|
reactable,
|
|
2274
|
+
overflow,
|
|
2180
2275
|
threadOnly,
|
|
2181
2276
|
audience,
|
|
2182
2277
|
broadcast,
|
|
@@ -2291,6 +2386,7 @@ async function cmdAskWaitFor(token: string, args: { link: string; timeout: numbe
|
|
|
2291
2386
|
ts,
|
|
2292
2387
|
question: parsed.question,
|
|
2293
2388
|
reactable: parsed.reactable,
|
|
2389
|
+
overflow: parsed.overflow,
|
|
2294
2390
|
threadOnly: parsed.threadOnly,
|
|
2295
2391
|
audience,
|
|
2296
2392
|
broadcast,
|
|
@@ -2725,6 +2821,11 @@ async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<v
|
|
|
2725
2821
|
const destOverride = args.targetOverride ? ` [${args.targetOverride} → ${channelId}]` : "";
|
|
2726
2822
|
|
|
2727
2823
|
if (args.code !== code) {
|
|
2824
|
+
// Quiet hours are judged on post_at, in the recipient's zone — the two facts
|
|
2825
|
+
// that decide whether a phone buzzes at 3am, and the two this gate was
|
|
2826
|
+
// guessing at. `send` already resolves the zone; here the time is known
|
|
2827
|
+
// exactly, which is the one command where it is.
|
|
2828
|
+
const recipientTz = await recipientTzFor(token, channelId, args.cookie);
|
|
2728
2829
|
requireCode(args.code, code, [
|
|
2729
2830
|
`--- Scheduling message -----------------------`,
|
|
2730
2831
|
fromLine(self, { asBot: args.asBot, pad: 9 }),
|
|
@@ -2732,7 +2833,7 @@ async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<v
|
|
|
2732
2833
|
` At: ${fmtPostAt(postAt)}`,
|
|
2733
2834
|
` Message: ${args.message}`,
|
|
2734
2835
|
`---------------------------------------------`,
|
|
2735
|
-
]);
|
|
2836
|
+
], recipientTz, new Date(postAt * 1000));
|
|
2736
2837
|
}
|
|
2737
2838
|
const id = await scheduleMessage(token, channelId, args.message, postAt, threadTs, args.cookie);
|
|
2738
2839
|
console.log(`✓ Scheduled (id: ${id}, at: ${postAtDate})`);
|
|
@@ -3542,7 +3643,7 @@ async function main(): Promise<void> {
|
|
|
3542
3643
|
.positional("choices", { type: "string", array: true, describe: "Up to 10 choices, seeded as 1️⃣..🔟 reactions. Beyond 10 they are listed but answerable only by text. With none, the question asks for a free-text reply." })
|
|
3543
3644
|
.option("code", { type: "string", describe: "Safety hash to confirm the ask" })
|
|
3544
3645
|
.option("body", { type: "string", describe: "Extra context shown under the question" })
|
|
3545
|
-
.option("wait", { type: "boolean", default: false, describe: "Block until answered; print ONLY the answer on stdout. Exit 0 = answered, 2 =
|
|
3646
|
+
.option("wait", { type: "boolean", default: false, describe: "Block until answered; print ONLY the answer on stdout. Exit 0 = answered, 2 = nobody replied, 3 = transport failure, 4 = somebody replied but picked none of the choices (stdout empty — do not act on it)." })
|
|
3546
3647
|
.option("waitFor", { type: "string", describe: "Collect the answer to a question already posted: pass its permalink. Nothing is posted. Same stdout/exit contract as --wait; --timeout 0 checks once and exits 2 if still open." })
|
|
3547
3648
|
.option("timeout", { type: "number", default: 3600, describe: "Overall limit for --wait / --waitFor, in seconds (0 with --waitFor = check once)" })
|
|
3548
3649
|
.option("channel-id", { type: "string", describe: "Raw channel ID" })
|