slack-term 1.7.0 → 1.14.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 +37 -2
- package/dist/cli.js +132 -31
- package/package.json +12 -5
- package/ts/auth.ts +242 -0
- package/ts/cli.ts +780 -476
- package/ts/profiles.ts +17 -12
- package/ts/rtm.ts +180 -0
- package/ts/slack.ts +116 -8
- package/ts/tail.ts +279 -0
package/ts/cli.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Slack CLI entry — mirrors the Rust impl in src/main.rs.
|
|
3
3
|
|
|
4
|
-
import { parseArgs } from "node:util";
|
|
5
4
|
import { createHash } from "node:crypto";
|
|
6
5
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
7
6
|
import { homedir } from "node:os";
|
|
8
7
|
import { join } from "node:path";
|
|
9
8
|
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
9
|
+
import yargs from "yargs";
|
|
10
|
+
import { hideBin } from "yargs/helpers";
|
|
11
|
+
import { listProfiles, removeProfile, resolveCookie, resolveToken, useProfile } from "./profiles.ts";
|
|
12
|
+
import { cmdAuthLogin } from "./auth.ts";
|
|
13
|
+
import { cmdTail } from "./tail.ts";
|
|
12
14
|
|
|
13
15
|
import {
|
|
14
|
-
authTest,
|
|
15
16
|
authTestSession,
|
|
16
17
|
conversationInfoSession,
|
|
17
18
|
createDraft,
|
|
@@ -21,6 +22,9 @@ import {
|
|
|
21
22
|
history,
|
|
22
23
|
listConversations,
|
|
23
24
|
listDrafts,
|
|
25
|
+
listUsers,
|
|
26
|
+
userInfo,
|
|
27
|
+
conversationInfo,
|
|
24
28
|
openDm,
|
|
25
29
|
parseSlackPermalink,
|
|
26
30
|
replies,
|
|
@@ -28,13 +32,16 @@ import {
|
|
|
28
32
|
search,
|
|
29
33
|
searchAll,
|
|
30
34
|
send as slackSend,
|
|
35
|
+
scheduleMessage,
|
|
36
|
+
listScheduledMessages,
|
|
37
|
+
deleteScheduledMessage,
|
|
31
38
|
uploadFile,
|
|
32
39
|
userInfoPair,
|
|
33
40
|
userName,
|
|
34
41
|
getPath,
|
|
35
42
|
type Json,
|
|
36
43
|
} from "./slack.ts";
|
|
37
|
-
import { dayLabel,
|
|
44
|
+
import { dayLabel, formatYmdHm, resolveDateMarkup, resolveMentions } from "./format.ts";
|
|
38
45
|
|
|
39
46
|
function loadDotenv(path: string): void {
|
|
40
47
|
if (!existsSync(path)) return;
|
|
@@ -109,52 +116,87 @@ function formatYmdHmsUtc(epochSec: number): string {
|
|
|
109
116
|
return `${y}-${mo}-${da} ${h}:${mi}:${s}`;
|
|
110
117
|
}
|
|
111
118
|
|
|
112
|
-
//
|
|
119
|
+
// Lossless Slack ts → ISO string: "2026-05-11T06:01:04.000100"
|
|
120
|
+
function slackTsToIso(tsRaw: string): string {
|
|
121
|
+
const [secStr, fracStr = "000000"] = tsRaw.split(".");
|
|
122
|
+
const epochSec = Number(secStr);
|
|
123
|
+
const d = new Date(epochSec * 1000);
|
|
124
|
+
const y = d.getUTCFullYear();
|
|
125
|
+
const mo = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
126
|
+
const da = String(d.getUTCDate()).padStart(2, "0");
|
|
127
|
+
const h = String(d.getUTCHours()).padStart(2, "0");
|
|
128
|
+
const mi = String(d.getUTCMinutes()).padStart(2, "0");
|
|
129
|
+
const s = String(d.getUTCSeconds()).padStart(2, "0");
|
|
130
|
+
const frac = fracStr.padEnd(6, "0").slice(0, 6);
|
|
131
|
+
return `${y}-${mo}-${da}T${h}:${mi}:${s}.${frac}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Parse ISO ts back to Slack ts — throws if fractional is absent or not exactly 6 digits.
|
|
135
|
+
function isoToSlackTs(iso: string): string {
|
|
136
|
+
const m = iso.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\.(\d{6})$/);
|
|
137
|
+
if (!m) throw new Error(`Invalid message timestamp "${iso}" — expected 2026-05-11T06:01:04.000100`);
|
|
138
|
+
const epochMs = Date.parse(m[1] + "Z");
|
|
139
|
+
if (isNaN(epochMs)) throw new Error(`Cannot parse date in "${iso}"`);
|
|
140
|
+
return `${Math.floor(epochMs / 1000)}.${m[2]}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseInputTs(s: string): string {
|
|
144
|
+
return /^\d{4}-\d{2}-\d{2}T/.test(s) ? isoToSlackTs(s) : s;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Format one message line: `[ts] @handle text` (UTC). Pass chLabel (e.g. "#general") for search.
|
|
113
148
|
async function formatMsgLine(
|
|
114
149
|
token: string,
|
|
115
150
|
m: Record<string, Json>,
|
|
116
151
|
cache: Map<string, string>,
|
|
152
|
+
chLabel?: string,
|
|
117
153
|
): Promise<string> {
|
|
118
|
-
const
|
|
119
|
-
const stamp =
|
|
120
|
-
let real = "?";
|
|
154
|
+
const rawTs = typeof m.ts === "string" ? m.ts : `${tsNum(m)}.000000`;
|
|
155
|
+
const stamp = slackTsToIso(rawTs);
|
|
121
156
|
let handle = "?";
|
|
122
157
|
if (typeof m.user === "string") {
|
|
123
158
|
const uid = m.user;
|
|
124
|
-
const realKey = uid;
|
|
125
159
|
const handleKey = "@" + uid;
|
|
126
|
-
if (!cache.has(
|
|
127
|
-
const [
|
|
128
|
-
cache.set(realKey, d);
|
|
160
|
+
if (!cache.has(handleKey)) {
|
|
161
|
+
const [, h] = await userInfoPair(token, uid);
|
|
129
162
|
cache.set(handleKey, h);
|
|
130
163
|
}
|
|
131
|
-
real = cache.get(realKey) ?? uid;
|
|
132
164
|
handle = cache.get(handleKey) ?? uid;
|
|
133
165
|
} else if (typeof m.username === "string") {
|
|
134
|
-
real = m.username;
|
|
135
166
|
handle = m.username;
|
|
136
167
|
}
|
|
137
168
|
const raw = typeof m.text === "string" ? m.text : "";
|
|
138
169
|
const resolved = resolveDateMarkup(await resolveMentions(token, raw, cache));
|
|
139
|
-
const
|
|
140
|
-
|
|
170
|
+
const lines = resolved.split("\n");
|
|
171
|
+
const body = lines[0] + (lines.length > 1 ? "\n" + lines.slice(1).map(l => ` ${l}`).join("\n") : "");
|
|
172
|
+
const who = chLabel ? `${chLabel} @${handle}` : `@${handle}`;
|
|
173
|
+
return `${stamp} ${who}: ${body}`;
|
|
141
174
|
}
|
|
142
175
|
|
|
143
176
|
// --- msgs <target> — channel/DM history with timestamps ---
|
|
144
177
|
async function cmdMsgsTarget(token: string, target: string, limit: number): Promise<void> {
|
|
178
|
+
const parsed = parseSlackPermalink(target);
|
|
145
179
|
const channelId = await resolveChannel(token, target);
|
|
146
|
-
const hist = (await history(token, channelId, limit)) as Record<string, Json>;
|
|
147
|
-
const msgs = asArray(hist.messages).map(asRecord);
|
|
148
180
|
const cache = new Map<string, string>();
|
|
149
|
-
|
|
150
|
-
|
|
181
|
+
if (parsed?.threadTs) {
|
|
182
|
+
const resp = (await replies(token, channelId, parsed.threadTs, limit)) as Record<string, Json>;
|
|
183
|
+
const msgs = asArray(resp.messages).map(asRecord);
|
|
184
|
+
for (const m of msgs) {
|
|
185
|
+
console.log(await formatMsgLine(token, m, cache));
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
const hist = (await history(token, channelId, limit)) as Record<string, Json>;
|
|
189
|
+
const msgs = asArray(hist.messages).map(asRecord);
|
|
190
|
+
for (const m of msgs.reverse()) {
|
|
191
|
+
console.log(await formatMsgLine(token, m, cache));
|
|
192
|
+
}
|
|
151
193
|
}
|
|
152
194
|
}
|
|
153
195
|
|
|
154
196
|
// --- thread ---
|
|
155
197
|
async function cmdThread(token: string, target: string, ts: string, limit: number): Promise<void> {
|
|
156
198
|
const channelId = await resolveChannel(token, target);
|
|
157
|
-
const resp = (await replies(token, channelId, ts, limit)) as Record<string, Json>;
|
|
199
|
+
const resp = (await replies(token, channelId, parseInputTs(ts), limit)) as Record<string, Json>;
|
|
158
200
|
const msgs = asArray(resp.messages).map(asRecord);
|
|
159
201
|
const cache = new Map<string, string>();
|
|
160
202
|
for (const m of msgs) {
|
|
@@ -183,9 +225,10 @@ async function cmdMsgs(token: string): Promise<void> {
|
|
|
183
225
|
const t = typeof m.text === "string" ? m.text : "";
|
|
184
226
|
return t.length > 0 && !t.startsWith("<@");
|
|
185
227
|
})
|
|
186
|
-
.slice(0, 3)
|
|
228
|
+
.slice(0, 3)
|
|
229
|
+
.reverse();
|
|
187
230
|
if (msgs.length === 0) continue;
|
|
188
|
-
console.log(
|
|
231
|
+
console.log(`-- #${name} --------------------------------`);
|
|
189
232
|
for (const m of msgs) {
|
|
190
233
|
const who = await displayUser(token, m, cache);
|
|
191
234
|
const raw = (typeof m.text === "string" ? m.text : "").split("\n")[0] ?? "";
|
|
@@ -198,44 +241,49 @@ async function cmdMsgs(token: string): Promise<void> {
|
|
|
198
241
|
// --- news ---
|
|
199
242
|
async function cmdNews(token: string, limit: number): Promise<void> {
|
|
200
243
|
const resp = (await search(token, "to:me")) as Record<string, Json>;
|
|
201
|
-
const matches = asArray(getPath(resp, ["messages", "matches"])).map(asRecord);
|
|
244
|
+
const matches = asArray(getPath(resp, ["messages", "matches"])).map(asRecord).slice(0, limit);
|
|
202
245
|
const cache = new Map<string, string>();
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
246
|
+
|
|
247
|
+
// Group by day (API returns newest-first; reverse within each group for chronological reading)
|
|
248
|
+
const groups: { label: string; msgs: Record<string, Json>[] }[] = [];
|
|
249
|
+
for (const m of matches) {
|
|
250
|
+
const label = dayLabel(tsNum(m));
|
|
251
|
+
const last = groups[groups.length - 1];
|
|
252
|
+
if (last?.label === label) last.msgs.push(m);
|
|
253
|
+
else groups.push({ label, msgs: [m] });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
257
|
+
const { label, msgs } = groups[gi]!;
|
|
258
|
+
if (gi > 0) console.log("");
|
|
259
|
+
console.log(` ${label}`);
|
|
260
|
+
console.log(" ----------------------------");
|
|
261
|
+
for (const m of [...msgs].reverse()) {
|
|
262
|
+
const ch = asRecord(m.channel);
|
|
263
|
+
const isIm = ch.is_im === true;
|
|
264
|
+
const rawName = typeof ch.name === "string" ? ch.name : "dm";
|
|
265
|
+
let chLabel: string;
|
|
266
|
+
if (isIm && rawName.startsWith("U")) {
|
|
267
|
+
const handleKey = "@" + rawName;
|
|
268
|
+
if (!cache.has(handleKey)) {
|
|
269
|
+
const [, h] = await userInfoPair(token, rawName);
|
|
270
|
+
cache.set(handleKey, h);
|
|
271
|
+
}
|
|
272
|
+
chLabel = `@${cache.get(handleKey) ?? rawName}`;
|
|
273
|
+
} else if (isIm) {
|
|
274
|
+
chLabel = `@${rawName}`;
|
|
275
|
+
} else {
|
|
276
|
+
chLabel = `#${rawName}`;
|
|
277
|
+
}
|
|
278
|
+
console.log(await formatMsgLine(token, m, cache, chLabel));
|
|
224
279
|
}
|
|
225
|
-
const display = await displayUser(token, m, cache);
|
|
226
|
-
const raw = typeof m.text === "string" ? m.text : "";
|
|
227
|
-
const resolved = resolveDateMarkup(await resolveMentions(token, raw, cache));
|
|
228
|
-
const firstLine = (resolved.split("\n")[0] ?? "").slice(0, 80);
|
|
229
|
-
const icon = isIm ? "💬" : "🔔";
|
|
230
|
-
console.log(` ${icon} ${chLabel} ${formatHm(ts)}`);
|
|
231
|
-
console.log(` ${display}: ${firstLine}`);
|
|
232
280
|
}
|
|
233
281
|
}
|
|
234
282
|
|
|
235
283
|
// --- channels ---
|
|
236
|
-
async function cmdChannels(token: string, limit: number, filter?: string, all?: boolean): Promise<void> {
|
|
284
|
+
async function cmdChannels(token: string, limit: number, filter?: string, all?: boolean, format = "text"): Promise<void> {
|
|
237
285
|
const resp = (await listConversations(token)) as Record<string, Json>;
|
|
238
|
-
|
|
286
|
+
const channels = asArray(resp.channels)
|
|
239
287
|
.map(asRecord)
|
|
240
288
|
.filter((c) => all || c.is_member === true)
|
|
241
289
|
.filter((c) => {
|
|
@@ -246,6 +294,10 @@ async function cmdChannels(token: string, limit: number, filter?: string, all?:
|
|
|
246
294
|
.sort((a, b) => Number(b.updated ?? 0) - Number(a.updated ?? 0))
|
|
247
295
|
.slice(0, limit);
|
|
248
296
|
|
|
297
|
+
if (format === "jsonl") {
|
|
298
|
+
for (const ch of channels) console.log(JSON.stringify(ch));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
249
301
|
for (const ch of channels) {
|
|
250
302
|
const id = String(ch.id ?? "");
|
|
251
303
|
const name = typeof ch.name === "string" ? ch.name : typeof ch.user === "string" ? ch.user : id;
|
|
@@ -253,14 +305,40 @@ async function cmdChannels(token: string, limit: number, filter?: string, all?:
|
|
|
253
305
|
const isMpim = ch.is_mpim === true;
|
|
254
306
|
const prefix = isIm || isMpim ? "@" : "#";
|
|
255
307
|
const memberMark = ch.is_member === true ? "" : " (not joined)";
|
|
256
|
-
|
|
308
|
+
const purpose = typeof asRecord(ch.purpose).value === "string" ? String(asRecord(ch.purpose).value) : "";
|
|
309
|
+
const meta = purpose ? ` ${purpose.split("\n")[0]?.slice(0, 60)}` : "";
|
|
310
|
+
console.log(`${prefix}${name} ${id}${memberMark}${meta}`);
|
|
257
311
|
}
|
|
258
312
|
}
|
|
259
313
|
|
|
260
314
|
// --- search ---
|
|
261
|
-
async function cmdSearch(token: string, query: string, count: number): Promise<void> {
|
|
315
|
+
async function cmdSearch(token: string, query: string, count: number, json: boolean): Promise<void> {
|
|
262
316
|
const resp = await searchAll(token, query, count);
|
|
263
|
-
|
|
317
|
+
if (json) {
|
|
318
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const matches = asArray(getPath(resp as Record<string, Json>, ["messages", "matches"])).map(asRecord);
|
|
322
|
+
const cache = new Map<string, string>();
|
|
323
|
+
for (const m of matches) {
|
|
324
|
+
const ch = asRecord(m.channel);
|
|
325
|
+
const isIm = ch.is_im === true;
|
|
326
|
+
const rawName = typeof ch.name === "string" ? ch.name : "dm";
|
|
327
|
+
let chLabel: string;
|
|
328
|
+
if (isIm && rawName.startsWith("U")) {
|
|
329
|
+
const handleKey = "@" + rawName;
|
|
330
|
+
if (!cache.has(handleKey)) {
|
|
331
|
+
const [, h] = await userInfoPair(token, rawName);
|
|
332
|
+
cache.set(handleKey, h);
|
|
333
|
+
}
|
|
334
|
+
chLabel = `@${cache.get(handleKey) ?? rawName}`;
|
|
335
|
+
} else if (isIm) {
|
|
336
|
+
chLabel = `@${rawName}`;
|
|
337
|
+
} else {
|
|
338
|
+
chLabel = `#${rawName}`;
|
|
339
|
+
}
|
|
340
|
+
console.log(await formatMsgLine(token, m, cache, chLabel));
|
|
341
|
+
}
|
|
264
342
|
}
|
|
265
343
|
|
|
266
344
|
// --- dump ---
|
|
@@ -400,7 +478,7 @@ async function cmdDrafts(token: string, cookie?: string, showAll = false): Promi
|
|
|
400
478
|
const id = typeof d.id === "string" ? d.id : "";
|
|
401
479
|
const sentTag = d.is_sent === true ? " [SENT]" : "";
|
|
402
480
|
const resolved = resolveDateMarkup(await resolveMentions(token, text, mentionCache));
|
|
403
|
-
console.log(
|
|
481
|
+
console.log(`-- ${id} ${chLabel} [${stamp}]${sentTag}`);
|
|
404
482
|
for (const line of resolved.split("\n")) console.log(` ${line}`);
|
|
405
483
|
}
|
|
406
484
|
}
|
|
@@ -450,7 +528,8 @@ function requireCode(provided: string | undefined, expected: string, contextLine
|
|
|
450
528
|
}
|
|
451
529
|
|
|
452
530
|
/** Split a target ref that may embed a message ts.
|
|
453
|
-
* Accepts: `#chan:
|
|
531
|
+
* Accepts: `#chan:1700000000.000100`, `#chan:2026-05-11T06:01:04.000100`, Slack permalink URL, or plain ref.
|
|
532
|
+
* Throws if an ISO-format ts is present but missing the required 6-digit fractional part. */
|
|
454
533
|
function splitRefTs(s: string): { ref: string; ts?: string } {
|
|
455
534
|
const url = parseSlackPermalink(s);
|
|
456
535
|
if (url) return url.ts ? { ref: url.channel, ts: url.ts } : { ref: url.channel };
|
|
@@ -461,6 +540,9 @@ function splitRefTs(s: string): { ref: string; ts?: string } {
|
|
|
461
540
|
if (/^\d{10}\.\d{6}$/.test(maybeTs)) {
|
|
462
541
|
return { ref: s.slice(0, colon), ts: maybeTs };
|
|
463
542
|
}
|
|
543
|
+
if (/^\d{4}-\d{2}-\d{2}T/.test(maybeTs)) {
|
|
544
|
+
return { ref: s.slice(0, colon), ts: isoToSlackTs(maybeTs) };
|
|
545
|
+
}
|
|
464
546
|
}
|
|
465
547
|
}
|
|
466
548
|
return { ref: s };
|
|
@@ -476,7 +558,7 @@ interface EditArgs {
|
|
|
476
558
|
async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
477
559
|
const { ref, ts } = splitRefTs(args.target);
|
|
478
560
|
if (!ts) {
|
|
479
|
-
console.error("Error: target must embed a message ts (e.g. #chan:
|
|
561
|
+
console.error("Error: target must embed a message ts (e.g. #chan:2026-05-11T06:01:04.000100 or a Slack permalink URL)");
|
|
480
562
|
process.exit(2);
|
|
481
563
|
}
|
|
482
564
|
|
|
@@ -497,11 +579,11 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
|
497
579
|
const code = safetyCode(originalText, args.newText);
|
|
498
580
|
if (args.code !== code) {
|
|
499
581
|
requireCode(args.code, code, [
|
|
500
|
-
|
|
582
|
+
`--- Original message -------------------------`,
|
|
501
583
|
...originalText.split("\n").map((l) => ` ${l}`),
|
|
502
|
-
|
|
584
|
+
`--- Replacing with ---------------------------`,
|
|
503
585
|
...args.newText.split("\n").map((l) => ` ${l}`),
|
|
504
|
-
|
|
586
|
+
`--------------------------------────────────`,
|
|
505
587
|
]);
|
|
506
588
|
}
|
|
507
589
|
|
|
@@ -541,22 +623,109 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
541
623
|
|
|
542
624
|
if (args.code !== code) {
|
|
543
625
|
requireCode(args.code, code, [
|
|
544
|
-
|
|
626
|
+
`--- Last message in channel ------------------`,
|
|
545
627
|
` ${lastUser}: ${lastText.split("\n")[0]?.slice(0, 100) ?? "(empty)"}`,
|
|
546
|
-
|
|
628
|
+
`--- Sending ----------------------------------`,
|
|
547
629
|
` To: ${args.target}${args.thread ? ` (thread ${args.thread})` : ""}`,
|
|
548
630
|
` Message: ${args.message}`,
|
|
549
|
-
|
|
631
|
+
`--------------------------------────────────`,
|
|
550
632
|
]);
|
|
551
633
|
}
|
|
552
634
|
const ts = await slackSend(token, channelId, args.message, args.thread);
|
|
553
635
|
console.log(`✓ Sent (ts: ${ts})`);
|
|
554
636
|
}
|
|
555
637
|
|
|
638
|
+
// --- schedule ---
|
|
639
|
+
function parsePostAt(at: string): number {
|
|
640
|
+
if (/^\d{10,}$/.test(at)) return parseInt(at, 10);
|
|
641
|
+
const d = new Date(at.replace(" ", "T"));
|
|
642
|
+
if (isNaN(d.getTime())) throw new Error(`Cannot parse time: ${at}`);
|
|
643
|
+
return Math.floor(d.getTime() / 1000);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
interface ScheduleSendArgs {
|
|
647
|
+
target: string;
|
|
648
|
+
message: string;
|
|
649
|
+
at: string;
|
|
650
|
+
thread?: string;
|
|
651
|
+
code?: string;
|
|
652
|
+
channelId?: string;
|
|
653
|
+
userId?: string;
|
|
654
|
+
}
|
|
655
|
+
async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<void> {
|
|
656
|
+
let channelId: string;
|
|
657
|
+
if (args.channelId) channelId = args.channelId;
|
|
658
|
+
else if (args.userId) channelId = await openDm(token, args.userId);
|
|
659
|
+
else if (args.target.startsWith("#") || args.target.startsWith("@")) {
|
|
660
|
+
channelId = await resolveChannel(token, args.target);
|
|
661
|
+
} else {
|
|
662
|
+
console.error(`Error: target must be #channel-name or @username (got: ${args.target})`);
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const postAt = parsePostAt(args.at);
|
|
667
|
+
const postAtDate = new Date(postAt * 1000).toISOString();
|
|
668
|
+
const code = safetyCode(channelId, args.message, String(postAt));
|
|
669
|
+
|
|
670
|
+
if (args.code !== code) {
|
|
671
|
+
requireCode(args.code, code, [
|
|
672
|
+
`--- Scheduling message -----------------------`,
|
|
673
|
+
` To: ${args.target}${args.thread ? ` (thread ${args.thread})` : ""}`,
|
|
674
|
+
` At: ${postAtDate} (Unix: ${postAt})`,
|
|
675
|
+
` Message: ${args.message}`,
|
|
676
|
+
`---------------------------------------------`,
|
|
677
|
+
]);
|
|
678
|
+
}
|
|
679
|
+
const id = await scheduleMessage(token, channelId, args.message, postAt, args.thread);
|
|
680
|
+
console.log(`✓ Scheduled (id: ${id}, at: ${postAtDate})`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function cmdScheduleList(token: string, target?: string, channelId?: string): Promise<void> {
|
|
684
|
+
let channel: string | undefined;
|
|
685
|
+
if (channelId) {
|
|
686
|
+
channel = channelId;
|
|
687
|
+
} else if (target) {
|
|
688
|
+
channel = await resolveChannel(token, target);
|
|
689
|
+
}
|
|
690
|
+
const resp = (await listScheduledMessages(token, channel)) as {
|
|
691
|
+
scheduled_messages?: { id: string; channel_id: string; post_at: number; text: string }[];
|
|
692
|
+
};
|
|
693
|
+
const msgs = resp.scheduled_messages ?? [];
|
|
694
|
+
if (msgs.length === 0) { console.log("(no scheduled messages)"); return; }
|
|
695
|
+
for (const m of msgs) {
|
|
696
|
+
const at = new Date(m.post_at * 1000).toISOString();
|
|
697
|
+
console.log(`${m.id} ${at} [${m.channel_id}] ${m.text.split("\n")[0]?.slice(0, 80) ?? ""}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
interface ScheduleRmArgs {
|
|
702
|
+
target: string;
|
|
703
|
+
id: string;
|
|
704
|
+
code?: string;
|
|
705
|
+
channelId?: string;
|
|
706
|
+
}
|
|
707
|
+
async function cmdScheduleRm(token: string, args: ScheduleRmArgs): Promise<void> {
|
|
708
|
+
let channelId: string;
|
|
709
|
+
if (args.channelId) channelId = args.channelId;
|
|
710
|
+
else channelId = await resolveChannel(token, args.target);
|
|
711
|
+
|
|
712
|
+
const code = safetyCode(channelId, args.id);
|
|
713
|
+
if (args.code !== code) {
|
|
714
|
+
requireCode(args.code, code, [
|
|
715
|
+
`--- Deleting scheduled message ---------------`,
|
|
716
|
+
` Channel: ${args.target}`,
|
|
717
|
+
` ID: ${args.id}`,
|
|
718
|
+
`---------------------------------------------`,
|
|
719
|
+
]);
|
|
720
|
+
}
|
|
721
|
+
await deleteScheduledMessage(token, channelId, args.id);
|
|
722
|
+
console.log(`✓ Deleted scheduled message ${args.id}`);
|
|
723
|
+
}
|
|
724
|
+
|
|
556
725
|
// --- upload ---
|
|
557
726
|
interface UploadArgs {
|
|
558
727
|
target: string;
|
|
559
|
-
|
|
728
|
+
filePaths: string[];
|
|
560
729
|
title?: string;
|
|
561
730
|
thread?: string;
|
|
562
731
|
comment?: string;
|
|
@@ -568,11 +737,12 @@ async function cmdUpload(token: string, args: UploadArgs): Promise<void> {
|
|
|
568
737
|
const { statSync, existsSync } = await import("node:fs");
|
|
569
738
|
const { basename } = await import("node:path");
|
|
570
739
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
740
|
+
for (const fp of args.filePaths) {
|
|
741
|
+
if (!existsSync(fp)) {
|
|
742
|
+
console.error(`Error: file not found: ${fp}`);
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
574
745
|
}
|
|
575
|
-
const stat = statSync(args.filePath);
|
|
576
746
|
|
|
577
747
|
let channelId: string;
|
|
578
748
|
if (args.channelId) channelId = args.channelId;
|
|
@@ -584,427 +754,561 @@ async function cmdUpload(token: string, args: UploadArgs): Promise<void> {
|
|
|
584
754
|
process.exit(1);
|
|
585
755
|
}
|
|
586
756
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
757
|
+
function fmtSize(n: number): string {
|
|
758
|
+
return n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1048576).toFixed(1)} MB`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const isBatch = args.filePaths.length > 1;
|
|
762
|
+
const files = args.filePaths.map((fp) => {
|
|
763
|
+
const filename = basename(fp);
|
|
764
|
+
const title = isBatch ? filename : (args.title ?? filename);
|
|
765
|
+
return { fp, filename, title, sizeFmt: fmtSize(statSync(fp).size) };
|
|
766
|
+
});
|
|
594
767
|
|
|
595
|
-
|
|
768
|
+
// Safety code covers the full batch — single-file code is identical to the old formula.
|
|
769
|
+
const code = safetyCode(channelId, ...files.flatMap((f) => [f.fp, f.title]));
|
|
596
770
|
if (args.code !== code) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
771
|
+
const destLine = ` To: ${args.target}${args.thread ? ` (thread ${args.thread})` : ""}`;
|
|
772
|
+
const lines = isBatch
|
|
773
|
+
? [
|
|
774
|
+
`--- Uploading ${files.length} files ------------------------`,
|
|
775
|
+
destLine,
|
|
776
|
+
...files.map((f) => ` ${f.filename} (${f.sizeFmt})`),
|
|
777
|
+
`--------------------------------────────────`,
|
|
778
|
+
]
|
|
779
|
+
: [
|
|
780
|
+
`--- Uploading file ---------------------------`,
|
|
781
|
+
destLine,
|
|
782
|
+
` File: ${files[0]!.fp}`,
|
|
783
|
+
` Title: ${files[0]!.title}`,
|
|
784
|
+
` Size: ${files[0]!.sizeFmt}`,
|
|
785
|
+
`--------------------------------────────────`,
|
|
786
|
+
];
|
|
787
|
+
requireCode(args.code, code, lines);
|
|
605
788
|
}
|
|
606
789
|
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
790
|
+
const total = files.length;
|
|
791
|
+
for (let i = 0; i < total; i++) {
|
|
792
|
+
const f = files[i]!;
|
|
793
|
+
const uploadOpts: { title?: string; threadTs?: string; initialComment?: string } = { title: f.title };
|
|
794
|
+
if (args.thread !== undefined) uploadOpts.threadTs = args.thread;
|
|
795
|
+
if (args.comment !== undefined && i === 0) uploadOpts.initialComment = args.comment;
|
|
796
|
+
const { fileId, permalink } = await uploadFile(token, channelId, f.fp, uploadOpts);
|
|
797
|
+
const prefix = total > 1 ? `[${i + 1}/${total}] ` : "";
|
|
798
|
+
console.log(`${prefix}✓ Uploaded (file_id: ${fileId}${permalink ? `, url: ${permalink}` : ""})`);
|
|
799
|
+
}
|
|
612
800
|
}
|
|
613
801
|
|
|
614
802
|
// --- dispatch ---
|
|
615
|
-
function usage(): never {
|
|
616
|
-
console.error(
|
|
617
|
-
[
|
|
618
|
-
"Usage: slack [--workspace=<name>] <command> [args]",
|
|
619
|
-
"Commands:",
|
|
620
|
-
" msgs [<#channel|@user|url>] [-n|--limit N]",
|
|
621
|
-
" thread <#channel|@user|url> <ts> [-n|--limit N]",
|
|
622
|
-
" channels [-n|--limit N] [--filter STR] [--all]",
|
|
623
|
-
" news [-l|--limit N]",
|
|
624
|
-
" search <query> [-n|--count N]",
|
|
625
|
-
" drafts [--all] list pending drafts (--all includes sent)",
|
|
626
|
-
" drafts new <#channel|@user> <text>",
|
|
627
|
-
" drafts get <draft-id>",
|
|
628
|
-
" drafts edit <draft-id> [--code=XXXX] <new-text>",
|
|
629
|
-
" drafts delete <draft-id> [--code=XXXX]",
|
|
630
|
-
" send <target> <message> [--thread TS] [--code XXXX] [--channel-id ID] [--user-id ID]",
|
|
631
|
-
" edit <#chan:ts|@user:ts|url> <new-text> [--code XXXX] [--channel-id ID]",
|
|
632
|
-
" upload <target> <file> [--title TEXT] [--thread TS] [--comment TEXT] [--code XXXX] [--channel-id ID] [--user-id ID]",
|
|
633
|
-
" dump [-d|--days N] [-l|--limit N] [-f|--filter STR]",
|
|
634
|
-
" workspace ls|list",
|
|
635
|
-
" workspace import (auto-import from Slack desktop app)",
|
|
636
|
-
" workspace add <name> <token>",
|
|
637
|
-
" workspace set-token <name> <token>",
|
|
638
|
-
" workspace set-cookie <name> <xoxd> (store xoxd cookie for draft API)",
|
|
639
|
-
" workspace use <name>",
|
|
640
|
-
" workspace remove <name>",
|
|
641
|
-
" workspace current",
|
|
642
|
-
].join("\n"),
|
|
643
|
-
);
|
|
644
|
-
process.exit(2);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
async function cmdWorkspace(sub: string, args: string[]): Promise<void> {
|
|
648
|
-
switch (sub) {
|
|
649
|
-
case "list":
|
|
650
|
-
case "ls": {
|
|
651
|
-
const profiles = listProfiles();
|
|
652
|
-
if (profiles.length === 0) {
|
|
653
|
-
console.log("No workspaces configured.");
|
|
654
|
-
console.log("Import from the Slack desktop app: slack workspace import");
|
|
655
|
-
console.log("Or add manually: slack workspace add <name> <token>");
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
for (const { name, profile, current } of profiles) {
|
|
659
|
-
console.log(`${current ? "* " : " "}${name} ${profile.team} (${profile.user}) ${profile.url ?? ""}`);
|
|
660
|
-
}
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
case "import": {
|
|
664
|
-
console.error("Scanning Slack desktop app...");
|
|
665
|
-
const sessions = await extractSessions();
|
|
666
|
-
if (sessions.length === 0) {
|
|
667
|
-
console.error("No sessions found. Make sure Slack is installed and you have logged in.");
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
for (const s of sessions) {
|
|
671
|
-
const teamLabel = s.teamName ?? s.teamId;
|
|
672
|
-
console.error(` Found: ${teamLabel} ${s.url ?? ""}`);
|
|
673
|
-
const name = teamLabel.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
674
|
-
const profile: Parameters<typeof addProfile>[1] = {
|
|
675
|
-
token: s.token,
|
|
676
|
-
team: s.teamName ?? s.teamId,
|
|
677
|
-
teamId: s.teamId,
|
|
678
|
-
url: s.url ?? "",
|
|
679
|
-
user: "",
|
|
680
|
-
};
|
|
681
|
-
if (s.cookie) profile.cookie = s.cookie;
|
|
682
|
-
addProfile(name, profile);
|
|
683
|
-
const cookieNote = s.cookie ? " + xoxd cookie" : "";
|
|
684
|
-
console.log(`Added workspace "${name}": ${teamLabel}${cookieNote}`);
|
|
685
|
-
}
|
|
686
|
-
console.log(`\nNote: desktop app tokens (xoxc-) are internal Slack tokens.`);
|
|
687
|
-
console.log(`If API calls fail, replace with an xoxp- token:`);
|
|
688
|
-
console.log(` slack workspace set-token <name> <xoxp-token>`);
|
|
689
|
-
console.log(`\nDone. Run: slack workspace ls`);
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
case "add": {
|
|
693
|
-
const [name, token] = args;
|
|
694
|
-
if (!name || !token) {
|
|
695
|
-
console.error("Usage: slack workspace add <name> <token>");
|
|
696
|
-
process.exit(2);
|
|
697
|
-
}
|
|
698
|
-
console.error(`Verifying token...`);
|
|
699
|
-
const info = await authTest(token);
|
|
700
|
-
addProfile(name, { token, ...info });
|
|
701
|
-
console.log(`Added workspace "${name}": ${info.team} (${info.user})`);
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
case "set-token": {
|
|
705
|
-
const [name, token] = args;
|
|
706
|
-
if (!name || !token) {
|
|
707
|
-
console.error("Usage: slack workspace set-token <name> <token>");
|
|
708
|
-
console.error(" Updates the token for an existing workspace profile.");
|
|
709
|
-
process.exit(2);
|
|
710
|
-
}
|
|
711
|
-
console.error(`Verifying token...`);
|
|
712
|
-
const info = await authTest(token);
|
|
713
|
-
addProfile(name, { token, ...info });
|
|
714
|
-
console.log(`Updated workspace "${name}": ${info.team} (${info.user})`);
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
case "set-cookie": {
|
|
718
|
-
const [name, xoxd] = args;
|
|
719
|
-
if (!name || !xoxd) {
|
|
720
|
-
console.error("Usage: slack workspace set-cookie <name> <xoxd-value>");
|
|
721
|
-
console.error(" Stores the xoxd session cookie for draft API access.");
|
|
722
|
-
console.error(" Get it from: DevTools → Application → Cookies → slack.com → d");
|
|
723
|
-
process.exit(2);
|
|
724
|
-
}
|
|
725
|
-
const cookieVal = xoxd.startsWith("d=") ? xoxd.slice(2) : xoxd;
|
|
726
|
-
setCookie(name, cookieVal);
|
|
727
|
-
console.log(`Cookie set for workspace "${name}". Run: slack drafts`);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
case "use": {
|
|
731
|
-
const globalFlag = args.includes("-g");
|
|
732
|
-
const name = args.find((a) => a !== "-g");
|
|
733
|
-
if (!name) {
|
|
734
|
-
console.error("Usage: slack workspace use [-g] <name>");
|
|
735
|
-
console.error(" -g write to ~/.slack-cli/workspace (global)");
|
|
736
|
-
console.error(" default: write to .slack-cli/workspace (local cwd)");
|
|
737
|
-
process.exit(2);
|
|
738
|
-
}
|
|
739
|
-
if (!globalFlag) ensureSlackCliDir(join(process.cwd(), ".slack-cli"));
|
|
740
|
-
useProfile(name, globalFlag);
|
|
741
|
-
const scope = globalFlag ? "globally (~/.slack-cli/workspace)" : "locally (.slack-cli/workspace)";
|
|
742
|
-
console.log(`Switched to workspace "${name}" ${scope}`);
|
|
743
|
-
if (!globalFlag) console.log(`Tip: add .slack-cli/ to your .gitignore`);
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
case "remove": {
|
|
747
|
-
const [name] = args;
|
|
748
|
-
if (!name) { console.error("Usage: slack workspace remove <name>"); process.exit(2); }
|
|
749
|
-
removeProfile(name);
|
|
750
|
-
console.log(`Removed workspace "${name}"`);
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
case "current": {
|
|
754
|
-
const profiles = listProfiles();
|
|
755
|
-
const cur = profiles.find((p) => p.current);
|
|
756
|
-
if (!cur) { console.log("No workspace selected"); return; }
|
|
757
|
-
console.log(`${cur.name} ${cur.profile.team} (${cur.profile.user})`);
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
default:
|
|
761
|
-
usage();
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
803
|
|
|
765
804
|
async function main(): Promise<void> {
|
|
766
805
|
loadDotenvFiles();
|
|
767
806
|
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
const filteredArgs: string[] = [];
|
|
772
|
-
for (const arg of rawArgs) {
|
|
773
|
-
const m = arg.match(/^--workspace=(.+)$/);
|
|
774
|
-
if (m) { workspaceFlag = m[1]; }
|
|
775
|
-
else filteredArgs.push(arg);
|
|
776
|
-
}
|
|
777
|
-
const [cmd, ...rest] = filteredArgs;
|
|
778
|
-
// workspace subcommand needs no token
|
|
779
|
-
if (cmd === "workspace") {
|
|
780
|
-
await cmdWorkspace(rest[0] ?? "", rest.slice(1));
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
const token = resolveToken(workspaceFlag);
|
|
785
|
-
const cookie = resolveCookie(workspaceFlag);
|
|
807
|
+
type W = { workspace?: string };
|
|
808
|
+
const tok = (a: W) => resolveToken(a.workspace);
|
|
809
|
+
const ck = (a: W) => resolveCookie(a.workspace);
|
|
786
810
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
case "thread": {
|
|
803
|
-
const { values, positionals } = parseArgs({
|
|
804
|
-
args: rest,
|
|
805
|
-
allowPositionals: true,
|
|
806
|
-
options: { limit: { type: "string", short: "n", default: "100" } },
|
|
807
|
-
strict: true,
|
|
808
|
-
});
|
|
809
|
-
const target = positionals[0];
|
|
810
|
-
const ts = positionals[1];
|
|
811
|
-
if (!target || !ts) usage();
|
|
812
|
-
await cmdThread(token, target, ts, Number(values.limit));
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
case "channels": {
|
|
816
|
-
const { values } = parseArgs({
|
|
817
|
-
args: rest,
|
|
818
|
-
options: {
|
|
819
|
-
limit: { type: "string", short: "n", default: "200" },
|
|
820
|
-
filter: { type: "string", short: "f" },
|
|
821
|
-
all: { type: "boolean" },
|
|
822
|
-
},
|
|
823
|
-
strict: true,
|
|
824
|
-
});
|
|
825
|
-
await cmdChannels(token, Number(values.limit), values.filter, values.all);
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
case "news": {
|
|
829
|
-
const { values } = parseArgs({
|
|
830
|
-
args: rest,
|
|
831
|
-
options: { limit: { type: "string", short: "l", default: "20" } },
|
|
832
|
-
strict: true,
|
|
833
|
-
});
|
|
834
|
-
await cmdNews(token, Number(values.limit));
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
case "drafts": {
|
|
838
|
-
const sub = rest[0];
|
|
839
|
-
if (sub === "new" || sub === "save") {
|
|
840
|
-
// drafts new <#channel|@user> <text...>
|
|
841
|
-
const args2 = rest.slice(1);
|
|
842
|
-
const target = args2[0];
|
|
843
|
-
const text = args2.slice(1).join(" ");
|
|
844
|
-
if (!target || !text) {
|
|
845
|
-
console.error("Usage: slack drafts new <#channel|@user> <text>");
|
|
846
|
-
process.exit(2);
|
|
847
|
-
}
|
|
848
|
-
const channelId = await resolveChannel(token, target, cookie);
|
|
849
|
-
const resp = (await createDraft(token, channelId, text, cookie)) as Record<string, Json>;
|
|
850
|
-
console.log(`✓ Draft created (id: ${asRecord(resp.draft).id ?? "?"})`);
|
|
851
|
-
} else if (sub === "get") {
|
|
852
|
-
const draftId = rest[1];
|
|
853
|
-
if (!draftId) { console.error("Usage: slack drafts get <draft-id>"); process.exit(2); }
|
|
854
|
-
await cmdDraftGet(token, cookie, draftId);
|
|
855
|
-
} else if (sub === "edit" || sub === "update") {
|
|
856
|
-
// drafts edit <draft-id> [--code=XXXX] <text...>
|
|
857
|
-
const draftId = rest[1];
|
|
858
|
-
const codeArg = rest.find((a) => a.startsWith("--code="))?.slice(7);
|
|
859
|
-
const textParts = rest.slice(2).filter((a) => !a.startsWith("--code="));
|
|
860
|
-
const text = textParts.join(" ");
|
|
861
|
-
if (!draftId || !text) {
|
|
862
|
-
console.error("Usage: slack drafts edit <draft-id> <new-text>");
|
|
863
|
-
process.exit(2);
|
|
864
|
-
}
|
|
865
|
-
const listResp = (await listDrafts(token, cookie)) as Record<string, Json>;
|
|
866
|
-
const d = asArray(listResp.drafts).map(asRecord).find((x) => String(x.id) === draftId);
|
|
867
|
-
if (!d) { console.error(`Draft not found: ${draftId}`); process.exit(1); }
|
|
868
|
-
const prevText = draftText(d);
|
|
869
|
-
const code = safetyCode(prevText, text);
|
|
870
|
-
if (codeArg !== code) {
|
|
871
|
-
requireCode(codeArg, code, [
|
|
872
|
-
`─── Current draft content ────────────────────`,
|
|
873
|
-
...prevText.split("\n").map((l) => ` ${l}`),
|
|
874
|
-
`─── Replacing with ───────────────────────────`,
|
|
875
|
-
...text.split("\n").map((l) => ` ${l}`),
|
|
876
|
-
`─────────────────────────────────────────────`,
|
|
877
|
-
]);
|
|
811
|
+
await yargs(hideBin(process.argv))
|
|
812
|
+
.scriptName("slack")
|
|
813
|
+
.option("workspace", { alias: "w", type: "string", describe: "Workspace name" })
|
|
814
|
+
.middleware(async (argv) => {
|
|
815
|
+
const cmd = String((argv._ ?? [])[0] ?? "");
|
|
816
|
+
if (!cmd || cmd === "auth" || cmd === "login") return;
|
|
817
|
+
try {
|
|
818
|
+
resolveToken((argv as W).workspace);
|
|
819
|
+
} catch (e) {
|
|
820
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
821
|
+
if (msg.startsWith("No profiles configured")) {
|
|
822
|
+
console.log("No workspace configured. Let's set that up:\n");
|
|
823
|
+
await cmdAuthLogin();
|
|
824
|
+
process.exit(0);
|
|
878
825
|
}
|
|
879
|
-
|
|
880
|
-
const resp = (await updateDraft(token, draftId, channelId, text, cookie)) as Record<string, Json>;
|
|
881
|
-
console.log(`✓ Draft updated (id: ${asRecord(resp.draft).id ?? "?"})`);
|
|
882
|
-
} else if (sub === "delete" || sub === "rm") {
|
|
883
|
-
const draftId = rest[1];
|
|
884
|
-
const codeArg = rest.find((a) => a.startsWith("--code="))?.slice(7);
|
|
885
|
-
if (!draftId) { console.error("Usage: slack drafts delete <draft-id>"); process.exit(2); }
|
|
886
|
-
const listResp = (await listDrafts(token, cookie)) as Record<string, Json>;
|
|
887
|
-
const d = asArray(listResp.drafts).map(asRecord).find((x) => String(x.id) === draftId);
|
|
888
|
-
if (!d) { console.error(`Draft not found: ${draftId}`); process.exit(1); }
|
|
889
|
-
const prevText = draftText(d);
|
|
890
|
-
const code = safetyCode(draftId, prevText);
|
|
891
|
-
if (codeArg !== code) {
|
|
892
|
-
requireCode(codeArg, code, [
|
|
893
|
-
`─── Deleting draft ───────────────────────────`,
|
|
894
|
-
` id: ${draftId}`,
|
|
895
|
-
...prevText.split("\n").map((l) => ` ${l}`),
|
|
896
|
-
`─────────────────────────────────────────────`,
|
|
897
|
-
]);
|
|
898
|
-
}
|
|
899
|
-
await deleteDraft(token, draftId, cookie);
|
|
900
|
-
console.log(`✓ Draft deleted (id: ${draftId})`);
|
|
901
|
-
} else {
|
|
902
|
-
const showAll = rest.includes("--all") || rest.includes("-a");
|
|
903
|
-
await cmdDrafts(token, cookie, showAll);
|
|
826
|
+
throw e;
|
|
904
827
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
828
|
+
}, true)
|
|
829
|
+
.command(
|
|
830
|
+
["read [target]", "msgs [target]"],
|
|
831
|
+
"Browse messages",
|
|
832
|
+
(y) => y
|
|
833
|
+
.positional("target", { type: "string", describe: "#channel, @user, or URL" })
|
|
834
|
+
.option("limit", { alias: "n", type: "number", default: 20, describe: "Number of messages" }),
|
|
835
|
+
async (argv) => {
|
|
836
|
+
const token = tok(argv as W);
|
|
837
|
+
if (argv.target) await cmdMsgsTarget(token, argv.target, argv.limit);
|
|
838
|
+
else await cmdMsgs(token);
|
|
839
|
+
},
|
|
840
|
+
)
|
|
841
|
+
.command(
|
|
842
|
+
"thread <target> <ts>",
|
|
843
|
+
"Show thread messages",
|
|
844
|
+
(y) => y
|
|
845
|
+
.positional("target", { type: "string", demandOption: true })
|
|
846
|
+
.positional("ts", { type: "string", demandOption: true })
|
|
847
|
+
.option("limit", { alias: "n", type: "number", default: 100 }),
|
|
848
|
+
async (argv) => {
|
|
849
|
+
await cmdThread(tok(argv as W), argv.target!, argv.ts!, argv.limit);
|
|
850
|
+
},
|
|
851
|
+
)
|
|
852
|
+
.command(
|
|
853
|
+
["channel", "channels", "ch"],
|
|
854
|
+
"Channel commands",
|
|
855
|
+
(y) => y
|
|
856
|
+
.command(
|
|
857
|
+
["ls", "list"],
|
|
858
|
+
"List channels",
|
|
859
|
+
(y2) => y2
|
|
860
|
+
.option("limit", { alias: "n", type: "number", default: 200 })
|
|
861
|
+
.option("filter", { alias: "f", type: "string" })
|
|
862
|
+
.option("all", { type: "boolean", default: false })
|
|
863
|
+
.option("format", { type: "string", choices: ["text", "jsonl"] as const, default: "text" })
|
|
864
|
+
.option("json", { type: "boolean", default: false, describe: "Alias for --format=jsonl" }),
|
|
865
|
+
async (argv) => {
|
|
866
|
+
await cmdChannels(tok(argv as W), argv.limit, argv.filter, argv.all, argv.json ? "jsonl" : argv.format);
|
|
867
|
+
},
|
|
868
|
+
)
|
|
869
|
+
.command(
|
|
870
|
+
"get <channel>",
|
|
871
|
+
"Show channel details",
|
|
872
|
+
(y2) => y2.positional("channel", { type: "string", demandOption: true, describe: "#name or channel ID" }),
|
|
873
|
+
async (argv) => {
|
|
874
|
+
const token = tok(argv as W);
|
|
875
|
+
const ref = argv.channel!;
|
|
876
|
+
const channelRef = ref.startsWith("#") || ref.startsWith("@") || ref.startsWith("C") || ref.startsWith("G") || ref.startsWith("D") ? ref : `#${ref}`;
|
|
877
|
+
const channelId = await resolveChannel(token, channelRef);
|
|
878
|
+
const resp = asRecord((await conversationInfo(token, channelId)) as Json);
|
|
879
|
+
const ch = asRecord(resp.channel);
|
|
880
|
+
const name = typeof ch.name === "string" ? ch.name : channelId;
|
|
881
|
+
const isIm = ch.is_im === true;
|
|
882
|
+
const prefix = isIm ? "@" : "#";
|
|
883
|
+
const purpose = String(asRecord(ch.purpose).value ?? "");
|
|
884
|
+
const topic = String(asRecord(ch.topic).value ?? "");
|
|
885
|
+
const memberCount = ch.num_members ?? ch.member_count ?? "";
|
|
886
|
+
console.log(`${prefix}${name} ${channelId}`);
|
|
887
|
+
if (topic) console.log(`topic: ${topic}`);
|
|
888
|
+
if (purpose) console.log(`purpose: ${purpose}`);
|
|
889
|
+
if (memberCount) console.log(`members: ${memberCount}`);
|
|
890
|
+
console.log(`private: ${ch.is_private === true}`);
|
|
891
|
+
},
|
|
892
|
+
)
|
|
893
|
+
.demandCommand(1, "")
|
|
894
|
+
.showHelpOnFail(true),
|
|
895
|
+
)
|
|
896
|
+
.command(
|
|
897
|
+
["user", "usr"],
|
|
898
|
+
"User commands",
|
|
899
|
+
(y) => y
|
|
900
|
+
.command(
|
|
901
|
+
["ls", "list"],
|
|
902
|
+
"List workspace members",
|
|
903
|
+
(y2) => y2
|
|
904
|
+
.option("limit", { alias: "n", type: "number", default: 200 })
|
|
905
|
+
.option("filter", { alias: "f", type: "string" })
|
|
906
|
+
.option("format", { type: "string", choices: ["text", "jsonl", "yaml"] as const, default: "text" })
|
|
907
|
+
.option("json", { type: "boolean", default: false, describe: "Alias for --format=jsonl" }),
|
|
908
|
+
async (argv) => {
|
|
909
|
+
const format = argv.json ? "jsonl" : argv.format;
|
|
910
|
+
const resp = (await listUsers(tok(argv as W))) as Record<string, Json>;
|
|
911
|
+
const filter = argv.filter?.toLowerCase();
|
|
912
|
+
const members = asArray(resp.members)
|
|
913
|
+
.map(asRecord)
|
|
914
|
+
.filter((u) => u.deleted !== true && u.is_bot !== true && String(u.id) !== "USLACKBOT")
|
|
915
|
+
.filter((u) => {
|
|
916
|
+
if (!filter) return true;
|
|
917
|
+
const name = String(u.name ?? "").toLowerCase();
|
|
918
|
+
const real = String(asRecord(u.profile).real_name ?? "").toLowerCase();
|
|
919
|
+
const email = String(asRecord(u.profile).email ?? "").toLowerCase();
|
|
920
|
+
return name.includes(filter) || real.includes(filter) || email.includes(filter);
|
|
921
|
+
})
|
|
922
|
+
.slice(0, argv.limit);
|
|
923
|
+
if (format === "jsonl") {
|
|
924
|
+
for (const u of members) console.log(JSON.stringify(u));
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (format === "yaml") {
|
|
928
|
+
for (const u of members) {
|
|
929
|
+
console.log("---");
|
|
930
|
+
function yamlVal(v: Json, indent = ""): string {
|
|
931
|
+
if (v === null) return "null";
|
|
932
|
+
if (typeof v === "string") return v.includes("\n") ? `|\n ${v.split("\n").join("\n ")}` : v;
|
|
933
|
+
if (typeof v !== "object") return String(v);
|
|
934
|
+
if (Array.isArray(v)) return v.map((i) => `\n${indent} - ${yamlVal(i, indent + " ")}`).join("");
|
|
935
|
+
return Object.entries(v as Record<string, Json>).map(([k, val]) =>
|
|
936
|
+
`\n${indent} ${k}: ${yamlVal(val, indent + " ")}`).join("");
|
|
937
|
+
}
|
|
938
|
+
for (const [k, v] of Object.entries(u)) console.log(`${k}: ${yamlVal(v)}`);
|
|
939
|
+
}
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
943
|
+
for (const u of members) {
|
|
944
|
+
const profile = asRecord(u.profile);
|
|
945
|
+
const handle = String(u.name ?? u.id);
|
|
946
|
+
const id = String(u.id ?? "");
|
|
947
|
+
const display = String(profile.display_name || "");
|
|
948
|
+
const real = String(profile.real_name || "");
|
|
949
|
+
const email = String(profile.email || "");
|
|
950
|
+
const tz = String(u.tz ?? "");
|
|
951
|
+
const names = [...new Set([display, real].filter((s) => s && s !== handle))];
|
|
952
|
+
const parts = names.join(" / ");
|
|
953
|
+
const tzPart = tz && tz !== localTz ? tz : "";
|
|
954
|
+
const meta = [email, tzPart].filter(Boolean).join(" ");
|
|
955
|
+
console.log(`@${handle} ${id} ${parts}${meta ? " " + meta : ""}`);
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
)
|
|
959
|
+
.command(
|
|
960
|
+
"get <user>",
|
|
961
|
+
"Show user details",
|
|
962
|
+
(y2) => y2.positional("user", { type: "string", demandOption: true, describe: "@handle or user ID" }),
|
|
963
|
+
async (argv) => {
|
|
964
|
+
const token = tok(argv as W);
|
|
965
|
+
const ref = argv.user!.startsWith("@") ? argv.user!.slice(1) : argv.user!;
|
|
966
|
+
// If not a Slack user ID (U + alphanumeric), resolve from users list by handle
|
|
967
|
+
let userId = ref;
|
|
968
|
+
if (!/^U[A-Z0-9]+$/.test(ref)) {
|
|
969
|
+
const listResp = (await listUsers(token)) as Record<string, Json>;
|
|
970
|
+
const match = asArray(listResp.members).map(asRecord)
|
|
971
|
+
.find((u) => String(u.name ?? "") === ref);
|
|
972
|
+
if (!match) { console.error(`User not found: @${ref}`); process.exit(1); }
|
|
973
|
+
userId = String(match.id);
|
|
974
|
+
}
|
|
975
|
+
const resp = asRecord((await userInfo(token, userId)) as Json);
|
|
976
|
+
const u = asRecord(resp.user);
|
|
977
|
+
const profile = asRecord(u.profile);
|
|
978
|
+
const localTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
979
|
+
const handle = String(u.name ?? userId);
|
|
980
|
+
const id = String(u.id ?? userId);
|
|
981
|
+
const display = String(profile.display_name || "");
|
|
982
|
+
const real = String(profile.real_name || "");
|
|
983
|
+
const email = String(profile.email || "");
|
|
984
|
+
const tz = String(u.tz ?? "");
|
|
985
|
+
const phone = String(profile.phone || "");
|
|
986
|
+
const title = String(profile.title || "");
|
|
987
|
+
console.log(`@${handle} ${id}`);
|
|
988
|
+
const names = [...new Set([display, real].filter((s) => s && s !== handle))];
|
|
989
|
+
if (names.length) console.log(`name: ${names.join(" / ")}`);
|
|
990
|
+
if (email) console.log(`email: ${email}`);
|
|
991
|
+
if (phone) console.log(`phone: ${phone}`);
|
|
992
|
+
if (title) console.log(`title: ${title}`);
|
|
993
|
+
if (tz && tz !== localTz) console.log(`tz: ${tz}`);
|
|
994
|
+
},
|
|
995
|
+
)
|
|
996
|
+
.demandCommand(1, "")
|
|
997
|
+
.showHelpOnFail(true),
|
|
998
|
+
)
|
|
999
|
+
.command(
|
|
1000
|
+
"news",
|
|
1001
|
+
"Activity feed (mentions to you)",
|
|
1002
|
+
(y) => y.option("limit", { alias: "l", type: "number", default: 20 }),
|
|
1003
|
+
async (argv) => {
|
|
1004
|
+
await cmdNews(tok(argv as W), argv.limit);
|
|
1005
|
+
},
|
|
1006
|
+
)
|
|
1007
|
+
.command(
|
|
1008
|
+
"search <query>",
|
|
1009
|
+
"Full-text search",
|
|
1010
|
+
(y) => y
|
|
1011
|
+
.positional("query", { type: "string", demandOption: true })
|
|
1012
|
+
.option("count", { alias: "n", type: "number", default: 100 })
|
|
1013
|
+
.option("json", { type: "boolean", default: false, describe: "Output raw JSON" }),
|
|
1014
|
+
async (argv) => {
|
|
1015
|
+
await cmdSearch(tok(argv as W), argv.query!, argv.count, argv.json);
|
|
1016
|
+
},
|
|
1017
|
+
)
|
|
1018
|
+
.command(
|
|
1019
|
+
"send <target> <message>",
|
|
1020
|
+
"Send a message (confirm-hash safety gate)",
|
|
1021
|
+
(y) => y
|
|
1022
|
+
.positional("target", { type: "string", demandOption: true })
|
|
1023
|
+
.positional("message", { type: "string", demandOption: true })
|
|
1024
|
+
.option("thread", { type: "string", describe: "Thread timestamp" })
|
|
1025
|
+
.option("code", { type: "string", describe: "Safety hash to confirm send" })
|
|
1026
|
+
.option("channel-id", { type: "string", describe: "Raw channel ID" })
|
|
1027
|
+
.option("user-id", { type: "string", describe: "Raw user ID (opens DM)" }),
|
|
1028
|
+
async (argv) => {
|
|
1029
|
+
const args: SendArgs = { target: argv.target!, message: argv.message! };
|
|
1030
|
+
if (argv.thread) args.thread = argv.thread;
|
|
1031
|
+
if (argv.code) args.code = argv.code;
|
|
1032
|
+
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1033
|
+
if (argv["user-id"]) args.userId = argv["user-id"];
|
|
1034
|
+
await cmdSend(tok(argv as W), args);
|
|
1035
|
+
},
|
|
1036
|
+
)
|
|
1037
|
+
.command(
|
|
1038
|
+
"schedule",
|
|
1039
|
+
"Manage scheduled messages",
|
|
1040
|
+
(y) => y
|
|
1041
|
+
.command(
|
|
1042
|
+
"send <target> <message>",
|
|
1043
|
+
"Schedule a message for later delivery",
|
|
1044
|
+
(y2) => y2
|
|
1045
|
+
.positional("target", { type: "string", demandOption: true })
|
|
1046
|
+
.positional("message", { type: "string", demandOption: true })
|
|
1047
|
+
.option("at", { type: "string", demandOption: true, describe: "Delivery time (ISO datetime or Unix ts)" })
|
|
1048
|
+
.option("thread", { type: "string", describe: "Thread timestamp" })
|
|
1049
|
+
.option("code", { type: "string", describe: "Safety hash to confirm" })
|
|
1050
|
+
.option("channel-id", { type: "string", describe: "Raw channel ID" })
|
|
1051
|
+
.option("user-id", { type: "string", describe: "Raw user ID (opens DM)" }),
|
|
1052
|
+
async (argv) => {
|
|
1053
|
+
const args: ScheduleSendArgs = { target: argv.target!, message: argv.message!, at: argv.at! };
|
|
1054
|
+
if (argv.thread) args.thread = argv.thread;
|
|
1055
|
+
if (argv.code) args.code = argv.code;
|
|
1056
|
+
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1057
|
+
if (argv["user-id"]) args.userId = argv["user-id"];
|
|
1058
|
+
await cmdScheduleSend(tok(argv as W), args);
|
|
1059
|
+
},
|
|
1060
|
+
)
|
|
1061
|
+
.command(
|
|
1062
|
+
["ls", "list"],
|
|
1063
|
+
"List pending scheduled messages",
|
|
1064
|
+
(y2) => y2
|
|
1065
|
+
.positional("target", { type: "string", describe: "#channel to filter by" })
|
|
1066
|
+
.option("channel-id", { type: "string", describe: "Raw channel ID" }),
|
|
1067
|
+
async (argv) => {
|
|
1068
|
+
await cmdScheduleList(tok(argv as W), argv.target as string | undefined, argv["channel-id"]);
|
|
1069
|
+
},
|
|
1070
|
+
)
|
|
1071
|
+
.command(
|
|
1072
|
+
"rm <target> <id>",
|
|
1073
|
+
"Delete a scheduled message",
|
|
1074
|
+
(y2) => y2
|
|
1075
|
+
.positional("target", { type: "string", demandOption: true, describe: "#channel or @user" })
|
|
1076
|
+
.positional("id", { type: "string", demandOption: true, describe: "Scheduled message ID" })
|
|
1077
|
+
.option("code", { type: "string", describe: "Safety hash to confirm" })
|
|
1078
|
+
.option("channel-id", { type: "string", describe: "Raw channel ID" }),
|
|
1079
|
+
async (argv) => {
|
|
1080
|
+
const args: ScheduleRmArgs = { target: argv.target!, id: argv.id! };
|
|
1081
|
+
if (argv.code) args.code = argv.code;
|
|
1082
|
+
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1083
|
+
await cmdScheduleRm(tok(argv as W), args);
|
|
1084
|
+
},
|
|
1085
|
+
)
|
|
1086
|
+
.demandCommand(1, "")
|
|
1087
|
+
.showHelpOnFail(true),
|
|
1088
|
+
() => {},
|
|
1089
|
+
)
|
|
1090
|
+
.command(
|
|
1091
|
+
"edit <target> <newText>",
|
|
1092
|
+
"Edit a sent message",
|
|
1093
|
+
(y) => y
|
|
1094
|
+
.positional("target", { type: "string", demandOption: true, describe: "#chan:ts, @user:ts, or permalink" })
|
|
1095
|
+
.positional("newText", { type: "string", demandOption: true })
|
|
1096
|
+
.option("code", { type: "string", describe: "Safety hash to confirm edit" })
|
|
1097
|
+
.option("channel-id", { type: "string", describe: "Raw channel ID" }),
|
|
1098
|
+
async (argv) => {
|
|
1099
|
+
const args: EditArgs = { target: argv.target!, newText: argv.newText! };
|
|
1100
|
+
if (argv.code) args.code = argv.code;
|
|
1101
|
+
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1102
|
+
await cmdEdit(tok(argv as W), args);
|
|
1103
|
+
},
|
|
1104
|
+
)
|
|
1105
|
+
.command(
|
|
1106
|
+
"upload <target> <file..>",
|
|
1107
|
+
"Upload one or more files to a channel or DM",
|
|
1108
|
+
(y) => y
|
|
1109
|
+
.positional("target", { type: "string", demandOption: true })
|
|
1110
|
+
.positional("file", { type: "string", array: true, demandOption: true, describe: "Path(s) to file(s)" })
|
|
1111
|
+
.option("title", { type: "string", describe: "Title (single file only)" })
|
|
1112
|
+
.option("thread", { type: "string", describe: "Thread timestamp" })
|
|
1113
|
+
.option("comment", { type: "string", describe: "Initial comment (first file)" })
|
|
1114
|
+
.option("code", { type: "string", describe: "4-hex safety code to confirm upload" })
|
|
1115
|
+
.option("channel-id", { type: "string" })
|
|
1116
|
+
.option("user-id", { type: "string" }),
|
|
1117
|
+
async (argv) => {
|
|
1118
|
+
const args: UploadArgs = { target: argv.target!, filePaths: argv.file as string[] };
|
|
1119
|
+
if (argv.title) args.title = argv.title;
|
|
1120
|
+
if (argv.thread) args.thread = argv.thread;
|
|
1121
|
+
if (argv.comment) args.comment = argv.comment;
|
|
1122
|
+
if (argv.code) args.code = argv.code;
|
|
1123
|
+
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1124
|
+
if (argv["user-id"]) args.userId = argv["user-id"];
|
|
1125
|
+
await cmdUpload(tok(argv as W), args);
|
|
1126
|
+
},
|
|
1127
|
+
)
|
|
1128
|
+
.command(
|
|
1129
|
+
"dump",
|
|
1130
|
+
"Bulk export channel history as markdown",
|
|
1131
|
+
(y) => y
|
|
1132
|
+
.option("days", { alias: "d", type: "number", default: 7, describe: "Days of history" })
|
|
1133
|
+
.option("limit", { alias: "l", type: "number", default: 200 })
|
|
1134
|
+
.option("filter", { alias: "f", type: "string", describe: "Filter channel names" }),
|
|
1135
|
+
async (argv) => {
|
|
1136
|
+
await cmdDump(tok(argv as W), argv.days, argv.limit, argv.filter);
|
|
1137
|
+
},
|
|
1138
|
+
)
|
|
1139
|
+
.command(
|
|
1140
|
+
"drafts",
|
|
1141
|
+
"Manage message drafts (requires xoxc desktop token)",
|
|
1142
|
+
(y) => y
|
|
1143
|
+
.command(
|
|
1144
|
+
["$0", "ls", "list"],
|
|
1145
|
+
"List pending drafts",
|
|
1146
|
+
(y2) => y2.option("all", { alias: "a", type: "boolean", default: false, describe: "Include sent drafts" }),
|
|
1147
|
+
async (argv) => {
|
|
1148
|
+
await cmdDrafts(tok(argv as W), ck(argv as W), argv.all);
|
|
1149
|
+
},
|
|
1150
|
+
)
|
|
1151
|
+
.command(
|
|
1152
|
+
["new <channel> [text..]", "save <channel> [text..]"],
|
|
1153
|
+
"Create a draft",
|
|
1154
|
+
(y2) => y2
|
|
1155
|
+
.positional("channel", { type: "string", demandOption: true })
|
|
1156
|
+
.positional("text", { type: "string", array: true, default: [] }),
|
|
1157
|
+
async (argv) => {
|
|
1158
|
+
const token = tok(argv as W); const cookie = ck(argv as W);
|
|
1159
|
+
const text = (argv.text as string[]).join(" ");
|
|
1160
|
+
if (!text) { console.error("Usage: slack drafts new <#channel|@user> <text>"); process.exit(2); }
|
|
1161
|
+
const channelId = await resolveChannel(token, argv.channel!, cookie);
|
|
1162
|
+
const resp = (await createDraft(token, channelId, text, cookie)) as Record<string, Json>;
|
|
1163
|
+
console.log(`✓ Draft created (id: ${asRecord(resp.draft).id ?? "?"})`);
|
|
1164
|
+
},
|
|
1165
|
+
)
|
|
1166
|
+
.command(
|
|
1167
|
+
"get <id>",
|
|
1168
|
+
"Show a draft",
|
|
1169
|
+
(y2) => y2.positional("id", { type: "string", demandOption: true }),
|
|
1170
|
+
async (argv) => {
|
|
1171
|
+
await cmdDraftGet(tok(argv as W), ck(argv as W), argv.id!);
|
|
1172
|
+
},
|
|
1173
|
+
)
|
|
1174
|
+
.command(
|
|
1175
|
+
["edit <id> [text..]", "update <id> [text..]"],
|
|
1176
|
+
"Edit a draft",
|
|
1177
|
+
(y2) => y2
|
|
1178
|
+
.positional("id", { type: "string", demandOption: true })
|
|
1179
|
+
.positional("text", { type: "string", array: true, default: [] })
|
|
1180
|
+
.option("code", { type: "string" }),
|
|
1181
|
+
async (argv) => {
|
|
1182
|
+
const token = tok(argv as W); const cookie = ck(argv as W);
|
|
1183
|
+
const text = (argv.text as string[]).join(" ");
|
|
1184
|
+
if (!text) { console.error("Usage: slack drafts edit <id> <new-text>"); process.exit(2); }
|
|
1185
|
+
const listResp = (await listDrafts(token, cookie)) as Record<string, Json>;
|
|
1186
|
+
const d = asArray(listResp.drafts).map(asRecord).find((x) => String(x.id) === argv.id);
|
|
1187
|
+
if (!d) { console.error(`Draft not found: ${argv.id}`); process.exit(1); }
|
|
1188
|
+
const prevText = draftText(d);
|
|
1189
|
+
const code = safetyCode(prevText, text);
|
|
1190
|
+
if (argv.code !== code) requireCode(argv.code, code, [
|
|
1191
|
+
`--- Current draft ----------------------------`,
|
|
1192
|
+
...prevText.split("\n").map((l) => ` ${l}`),
|
|
1193
|
+
`--- Replacing with ---------------------------`,
|
|
1194
|
+
...text.split("\n").map((l) => ` ${l}`),
|
|
1195
|
+
`--------------------------------────────────`,
|
|
1196
|
+
]);
|
|
1197
|
+
const resp = (await updateDraft(token, argv.id!, draftChannelId(d), text, cookie)) as Record<string, Json>;
|
|
1198
|
+
console.log(`✓ Draft updated (id: ${asRecord(resp.draft).id ?? "?"})`);
|
|
1199
|
+
},
|
|
1200
|
+
)
|
|
1201
|
+
.command(
|
|
1202
|
+
["delete <id>", "rm <id>"],
|
|
1203
|
+
"Delete a draft",
|
|
1204
|
+
(y2) => y2
|
|
1205
|
+
.positional("id", { type: "string", demandOption: true })
|
|
1206
|
+
.option("code", { type: "string" }),
|
|
1207
|
+
async (argv) => {
|
|
1208
|
+
const token = tok(argv as W); const cookie = ck(argv as W);
|
|
1209
|
+
const listResp = (await listDrafts(token, cookie)) as Record<string, Json>;
|
|
1210
|
+
const d = asArray(listResp.drafts).map(asRecord).find((x) => String(x.id) === argv.id);
|
|
1211
|
+
if (!d) { console.error(`Draft not found: ${argv.id}`); process.exit(1); }
|
|
1212
|
+
const prevText = draftText(d);
|
|
1213
|
+
const code = safetyCode(argv.id!, prevText);
|
|
1214
|
+
if (argv.code !== code) requireCode(argv.code, code, [
|
|
1215
|
+
`─-- Deleting draft ───────────────────────────`,
|
|
1216
|
+
` id: ${argv.id}`,
|
|
1217
|
+
...prevText.split("\n").map((l) => ` ${l}`),
|
|
1218
|
+
`--------------------------------────────────`,
|
|
1219
|
+
]);
|
|
1220
|
+
await deleteDraft(token, argv.id!, cookie);
|
|
1221
|
+
console.log(`✓ Draft deleted (id: ${argv.id})`);
|
|
1222
|
+
},
|
|
1223
|
+
),
|
|
1224
|
+
)
|
|
1225
|
+
.command(
|
|
1226
|
+
"auth",
|
|
1227
|
+
"Authentication and workspace management",
|
|
1228
|
+
(y) => y
|
|
1229
|
+
.command(
|
|
1230
|
+
"login",
|
|
1231
|
+
"Add a workspace (interactive or --token for non-interactive)",
|
|
1232
|
+
(y2) => y2
|
|
1233
|
+
.option("token", { type: "string", describe: "Token to save directly (non-interactive)" })
|
|
1234
|
+
.option("name", { type: "string", describe: "Workspace name (used with --token)" }),
|
|
1235
|
+
async (argv) => {
|
|
1236
|
+
await cmdAuthLogin({
|
|
1237
|
+
...(argv.token !== undefined ? { token: argv.token } : {}),
|
|
1238
|
+
...(argv.name !== undefined ? { name: argv.name } : {}),
|
|
1239
|
+
});
|
|
1240
|
+
},
|
|
1241
|
+
)
|
|
1242
|
+
.command(["ls", "status"], "Show auth status", () => {}, () => {
|
|
1243
|
+
const profiles = listProfiles();
|
|
1244
|
+
if (profiles.length === 0) { console.log("No workspaces configured. Run: slack auth login"); return; }
|
|
1245
|
+
for (const { name, profile, current } of profiles)
|
|
1246
|
+
console.log(`${current ? "* " : " "}${name} ${profile.team} (${profile.user}) ${profile.url ?? ""}`);
|
|
1247
|
+
})
|
|
1248
|
+
.command(
|
|
1249
|
+
"use <name>",
|
|
1250
|
+
"Switch active workspace",
|
|
1251
|
+
(y2) => y2
|
|
1252
|
+
.positional("name", { type: "string", demandOption: true })
|
|
1253
|
+
.option("g", { type: "boolean", default: false, describe: "Write global lockfile (~/.slack-cli/workspace)" }),
|
|
1254
|
+
(argv) => {
|
|
1255
|
+
if (!argv.g) ensureSlackCliDir(join(process.cwd(), ".slack-cli"));
|
|
1256
|
+
useProfile(argv.name!, argv.g);
|
|
1257
|
+
console.log(`Switched to workspace "${argv.name}" ${argv.g ? "globally" : "locally"}`);
|
|
1258
|
+
},
|
|
1259
|
+
)
|
|
1260
|
+
.command(
|
|
1261
|
+
["rm <name>", "remove <name>"],
|
|
1262
|
+
"Remove a workspace",
|
|
1263
|
+
(y2) => y2.positional("name", { type: "string", demandOption: true }),
|
|
1264
|
+
(argv) => {
|
|
1265
|
+
removeProfile(argv.name!);
|
|
1266
|
+
console.log(`Removed workspace "${argv.name}"`);
|
|
1267
|
+
},
|
|
1268
|
+
)
|
|
1269
|
+
.command("$0", false as unknown as string, () => {}, () => { y.showHelp(); process.exit(0); }),
|
|
1270
|
+
)
|
|
1271
|
+
.command(
|
|
1272
|
+
"tail [target]",
|
|
1273
|
+
"Stream new messages in real time (like tail -f)",
|
|
1274
|
+
(y) => y
|
|
1275
|
+
.positional("target", { type: "string", describe: "#channel or @user to follow" })
|
|
1276
|
+
.option("since", { type: "string", describe: "Backfill from N ago (e.g. 10m, 2h, 1d)" })
|
|
1277
|
+
.option("thread", { type: "string", describe: "Follow a single thread by timestamp" })
|
|
1278
|
+
.option("me", { type: "boolean", default: false, describe: "Filter to messages that mention you" })
|
|
1279
|
+
.option("interval", { type: "number", default: 60000, describe: "Poll interval in ms (default 60s; use --interval=3000 for near-real-time)" })
|
|
1280
|
+
.option("rtm", { type: "boolean", default: true, describe: "Use RTM WebSocket when available (xoxc + cookie); pass --no-rtm to force polling" }),
|
|
1281
|
+
async (argv) => {
|
|
1282
|
+
const token = tok(argv as W);
|
|
1283
|
+
const cookie = ck(argv as W);
|
|
1284
|
+
const signal = new AbortController();
|
|
1285
|
+
process.on("SIGINT", () => { signal.abort(); process.exit(0); });
|
|
1286
|
+
await cmdTail(token, argv.target, {
|
|
1287
|
+
...(argv.since !== undefined ? { since: argv.since } : {}),
|
|
1288
|
+
...(argv.thread !== undefined ? { thread: argv.thread } : {}),
|
|
1289
|
+
me: argv.me,
|
|
1290
|
+
interval: argv.interval,
|
|
1291
|
+
...(cookie !== undefined ? { cookie } : {}),
|
|
1292
|
+
...(argv.rtm === false ? { noRtm: true } : {}),
|
|
1293
|
+
}, signal.signal);
|
|
1294
|
+
},
|
|
1295
|
+
)
|
|
1296
|
+
.command("login", false as unknown as string, (y2) => y2
|
|
1297
|
+
.option("token", { type: "string" })
|
|
1298
|
+
.option("name", { type: "string" }), async (argv) => {
|
|
1299
|
+
await cmdAuthLogin({
|
|
1300
|
+
...(argv.token !== undefined ? { token: argv.token } : {}),
|
|
1301
|
+
...(argv.name !== undefined ? { name: argv.name } : {}),
|
|
930
1302
|
});
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
if (values["channel-id"] !== undefined) sendArgs.channelId = values["channel-id"];
|
|
938
|
-
if (values["user-id"] !== undefined) sendArgs.userId = values["user-id"];
|
|
939
|
-
await cmdSend(token, sendArgs);
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
case "edit": {
|
|
943
|
-
const { values, positionals } = parseArgs({
|
|
944
|
-
args: rest,
|
|
945
|
-
allowPositionals: true,
|
|
946
|
-
options: {
|
|
947
|
-
code: { type: "string" },
|
|
948
|
-
"channel-id": { type: "string" },
|
|
949
|
-
},
|
|
950
|
-
strict: true,
|
|
951
|
-
});
|
|
952
|
-
const target = positionals[0];
|
|
953
|
-
const newText = positionals[1];
|
|
954
|
-
if (!target || !newText) usage();
|
|
955
|
-
const editArgs: EditArgs = { target, newText };
|
|
956
|
-
if (values.code !== undefined) editArgs.code = values.code;
|
|
957
|
-
if (values["channel-id"] !== undefined) editArgs.channelId = values["channel-id"];
|
|
958
|
-
await cmdEdit(token, editArgs);
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
case "upload": {
|
|
962
|
-
const { values, positionals } = parseArgs({
|
|
963
|
-
args: rest,
|
|
964
|
-
allowPositionals: true,
|
|
965
|
-
options: {
|
|
966
|
-
title: { type: "string" },
|
|
967
|
-
thread: { type: "string" },
|
|
968
|
-
comment: { type: "string" },
|
|
969
|
-
code: { type: "string" },
|
|
970
|
-
"channel-id": { type: "string" },
|
|
971
|
-
"user-id": { type: "string" },
|
|
972
|
-
},
|
|
973
|
-
strict: true,
|
|
974
|
-
});
|
|
975
|
-
const target = positionals[0];
|
|
976
|
-
const filePath = positionals[1];
|
|
977
|
-
if (!target || !filePath) usage();
|
|
978
|
-
const uploadArgs: UploadArgs = { target, filePath };
|
|
979
|
-
if (values.title !== undefined) uploadArgs.title = values.title;
|
|
980
|
-
if (values.thread !== undefined) uploadArgs.thread = values.thread;
|
|
981
|
-
if (values.comment !== undefined) uploadArgs.comment = values.comment;
|
|
982
|
-
if (values.code !== undefined) uploadArgs.code = values.code;
|
|
983
|
-
if (values["channel-id"] !== undefined) uploadArgs.channelId = values["channel-id"];
|
|
984
|
-
if (values["user-id"] !== undefined) uploadArgs.userId = values["user-id"];
|
|
985
|
-
await cmdUpload(token, uploadArgs);
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
case "dump": {
|
|
989
|
-
const { values } = parseArgs({
|
|
990
|
-
args: rest,
|
|
991
|
-
options: {
|
|
992
|
-
days: { type: "string", short: "d", default: "7" },
|
|
993
|
-
limit: { type: "string", short: "l", default: "200" },
|
|
994
|
-
filter: { type: "string", short: "f" },
|
|
995
|
-
},
|
|
996
|
-
strict: true,
|
|
997
|
-
});
|
|
998
|
-
await cmdDump(token, Number(values.days), Number(values.limit), values.filter);
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
default:
|
|
1002
|
-
usage();
|
|
1003
|
-
}
|
|
1303
|
+
})
|
|
1304
|
+
.demandCommand(1, "Specify a command. Run with --help for usage.")
|
|
1305
|
+
.strict()
|
|
1306
|
+
.help()
|
|
1307
|
+
.alias("help", "h")
|
|
1308
|
+
.parseAsync();
|
|
1004
1309
|
}
|
|
1005
1310
|
|
|
1006
1311
|
main().catch((e: unknown) => {
|
|
1007
|
-
|
|
1008
|
-
console.error(`Error: ${msg}`);
|
|
1312
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
1009
1313
|
process.exit(1);
|
|
1010
1314
|
});
|