slack-term 1.35.0 → 1.37.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 +35 -3
- package/SKILL.md +27 -9
- package/dist/cli.js +277 -141
- package/package.json +1 -1
- package/ts/ask.ts +14 -3
- package/ts/auth.ts +383 -54
- package/ts/chromeCookieWin.ts +230 -0
- package/ts/cli.ts +252 -42
- package/ts/leveldb.ts +154 -0
- package/ts/profiles.ts +73 -19
- package/ts/sentlog.ts +356 -0
- package/ts/slack-app.ts +287 -185
- package/ts/slack.ts +31 -5
package/ts/cli.ts
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
// Slack CLI entry — mirrors the Rust impl in src/main.rs.
|
|
3
3
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, readFileSync, mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
8
|
|
|
9
9
|
import yargs, { type Options } from "yargs";
|
|
10
10
|
import { hideBin } from "yargs/helpers";
|
|
11
11
|
import { guardUrlBoundaries } from "./urlGuard.ts";
|
|
12
12
|
import { listProfiles, removeProfile, resolveBotToken, resolveCookie, resolveToken, useProfile, type Profile } from "./profiles.ts";
|
|
13
13
|
import { diagnoseBotMessaging, formatDiagnosis } from "./botdoctor.ts";
|
|
14
|
-
import { cmdAuthLogin, cmdAuthChrome, cmdAuthFirefox, cmdAuthToken, cmdAuthApp } from "./auth.ts";
|
|
14
|
+
import { cmdAuthLogin, cmdAuthLoginChrome, cmdAuthChrome, cmdAuthFirefox, cmdAuthToken, cmdAuthApp, cmdAuthSave, cmdAuthTokens } from "./auth.ts";
|
|
15
15
|
import { cmdTail } from "./tail.ts";
|
|
16
16
|
import { agentCommands } from "./agent.ts";
|
|
17
17
|
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
pollTally,
|
|
43
43
|
} from "./poll.ts";
|
|
44
44
|
import { seedReactionsInOrder } from "./reactionSeed.ts";
|
|
45
|
+
import { attributionEnabled, attributionMetadata, captureAttribution, parseSince, querySent, recordSent, SENT_LOG_UNAVAILABLE, sentDbPath, type RecordSent, type SentKind, type SentRow } from "./sentlog.ts";
|
|
45
46
|
import {
|
|
46
47
|
authTest,
|
|
47
48
|
authScopes,
|
|
@@ -75,6 +76,7 @@ import {
|
|
|
75
76
|
search,
|
|
76
77
|
searchAll,
|
|
77
78
|
send as slackSend,
|
|
79
|
+
type MessageMetadata,
|
|
78
80
|
getPermalink,
|
|
79
81
|
scheduleMessage,
|
|
80
82
|
listScheduledMessages,
|
|
@@ -463,8 +465,8 @@ async function cmdNews(token: string, limit: number): Promise<void> {
|
|
|
463
465
|
}
|
|
464
466
|
|
|
465
467
|
// --- channels ---
|
|
466
|
-
async function cmdChannels(token: string, limit: number, filter?: string, all?: boolean, format = "text"): Promise<void> {
|
|
467
|
-
const resp = (await listConversations(token)) as Record<string, Json>;
|
|
468
|
+
async function cmdChannels(token: string, limit: number, filter?: string, all?: boolean, format = "text", cookie?: string): Promise<void> {
|
|
469
|
+
const resp = (await listConversations(token, cookie)) as Record<string, Json>;
|
|
468
470
|
const channels = asArray(resp.channels)
|
|
469
471
|
.map(asRecord)
|
|
470
472
|
.filter((c) => all || c.is_member === true)
|
|
@@ -1068,6 +1070,38 @@ function selfLookup(token: string, cookie?: string): () => Promise<Self | null>
|
|
|
1068
1070
|
};
|
|
1069
1071
|
}
|
|
1070
1072
|
|
|
1073
|
+
/** Who is sending, captured once per write: the Slack `metadata` to attach to
|
|
1074
|
+
* the post, and `record` to append it to the local log once the ts is known.
|
|
1075
|
+
* Both vanish under SLACK_TERM_ATTRIBUTION=off. */
|
|
1076
|
+
function sentAttribution(kind: SentKind): {
|
|
1077
|
+
metadata?: MessageMetadata;
|
|
1078
|
+
record: (r: Omit<RecordSent, "kind" | "attribution">) => void;
|
|
1079
|
+
} {
|
|
1080
|
+
if (!attributionEnabled()) return { record: () => {} };
|
|
1081
|
+
const attribution = captureAttribution();
|
|
1082
|
+
return {
|
|
1083
|
+
metadata: attributionMetadata(kind, attribution),
|
|
1084
|
+
record: (r) => { recordSent({ ...r, kind, attribution }); },
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/** One `slack sent` row: when/what/where, then who — the sender block is the
|
|
1089
|
+
* point of the command, so it gets its own line rather than a trailing column. */
|
|
1090
|
+
function formatSentRow(r: SentRow): string {
|
|
1091
|
+
const first = stripTerminalControls(r.text.split("\n")[0] ?? "").slice(0, 100);
|
|
1092
|
+
const where = r.permalink || `${r.channel}:${r.ts}`;
|
|
1093
|
+
const who = [
|
|
1094
|
+
r.cli ?? "?",
|
|
1095
|
+
r.session_id ? `session=${r.session_id}` : "",
|
|
1096
|
+
r.agent_pid ? `pid=${r.agent_pid}` : `pid=${r.pid}`,
|
|
1097
|
+
r.git_branch ? `branch=${r.git_branch}` : "",
|
|
1098
|
+
`cwd=${r.cwd}`,
|
|
1099
|
+
].filter(Boolean).join(" ");
|
|
1100
|
+
return `${formatYmdHm(r.sent_at / 1000)} ${r.kind.padEnd(4)} ${stripTerminalControls(r.target ?? r.channel)} ${where}\n` +
|
|
1101
|
+
` ${first}\n` +
|
|
1102
|
+
` ${stripTerminalControls(who)}`;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1071
1105
|
/** Name the identity a write will be performed AS, for confirm gates:
|
|
1072
1106
|
* "@snomiao (U0123ABC) — Acme". Slack shows only the author on the resulting
|
|
1073
1107
|
* message, so a wrong profile / stale SLACK_TOKEN / unintended --as-bot is
|
|
@@ -1275,8 +1309,10 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
|
1275
1309
|
]);
|
|
1276
1310
|
}
|
|
1277
1311
|
|
|
1278
|
-
const
|
|
1312
|
+
const attr = sentAttribution("edit");
|
|
1313
|
+
const newTs = await editMessage(token, channelId, ts, newText, args.cookie, undefined, attr.metadata);
|
|
1279
1314
|
console.log(`✓ Edited (ts: ${newTs})`);
|
|
1315
|
+
attr.record({ team: self?.team, channel: channelId, target: args.target, ts, text: newText, asBot: args.asBot });
|
|
1280
1316
|
}
|
|
1281
1317
|
|
|
1282
1318
|
// --- delete ---
|
|
@@ -1657,7 +1693,8 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1657
1693
|
], recipientTz);
|
|
1658
1694
|
}
|
|
1659
1695
|
}
|
|
1660
|
-
const
|
|
1696
|
+
const attr = sentAttribution("send");
|
|
1697
|
+
const ts = await slackSend(token, channelId, message, threadTs, args.broadcast, cookie, undefined, attr.metadata);
|
|
1661
1698
|
let permalink = "";
|
|
1662
1699
|
try {
|
|
1663
1700
|
permalink = await getPermalink(token, channelId, ts, cookie);
|
|
@@ -1665,6 +1702,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1665
1702
|
// fail-soft: getPermalink failure (rate limit, network, etc.) should not
|
|
1666
1703
|
// mask send success — fall back to ts-only output below.
|
|
1667
1704
|
}
|
|
1705
|
+
attr.record({ team: (await getSelf())?.team, channel: channelId, target: args.target, ts, threadTs, permalink, text: message, asBot: args.asBot });
|
|
1668
1706
|
if (permalink) {
|
|
1669
1707
|
console.log(`✓ Sent: ${permalink}`);
|
|
1670
1708
|
} else {
|
|
@@ -1709,7 +1747,10 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1709
1747
|
// 0 answered (the answer text is on stdout, and nothing else is)
|
|
1710
1748
|
// 2 timed out (nobody replied)
|
|
1711
1749
|
// 3 transport/config failure
|
|
1712
|
-
// 4 replied, but
|
|
1750
|
+
// 4 replied, but the reply matches SEVERAL choices — stdout stays EMPTY
|
|
1751
|
+
// 5 replied in free text, picking none of the offered choices — the reply
|
|
1752
|
+
// text is on stdout. NOT a decision: the question is left open, so a pill
|
|
1753
|
+
// pressed later still settles it (`--waitFor … --after=<reply ts>`).
|
|
1713
1754
|
// Everything human-facing goes to stderr, so `ANS=$(slack ask ... --wait)` is safe.
|
|
1714
1755
|
|
|
1715
1756
|
const ASK_EXIT_TIMEOUT = 2;
|
|
@@ -1719,6 +1760,12 @@ const ASK_EXIT_ERROR = 3;
|
|
|
1719
1760
|
* automated caller act on a decision nobody took, and exit 2 ("nobody
|
|
1720
1761
|
* answered") would hide that a human is standing there waiting for something. */
|
|
1721
1762
|
const ASK_EXIT_UNCHOSEN = 4;
|
|
1763
|
+
/** A free-text reply that picks none of the choices, DELIVERED: the text is on
|
|
1764
|
+
* stdout. Exit 4 with stdout empty used to be the answer here, and the
|
|
1765
|
+
* instruction the human typed never reached the agent — it kept waiting
|
|
1766
|
+
* (measured 2026-09-27: 8 of 71 asks answered this way, none collected). Still
|
|
1767
|
+
* not 0, so nothing mistakes it for a decision. */
|
|
1768
|
+
const ASK_EXIT_FREETEXT = 5;
|
|
1722
1769
|
|
|
1723
1770
|
const ASK_PHI = 1.618033988749895;
|
|
1724
1771
|
const ASK_POLL_MIN_MS = 1000;
|
|
@@ -1823,6 +1870,9 @@ interface AskWaitCtx {
|
|
|
1823
1870
|
/** Only so the resume hint printed on timeout names the right identity — the
|
|
1824
1871
|
* ✅ rewrite can be done by the original poster alone. */
|
|
1825
1872
|
asBot: boolean;
|
|
1873
|
+
/** Text replies at or before this ts are ignored — the free-text reply a
|
|
1874
|
+
* previous exit 5 already delivered. Reactions still count. */
|
|
1875
|
+
after?: string;
|
|
1826
1876
|
}
|
|
1827
1877
|
|
|
1828
1878
|
/** Poll until answered. Never returns: exits 0 with the answer on stdout, or 2
|
|
@@ -1916,6 +1966,7 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
1916
1966
|
// Strictly after the question. A question posted INTO an existing thread
|
|
1917
1967
|
// sits among replies that predate it, and those answered something else.
|
|
1918
1968
|
if (!(Number(m.ts) > Number(ts))) continue;
|
|
1969
|
+
if (ctx.after && !(Number(m.ts) > Number(ctx.after))) continue;
|
|
1919
1970
|
if (!isAnswerer(m.user)) continue;
|
|
1920
1971
|
if (typeof m.subtype === "string" && !ASK_ANSWERABLE_SUBTYPES.has(m.subtype)) continue;
|
|
1921
1972
|
if (Number(m.ts) >= before) break;
|
|
@@ -1934,8 +1985,14 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
1934
1985
|
// Replied, but picked nothing. Recorded rather than returned: a later
|
|
1935
1986
|
// reply may still choose, and the first one is what the operator needs to
|
|
1936
1987
|
// see — it is usually a question back.
|
|
1937
|
-
|
|
1938
|
-
|
|
1988
|
+
// A plain free-text reply outranks an ambiguous one, whatever the order:
|
|
1989
|
+
// the free text is delivered (exit 5), and an earlier ambiguous attempt
|
|
1990
|
+
// must not hide it behind the refuse-to-guess path. Within a class, the
|
|
1991
|
+
// earliest wins.
|
|
1992
|
+
const ambiguous = match.kind === "ambiguous";
|
|
1993
|
+
if (!unchosen || (unchosen.ambiguous && !ambiguous) ||
|
|
1994
|
+
(unchosen.ambiguous === ambiguous && Number(m.ts) < Number(unchosen.ts))) {
|
|
1995
|
+
unchosen = { text: t, who: typeof m.user === "string" ? m.user : "", ts: String(m.ts), ambiguous };
|
|
1939
1996
|
}
|
|
1940
1997
|
}
|
|
1941
1998
|
return null;
|
|
@@ -2065,6 +2122,25 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
2065
2122
|
console.log(found.answer);
|
|
2066
2123
|
process.exit(0);
|
|
2067
2124
|
}
|
|
2125
|
+
// A free-text reply that picks nothing is delivered NOW, not held until the
|
|
2126
|
+
// deadline: it is usually an instruction or a question back, and the only
|
|
2127
|
+
// one who can act on it is the caller. Left unstamped — a pill pressed
|
|
2128
|
+
// later is still the decision. An AMBIGUOUS reply did try to choose, so it
|
|
2129
|
+
// keeps the refuse-to-guess path below.
|
|
2130
|
+
if (unchosen && !unchosen.ambiguous) {
|
|
2131
|
+
let who = unchosen.who;
|
|
2132
|
+
try {
|
|
2133
|
+
if (who) who = await userName(token, who, cookie);
|
|
2134
|
+
} catch {
|
|
2135
|
+
// the id is still a usable answer to "who"
|
|
2136
|
+
}
|
|
2137
|
+
console.error(` (${stripTerminalControls(who)} が選択肢外の返信をしました — 決定ではありません。本文を stdout に出します)`);
|
|
2138
|
+
console.error(` 選択肢: ${candidates.map((c, i) => `${i + 1}. ${stripTerminalControls(askFlatten(c))}`).join(" ")}`);
|
|
2139
|
+
console.error(` ${shown}`);
|
|
2140
|
+
console.error(` この返信の後を待つ: ${askResumeCommand(shown, ctx.asBot, unchosen.ts)}`);
|
|
2141
|
+
console.log(unchosen.text);
|
|
2142
|
+
process.exit(ASK_EXIT_FREETEXT);
|
|
2143
|
+
}
|
|
2068
2144
|
// Said as soon as it is seen, not only at the timeout: the reply is usually
|
|
2069
2145
|
// a question BACK, and the person who can unblock it is the one watching
|
|
2070
2146
|
// this command. Once per reply — a poller that repeated itself every tick
|
|
@@ -2077,11 +2153,8 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
2077
2153
|
} catch {
|
|
2078
2154
|
// the id is still a usable answer to "who"
|
|
2079
2155
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
? ` (${stripTerminalControls(who)} の返信は複数の選択肢に一致します。1 つに絞ってもらうまで待ちます)`
|
|
2083
|
-
: ` (${stripTerminalControls(who)} が返信しましたが、選択肢のどれでもありません。待機を続けます)`,
|
|
2084
|
-
);
|
|
2156
|
+
// Only the ambiguous case gets here — a plain free-text reply exited above.
|
|
2157
|
+
console.error(` (${stripTerminalControls(who)} の返信は複数の選択肢に一致します。1 つに絞ってもらうまで待ちます)`);
|
|
2085
2158
|
console.error(` 「${stripTerminalControls(unchosen.text)}」`);
|
|
2086
2159
|
}
|
|
2087
2160
|
// Checked after the first poll, never before it: `--timeout 0` means one
|
|
@@ -2101,15 +2174,12 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
2101
2174
|
// standing there. stdout stays empty in both: the caller must not be handed
|
|
2102
2175
|
// something to act on.
|
|
2103
2176
|
if (unchosen) {
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
? `Error: 返信はありましたが、複数の選択肢に一致するため確定できません (メッセージはそのまま残っています)`
|
|
2107
|
-
: `Error: 返信はありましたが、選択肢のどれも選ばれていません (メッセージはそのまま残っています)`,
|
|
2108
|
-
);
|
|
2177
|
+
// Ambiguous only: a plain free-text reply was delivered as exit 5 already.
|
|
2178
|
+
console.error(`Error: 返信はありましたが、複数の選択肢に一致するため確定できません (メッセージはそのまま残っています)`);
|
|
2109
2179
|
console.error(` 返信: 「${stripTerminalControls(unchosen.text)}」`);
|
|
2110
2180
|
console.error(` 選択肢: ${candidates.map((c, i) => `${i + 1}. ${stripTerminalControls(askFlatten(c))}`).join(" ")}`);
|
|
2111
2181
|
console.error(` ${shown}`);
|
|
2112
|
-
console.error(` 返答してから回収する: ${askResumeCommand(shown, ctx.asBot)}`);
|
|
2182
|
+
console.error(` 返答してから回収する: ${askResumeCommand(shown, ctx.asBot, ctx.after)}`);
|
|
2113
2183
|
process.exit(ASK_EXIT_UNCHOSEN);
|
|
2114
2184
|
}
|
|
2115
2185
|
if (timeout === 0) {
|
|
@@ -2117,7 +2187,7 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
2117
2187
|
} else {
|
|
2118
2188
|
console.error(`Error: ${timeout}s 以内に回答がありませんでした (メッセージはそのまま残っています)`);
|
|
2119
2189
|
console.error(` ${shown}`);
|
|
2120
|
-
console.error(` あとで回収する: ${askResumeCommand(shown, ctx.asBot)}`);
|
|
2190
|
+
console.error(` あとで回収する: ${askResumeCommand(shown, ctx.asBot, ctx.after)}`);
|
|
2121
2191
|
}
|
|
2122
2192
|
process.exit(ASK_EXIT_TIMEOUT);
|
|
2123
2193
|
}
|
|
@@ -2125,8 +2195,10 @@ async function askWaitForAnswer(token: string, ctx: AskWaitCtx): Promise<never>
|
|
|
2125
2195
|
/** The `slack ask --waitFor=…` line handed to a caller that did not block.
|
|
2126
2196
|
* Printed as a runnable command rather than a bare permalink: the collect step
|
|
2127
2197
|
* is the half everyone forgets, and a link does not tell you how to collect. */
|
|
2128
|
-
|
|
2129
|
-
|
|
2198
|
+
/** `after` is carried into EVERY resume hint once set: dropping it would have
|
|
2199
|
+
* the next `--waitFor` re-deliver a free-text reply already acted on. */
|
|
2200
|
+
function askResumeCommand(shown: string, asBot: boolean, after?: string): string {
|
|
2201
|
+
return `slack ask --waitFor='${shown}'${asBot ? " --as-bot" : ""}${after ? ` --after=${after}` : ""}`;
|
|
2130
2202
|
}
|
|
2131
2203
|
|
|
2132
2204
|
async function cmdAsk(token: string, args: AskArgs): Promise<void> {
|
|
@@ -2283,7 +2355,8 @@ async function cmdAsk(token: string, args: AskArgs): Promise<void> {
|
|
|
2283
2355
|
// `plain`: with blocks attached Slack rewrites the stored text (newlines to
|
|
2284
2356
|
// spaces, emoji to `:one:`) and `--waitFor` can no longer read the question
|
|
2285
2357
|
// back out of it — which is the entire recovery path.
|
|
2286
|
-
const
|
|
2358
|
+
const attr = sentAttribution("ask");
|
|
2359
|
+
const ts = await slackSend(token, channelId, message, threadTs, false, cookie, true, attr.metadata);
|
|
2287
2360
|
|
|
2288
2361
|
// The marker goes on FIRST so it sits left of the pills, and as a reaction so
|
|
2289
2362
|
// `has::question:` lists every question the way `has::pushpin:` lists every
|
|
@@ -2318,6 +2391,7 @@ async function cmdAsk(token: string, args: AskArgs): Promise<void> {
|
|
|
2318
2391
|
}
|
|
2319
2392
|
const shown = permalink || `${channelId}:${ts}`;
|
|
2320
2393
|
console.error(`✓ Asked: ${shown}`);
|
|
2394
|
+
attr.record({ team: self?.team, channel: channelId, target: args.target, ts, threadTs, permalink, text: message, asBot: args.asBot });
|
|
2321
2395
|
|
|
2322
2396
|
if (!args.wait) {
|
|
2323
2397
|
// Not waiting. stdout is the RESUME COMMAND, not just a link: nothing else
|
|
@@ -2362,7 +2436,7 @@ async function cmdAsk(token: string, args: AskArgs): Promise<void> {
|
|
|
2362
2436
|
* file to go stale, lose, or disagree with Slack, and any machine holding the
|
|
2363
2437
|
* permalink can collect — at the cost of the body being a parseable format
|
|
2364
2438
|
* (`askBuildText` ⇄ `askParseMessage`). */
|
|
2365
|
-
async function cmdAskWaitFor(token: string, args: { link: string; timeout: number; asBot: boolean; cookie?: string }): Promise<void> {
|
|
2439
|
+
async function cmdAskWaitFor(token: string, args: { link: string; timeout: number; asBot: boolean; cookie?: string; after?: string }): Promise<void> {
|
|
2366
2440
|
const url = parseSlackPermalink(args.link);
|
|
2367
2441
|
let channelId: string;
|
|
2368
2442
|
let ts: string;
|
|
@@ -2464,6 +2538,7 @@ async function cmdAskWaitFor(token: string, args: { link: string; timeout: numbe
|
|
|
2464
2538
|
};
|
|
2465
2539
|
if (threadTs) ctx.threadParentTs = threadTs;
|
|
2466
2540
|
if (args.cookie) ctx.cookie = args.cookie;
|
|
2541
|
+
if (args.after) ctx.after = args.after;
|
|
2467
2542
|
await askWaitForAnswer(token, ctx);
|
|
2468
2543
|
}
|
|
2469
2544
|
|
|
@@ -2606,7 +2681,8 @@ async function cmdPoll(token: string, args: PollArgs): Promise<void> {
|
|
|
2606
2681
|
|
|
2607
2682
|
// `plain`: no blocks, or Slack rewrites the stored text and the ballot
|
|
2608
2683
|
// stops parsing on the way back.
|
|
2609
|
-
const
|
|
2684
|
+
const attr = sentAttribution("poll");
|
|
2685
|
+
const ts = await slackSend(token, channelId, message, threadTs, false, cookie, true, attr.metadata);
|
|
2610
2686
|
|
|
2611
2687
|
// Marker first, for the same reason as `ask`: it is what makes
|
|
2612
2688
|
// `has::ballot_box_with_ballot:` list every poll. An unrelated reaction on the message is
|
|
@@ -2634,6 +2710,7 @@ async function cmdPoll(token: string, args: PollArgs): Promise<void> {
|
|
|
2634
2710
|
}
|
|
2635
2711
|
const shown = permalink || `${channelId}:${ts}`;
|
|
2636
2712
|
console.error(`✓ Posted: ${shown}`);
|
|
2713
|
+
attr.record({ team: self?.team, channel: channelId, target: args.target, ts, threadTs, permalink, text: message, asBot: args.asBot });
|
|
2637
2714
|
console.log(pollResultsCommand(shown));
|
|
2638
2715
|
console.error(` 締め切る: slack poll --close='${shown}'${args.asBot ? " --as-bot" : ""}`);
|
|
2639
2716
|
}
|
|
@@ -3284,7 +3361,8 @@ async function main(): Promise<void> {
|
|
|
3284
3361
|
.option("workspace", { alias: "w", type: "string", describe: "Workspace name" })
|
|
3285
3362
|
.middleware(async (argv) => {
|
|
3286
3363
|
const cmd = String((argv._ ?? [])[0] ?? "");
|
|
3287
|
-
|
|
3364
|
+
// `sent` reads only the local log — no token, no network.
|
|
3365
|
+
if (!cmd || cmd === "auth" || cmd === "login" || cmd === "agent" || cmd === "sent") return;
|
|
3288
3366
|
try {
|
|
3289
3367
|
resolveToken((argv as W).workspace);
|
|
3290
3368
|
} catch (e) {
|
|
@@ -3342,7 +3420,7 @@ async function main(): Promise<void> {
|
|
|
3342
3420
|
.option("format", { type: "string", choices: ["text", "jsonl"] as const, default: "text" })
|
|
3343
3421
|
.option("json", { type: "boolean", default: false, describe: "Alias for --format=jsonl" }),
|
|
3344
3422
|
async (argv) => {
|
|
3345
|
-
await cmdChannels(tok(argv as W), argv.limit, argv.filter, argv.all, argv.json ? "jsonl" : argv.format);
|
|
3423
|
+
await cmdChannels(tok(argv as W), argv.limit, argv.filter, argv.all, argv.json ? "jsonl" : argv.format, ck(argv as W));
|
|
3346
3424
|
},
|
|
3347
3425
|
)
|
|
3348
3426
|
.command(
|
|
@@ -3712,8 +3790,9 @@ async function main(): Promise<void> {
|
|
|
3712
3790
|
.option("code", { type: "string", describe: "Safety hash to confirm the ask" })
|
|
3713
3791
|
.option("allow-url-adjacent", { type: "boolean", default: false, describe: "Warn instead of refusing ambiguous bare URL boundaries" })
|
|
3714
3792
|
.option("body", { type: "string", describe: "Extra context shown under the question" })
|
|
3715
|
-
.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 =
|
|
3793
|
+
.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 = a reply matched several choices (stdout empty), 5 = a free-text reply that picked none of the choices (the reply is on stdout; NOT a decision — the question stays open)." })
|
|
3716
3794
|
.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." })
|
|
3795
|
+
.option("after", { type: "string", describe: "With --waitFor: ignore text replies at or before this ts (the reply an exit 5 already delivered). Reactions still count." })
|
|
3717
3796
|
.option("timeout", { type: "number", default: 3600, describe: "Overall limit for --wait / --waitFor, in seconds (0 with --waitFor = check once)" })
|
|
3718
3797
|
.option("channel-id", { type: "string", describe: "Raw channel ID" })
|
|
3719
3798
|
.option("user-id", { type: "string", describe: "Raw user ID (opens DM)" })
|
|
@@ -3752,8 +3831,16 @@ async function main(): Promise<void> {
|
|
|
3752
3831
|
waitCookie = ck(argv as W);
|
|
3753
3832
|
}
|
|
3754
3833
|
try {
|
|
3755
|
-
const a: { link: string; timeout: number; asBot: boolean; cookie?: string } = { link: waitFor, timeout, asBot: !!argv["as-bot"] };
|
|
3834
|
+
const a: { link: string; timeout: number; asBot: boolean; cookie?: string; after?: string } = { link: waitFor, timeout, asBot: !!argv["as-bot"] };
|
|
3756
3835
|
if (waitCookie) a.cookie = waitCookie;
|
|
3836
|
+
if (argv.after) {
|
|
3837
|
+
const after = String(argv.after);
|
|
3838
|
+
if (!/^\d{10}\.\d{6}$/.test(after)) {
|
|
3839
|
+
console.error(`Error: --after needs a message ts like 1700000000.000100 (got ${stripTerminalControls(after)})`);
|
|
3840
|
+
process.exit(ASK_EXIT_ERROR);
|
|
3841
|
+
}
|
|
3842
|
+
a.after = after;
|
|
3843
|
+
}
|
|
3757
3844
|
await cmdAskWaitFor(waitToken, a);
|
|
3758
3845
|
} catch (e: unknown) {
|
|
3759
3846
|
console.error(friendlySlackError(e));
|
|
@@ -3951,6 +4038,56 @@ async function main(): Promise<void> {
|
|
|
3951
4038
|
}
|
|
3952
4039
|
},
|
|
3953
4040
|
)
|
|
4041
|
+
.command(
|
|
4042
|
+
"sent [query]",
|
|
4043
|
+
"Search what THIS machine posted/edited (send, ask, poll, edit) and who sent it — read-only, local log",
|
|
4044
|
+
(y) => y
|
|
4045
|
+
.positional("query", { type: "string", describe: "Substring of the message text (case-insensitive)" })
|
|
4046
|
+
.option("channel", { type: "string", describe: "Channel/DM id (C…/D…) or the target as typed (#eng, @bob)" })
|
|
4047
|
+
.option("since", { type: "string", describe: "Only newer than this: 30m, 2h, 7d, 1w, or an ISO date" })
|
|
4048
|
+
.option("session", { type: "string", describe: "Agent session id (prefix match)" })
|
|
4049
|
+
.option("cwd", { type: "string", describe: "Working directory the sender ran in (prefix match)" })
|
|
4050
|
+
.option("kind", { type: "string", choices: ["send", "ask", "poll", "edit"], describe: "Only this kind of write" })
|
|
4051
|
+
.option("json", { type: "boolean", default: false, describe: "One JSON object per line (every recorded field)" })
|
|
4052
|
+
.option("count", { alias: "n", type: "number", default: 50, describe: "Max rows (newest first)" }),
|
|
4053
|
+
(argv) => {
|
|
4054
|
+
let sinceMs: number | undefined;
|
|
4055
|
+
if (argv.since !== undefined) {
|
|
4056
|
+
sinceMs = parseSince(String(argv.since));
|
|
4057
|
+
if (sinceMs === undefined) {
|
|
4058
|
+
console.error(`Error: --since needs 30m / 2h / 7d / 1w or an ISO date (got ${stripTerminalControls(String(argv.since))})`);
|
|
4059
|
+
process.exit(1);
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
const q: Parameters<typeof querySent>[0] = { limit: Number(argv.count) || 50 };
|
|
4063
|
+
if (argv.query) q.text = String(argv.query);
|
|
4064
|
+
if (argv.channel) q.channel = String(argv.channel);
|
|
4065
|
+
if (sinceMs !== undefined) q.sinceMs = sinceMs;
|
|
4066
|
+
if (argv.session) q.session = String(argv.session);
|
|
4067
|
+
if (argv.cwd) {
|
|
4068
|
+
// Resolved like the recorded cwd was (process.cwd() is a realpath), so
|
|
4069
|
+
// `--cwd .` works and a symlinked path (macOS /var → /private/var)
|
|
4070
|
+
// still matches. A path that no longer exists is used as typed.
|
|
4071
|
+
const raw = resolve(String(argv.cwd));
|
|
4072
|
+
try { q.cwd = realpathSync(raw); } catch { q.cwd = raw; }
|
|
4073
|
+
}
|
|
4074
|
+
if (argv.kind) q.kind = argv.kind as SentKind;
|
|
4075
|
+
const rows = querySent(q);
|
|
4076
|
+
if (!rows) {
|
|
4077
|
+
console.error(`Error: ${SENT_LOG_UNAVAILABLE}\n (${sentDbPath()})`);
|
|
4078
|
+
process.exit(1);
|
|
4079
|
+
}
|
|
4080
|
+
if (argv.json) {
|
|
4081
|
+
for (const r of rows) console.log(JSON.stringify(r));
|
|
4082
|
+
return;
|
|
4083
|
+
}
|
|
4084
|
+
if (!rows.length) {
|
|
4085
|
+
console.error(`No sent messages match (log: ${sentDbPath()}).`);
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
for (const r of rows) console.log(formatSentRow(r));
|
|
4089
|
+
},
|
|
4090
|
+
)
|
|
3954
4091
|
.command(
|
|
3955
4092
|
"doctor",
|
|
3956
4093
|
"Check the bot token (xoxb) can carry a two-way DM (scopes + Messages Tab)",
|
|
@@ -4526,17 +4663,73 @@ async function main(): Promise<void> {
|
|
|
4526
4663
|
)
|
|
4527
4664
|
.command(
|
|
4528
4665
|
"login",
|
|
4529
|
-
"
|
|
4666
|
+
"Log in: interactive wizard, or a source (chrome, firefox)",
|
|
4530
4667
|
(y2) => y2
|
|
4668
|
+
.command(
|
|
4669
|
+
"chrome",
|
|
4670
|
+
"Import workspaces from Chrome, grouped by account (all platforms)",
|
|
4671
|
+
(y3) => y3
|
|
4672
|
+
.option("profile", { type: "string", alias: "p", describe: 'Chrome profile email or dir (e.g. "you@example.com" or "Profile 3")' }),
|
|
4673
|
+
async (argv) => {
|
|
4674
|
+
await cmdAuthLoginChrome({ ...(argv.profile !== undefined ? { profile: argv.profile } : {}) });
|
|
4675
|
+
},
|
|
4676
|
+
)
|
|
4677
|
+
.command(
|
|
4678
|
+
"firefox",
|
|
4679
|
+
"Import the xoxd cookie from Firefox into a workspace",
|
|
4680
|
+
(y3) => y3
|
|
4681
|
+
.option("workspace", { type: "string", alias: "w", describe: "Workspace name to update (default: active)" }),
|
|
4682
|
+
async (argv) => {
|
|
4683
|
+
await cmdAuthFirefox({ ...(argv.workspace !== undefined ? { workspace: argv.workspace } : {}) });
|
|
4684
|
+
},
|
|
4685
|
+
)
|
|
4531
4686
|
.option("token", { type: "string", describe: "Token to save directly (non-interactive)" })
|
|
4532
|
-
.option("name", { type: "string", describe: "Workspace name (used with --token)" })
|
|
4687
|
+
.option("name", { type: "string", describe: "Workspace name (used with --token)" })
|
|
4688
|
+
.option("yes", { type: "boolean", default: false, describe: "Allow reading browser profiles without a confirmation prompt" })
|
|
4689
|
+
.option("from-desktop", { type: "boolean", default: false, describe: "Import the Slack Desktop token without scanning a browser" })
|
|
4690
|
+
.option("from-chrome", { type: "boolean", default: false, describe: "Import the desktop token and Chrome cookie" })
|
|
4691
|
+
.option("from-firefox", { type: "boolean", default: false, describe: "Import the desktop token and Firefox cookie" })
|
|
4692
|
+
.option("from-all", { type: "boolean", default: false, describe: "Import the desktop token and scan Chrome and Firefox" })
|
|
4693
|
+
.command("$0", false as unknown as string, () => {}, async (argv) => {
|
|
4694
|
+
await cmdAuthLogin({
|
|
4695
|
+
...(argv.token !== undefined ? { token: argv.token as string } : {}),
|
|
4696
|
+
...(argv.name !== undefined ? { name: argv.name as string } : {}),
|
|
4697
|
+
yes: argv.yes as boolean,
|
|
4698
|
+
fromDesktop: argv.fromDesktop as boolean,
|
|
4699
|
+
fromChrome: argv.fromChrome as boolean,
|
|
4700
|
+
fromFirefox: argv.fromFirefox as boolean,
|
|
4701
|
+
fromAll: argv.fromAll as boolean,
|
|
4702
|
+
});
|
|
4703
|
+
}),
|
|
4533
4704
|
async (argv) => {
|
|
4534
4705
|
await cmdAuthLogin({
|
|
4535
|
-
...(argv.token !== undefined ? { token: argv.token } : {}),
|
|
4536
|
-
...(argv.name !== undefined ? { name: argv.name } : {}),
|
|
4706
|
+
...(argv.token !== undefined ? { token: argv.token as string } : {}),
|
|
4707
|
+
...(argv.name !== undefined ? { name: argv.name as string } : {}),
|
|
4708
|
+
yes: argv.yes as boolean,
|
|
4709
|
+
fromDesktop: argv.fromDesktop as boolean,
|
|
4710
|
+
fromChrome: argv.fromChrome as boolean,
|
|
4711
|
+
fromFirefox: argv.fromFirefox as boolean,
|
|
4712
|
+
fromAll: argv.fromAll as boolean,
|
|
4537
4713
|
});
|
|
4538
4714
|
},
|
|
4539
4715
|
)
|
|
4716
|
+
.command(
|
|
4717
|
+
"tokens",
|
|
4718
|
+
"Print the selected credentials as dotenv assignments",
|
|
4719
|
+
(y2) => y2.option("workspace", { type: "string", alias: "w", describe: "Workspace name (default: active)" }),
|
|
4720
|
+
(argv) => cmdAuthTokens({ ...(argv.workspace !== undefined ? { workspace: argv.workspace } : {}) }),
|
|
4721
|
+
)
|
|
4722
|
+
.command(
|
|
4723
|
+
"save",
|
|
4724
|
+
"Save the active profile token and cookie to an env file",
|
|
4725
|
+
(y2) => y2
|
|
4726
|
+
.option("envfile", { type: "string", demandOption: true, describe: "Destination env file (e.g. ./.env.local)" })
|
|
4727
|
+
.option("workspace", { type: "string", alias: "w", describe: "Workspace name (default: active)" }),
|
|
4728
|
+
(argv) => cmdAuthSave({
|
|
4729
|
+
envfile: argv.envfile!,
|
|
4730
|
+
...(argv.workspace !== undefined ? { workspace: argv.workspace } : {}),
|
|
4731
|
+
}),
|
|
4732
|
+
)
|
|
4540
4733
|
.command(["ls", "status"], "Show auth status", () => {}, () => {
|
|
4541
4734
|
const profiles = listProfiles();
|
|
4542
4735
|
if (profiles.length === 0) { console.log("No workspaces configured. Run: slack auth login"); return; }
|
|
@@ -4565,21 +4758,28 @@ async function main(): Promise<void> {
|
|
|
4565
4758
|
},
|
|
4566
4759
|
)
|
|
4567
4760
|
.command(
|
|
4568
|
-
["
|
|
4569
|
-
"Attach Chrome
|
|
4761
|
+
["cookie", "chrome"],
|
|
4762
|
+
"Attach a Chrome xoxd cookie to a workspace (macOS/Linux/Windows). To import workspaces, use: slack auth login chrome",
|
|
4570
4763
|
(y2) => y2
|
|
4571
|
-
.option("workspace", { type: "string", alias: "w", describe: "Workspace name to update (default: active)" })
|
|
4764
|
+
.option("workspace", { type: "string", alias: "w", describe: "Workspace name to update (default: active)" })
|
|
4765
|
+
.option("profile", { type: "string", alias: "p", describe: "Chrome profile email or dir (Windows, when several have a session)" })
|
|
4766
|
+
.option("yes", { type: "boolean", default: false, describe: "Allow reading browser profiles without a confirmation prompt" }),
|
|
4572
4767
|
async (argv) => {
|
|
4573
|
-
await cmdAuthChrome({
|
|
4768
|
+
await cmdAuthChrome({
|
|
4769
|
+
...(argv.workspace !== undefined ? { workspace: argv.workspace } : {}),
|
|
4770
|
+
...(argv.profile !== undefined ? { profile: argv.profile } : {}),
|
|
4771
|
+
yes: argv.yes as boolean,
|
|
4772
|
+
});
|
|
4574
4773
|
},
|
|
4575
4774
|
)
|
|
4576
4775
|
.command(
|
|
4577
4776
|
"firefox",
|
|
4578
4777
|
"Attach Firefox browser xoxd cookie to a workspace (all platforms)",
|
|
4579
4778
|
(y2) => y2
|
|
4580
|
-
.option("workspace", { type: "string", alias: "w", describe: "Workspace name to update (default: active)" })
|
|
4779
|
+
.option("workspace", { type: "string", alias: "w", describe: "Workspace name to update (default: active)" })
|
|
4780
|
+
.option("yes", { type: "boolean", default: false, describe: "Allow reading browser profiles without a confirmation prompt" }),
|
|
4581
4781
|
async (argv) => {
|
|
4582
|
-
await cmdAuthFirefox({ ...(argv.workspace !== undefined ? { workspace: argv.workspace } : {}) });
|
|
4782
|
+
await cmdAuthFirefox({ ...(argv.workspace !== undefined ? { workspace: argv.workspace } : {}), yes: argv.yes });
|
|
4583
4783
|
},
|
|
4584
4784
|
)
|
|
4585
4785
|
.command("$0", false as unknown as string, () => {}, () => { y.showHelp(); process.exit(0); }),
|
|
@@ -4617,10 +4817,20 @@ async function main(): Promise<void> {
|
|
|
4617
4817
|
)
|
|
4618
4818
|
.command("login", false as unknown as string, (y2) => y2
|
|
4619
4819
|
.option("token", { type: "string" })
|
|
4620
|
-
.option("name", { type: "string" })
|
|
4820
|
+
.option("name", { type: "string" })
|
|
4821
|
+
.option("yes", { type: "boolean", default: false })
|
|
4822
|
+
.option("from-desktop", { type: "boolean", default: false })
|
|
4823
|
+
.option("from-chrome", { type: "boolean", default: false })
|
|
4824
|
+
.option("from-firefox", { type: "boolean", default: false })
|
|
4825
|
+
.option("from-all", { type: "boolean", default: false }), async (argv) => {
|
|
4621
4826
|
await cmdAuthLogin({
|
|
4622
4827
|
...(argv.token !== undefined ? { token: argv.token } : {}),
|
|
4623
4828
|
...(argv.name !== undefined ? { name: argv.name } : {}),
|
|
4829
|
+
yes: argv.yes,
|
|
4830
|
+
fromDesktop: argv.fromDesktop,
|
|
4831
|
+
fromChrome: argv.fromChrome,
|
|
4832
|
+
fromFirefox: argv.fromFirefox,
|
|
4833
|
+
fromAll: argv.fromAll,
|
|
4624
4834
|
});
|
|
4625
4835
|
})
|
|
4626
4836
|
.demandCommand(1, "Specify a command. Run with --help for usage.")
|