slack-term 1.23.0 → 1.24.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/SKILL.md +5 -0
- package/dist/cli.js +85 -81
- package/package.json +4 -2
- package/ts/cli.ts +230 -58
- package/ts/slack.ts +92 -44
package/ts/cli.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
|
|
9
9
|
import yargs from "yargs";
|
|
10
10
|
import { hideBin } from "yargs/helpers";
|
|
11
|
-
import { listProfiles, removeProfile, resolveBotToken, resolveCookie, resolveToken, useProfile } from "./profiles.ts";
|
|
11
|
+
import { listProfiles, removeProfile, resolveBotToken, resolveCookie, resolveToken, useProfile, type Profile } from "./profiles.ts";
|
|
12
12
|
import { diagnoseBotMessaging, formatDiagnosis } from "./botdoctor.ts";
|
|
13
13
|
import { cmdAuthLogin, cmdAuthChrome, cmdAuthFirefox, cmdAuthToken, cmdAuthApp } from "./auth.ts";
|
|
14
14
|
import { cmdTail } from "./tail.ts";
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
reactionAdd,
|
|
29
29
|
reactionRemove,
|
|
30
30
|
filesInfo,
|
|
31
|
+
filesList,
|
|
31
32
|
history,
|
|
32
33
|
listConversations,
|
|
33
34
|
listDrafts,
|
|
@@ -387,8 +388,52 @@ async function cmdChannels(token: string, limit: number, filter?: string, all?:
|
|
|
387
388
|
}
|
|
388
389
|
|
|
389
390
|
// --- search ---
|
|
390
|
-
|
|
391
|
-
|
|
391
|
+
|
|
392
|
+
/** search.messages (and most read/write Web API methods) reject a bot token
|
|
393
|
+
* (xoxb-) with not_allowed_token_type — it's a user-only endpoint. Find a
|
|
394
|
+
* sibling profile that holds a user token (xoxp-/xoxc-) for the *same*
|
|
395
|
+
* workspace (teamId) as the bot profile currently in use, so the caller can
|
|
396
|
+
* retry with it automatically instead of just erroring. Deliberately does
|
|
397
|
+
* NOT fall back to a user-token profile for a different workspace — that
|
|
398
|
+
* would silently run the search against the wrong (possibly private)
|
|
399
|
+
* workspace instead of failing loudly. Returns undefined (no fallback) when
|
|
400
|
+
* the current token isn't a known profile at all, since its workspace can't
|
|
401
|
+
* be verified. */
|
|
402
|
+
function findUserTokenProfile(currentToken: string): { name: string; profile: Profile } | undefined {
|
|
403
|
+
const isUserToken = (t: string) => t.startsWith("xoxp-") || t.startsWith("xoxc-");
|
|
404
|
+
const profiles = listProfiles();
|
|
405
|
+
const current = profiles.find((p) => p.profile.token === currentToken);
|
|
406
|
+
if (!current) return undefined;
|
|
407
|
+
return profiles.find((p) => isUserToken(p.profile.token) && p.profile.teamId === current.profile.teamId);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function cmdSearch(token: string, query: string, count: number, json: boolean, cookie?: string): Promise<void> {
|
|
411
|
+
let searchToken = token;
|
|
412
|
+
let searchCookie = cookie;
|
|
413
|
+
let resp: Json;
|
|
414
|
+
try {
|
|
415
|
+
resp = await searchAll(searchToken, query, count, searchCookie);
|
|
416
|
+
} catch (e: unknown) {
|
|
417
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
418
|
+
if (token.startsWith("xoxb-") && msg.includes("not_allowed_token_type")) {
|
|
419
|
+
const fallback = findUserTokenProfile(token);
|
|
420
|
+
if (fallback) {
|
|
421
|
+
console.error(`Note: search.messages needs a user token — the active profile is a bot token (xoxb-); falling back to profile "${fallback.name}".`);
|
|
422
|
+
searchToken = fallback.profile.token;
|
|
423
|
+
searchCookie = fallback.profile.cookie;
|
|
424
|
+
resp = await searchAll(searchToken, query, count, searchCookie);
|
|
425
|
+
} else {
|
|
426
|
+
console.error(
|
|
427
|
+
"Error: search requires a user token (xoxp-/xoxc-) — the active profile is a bot token (xoxb-), which Slack rejects for search.messages.\n" +
|
|
428
|
+
" Add a user-token profile: slack auth login (or slack auth token)\n" +
|
|
429
|
+
" Then select it: slack auth use <name> (or pass --workspace <name>)",
|
|
430
|
+
);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
throw e;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
392
437
|
if (json) {
|
|
393
438
|
console.log(JSON.stringify(resp, null, 2));
|
|
394
439
|
return;
|
|
@@ -403,7 +448,7 @@ async function cmdSearch(token: string, query: string, count: number, json: bool
|
|
|
403
448
|
if (isIm && rawName.startsWith("U")) {
|
|
404
449
|
const handleKey = "@" + rawName;
|
|
405
450
|
if (!cache.has(handleKey)) {
|
|
406
|
-
const [, h] = await userInfoPair(
|
|
451
|
+
const [, h] = await userInfoPair(searchToken, rawName, searchCookie);
|
|
407
452
|
cache.set(handleKey, h);
|
|
408
453
|
}
|
|
409
454
|
chLabel = `@${cache.get(handleKey) ?? rawName}`;
|
|
@@ -412,7 +457,7 @@ async function cmdSearch(token: string, query: string, count: number, json: bool
|
|
|
412
457
|
} else {
|
|
413
458
|
chLabel = `#${rawName}`;
|
|
414
459
|
}
|
|
415
|
-
console.log(await formatMsgLine(
|
|
460
|
+
console.log(await formatMsgLine(searchToken, m, cache, chLabel));
|
|
416
461
|
}
|
|
417
462
|
}
|
|
418
463
|
|
|
@@ -705,15 +750,15 @@ function parseTargetThread(s: string): { ref: string; threadTs?: string } {
|
|
|
705
750
|
/** Human-readable destination label for confirm gates, e.g. "#symval (C0B0L6SSAMD)".
|
|
706
751
|
* When ref is already a #channel/@user label, keep it; otherwise resolve the channel
|
|
707
752
|
* name via conversations.info (fail-soft: a raw ID is still unambiguous). */
|
|
708
|
-
async function destLabel(token: string, channelId: string, ref: string): Promise<string> {
|
|
753
|
+
async function destLabel(token: string, channelId: string, ref: string, cookie?: string): Promise<string> {
|
|
709
754
|
let name = ref;
|
|
710
755
|
if (!ref.startsWith("#") && !ref.startsWith("@")) {
|
|
711
756
|
try {
|
|
712
|
-
const info = asRecord((await conversationInfo(token, channelId)) as Json);
|
|
757
|
+
const info = asRecord((await conversationInfo(token, channelId, cookie)) as Json);
|
|
713
758
|
const ch = asRecord(info.channel);
|
|
714
759
|
if (ch.is_im === true) {
|
|
715
760
|
const uid = typeof ch.user === "string" ? ch.user : "";
|
|
716
|
-
name = uid ? `@${await userName(token, uid)}` : channelId;
|
|
761
|
+
name = uid ? `@${await userName(token, uid, cookie)}` : channelId;
|
|
717
762
|
} else {
|
|
718
763
|
name = typeof ch.name === "string" ? `#${ch.name}` : channelId;
|
|
719
764
|
}
|
|
@@ -726,9 +771,9 @@ async function destLabel(token: string, channelId: string, ref: string): Promise
|
|
|
726
771
|
|
|
727
772
|
/** One-line description of a single thread message for confirm gates:
|
|
728
773
|
* "2026-06-11 08:05 @handle: head…". `headLen` bounds the text preview. */
|
|
729
|
-
async function threadMsgLine(token: string, m: Record<string, Json>, headLen = 60): Promise<string> {
|
|
774
|
+
async function threadMsgLine(token: string, m: Record<string, Json>, headLen = 60, cookie?: string): Promise<string> {
|
|
730
775
|
const author = typeof m.user === "string"
|
|
731
|
-
? await userName(token, m.user)
|
|
776
|
+
? await userName(token, m.user, cookie)
|
|
732
777
|
: typeof m.username === "string"
|
|
733
778
|
? m.username
|
|
734
779
|
: "?";
|
|
@@ -741,10 +786,10 @@ async function threadMsgLine(token: string, m: Record<string, Json>, headLen = 6
|
|
|
741
786
|
|
|
742
787
|
/** One-line thread-parent description for confirm gates: "2026-06-11 08:05 @handle: head…".
|
|
743
788
|
* Fail-soft: returns the raw ts if the parent cannot be found. */
|
|
744
|
-
async function threadParentLine(token: string, threadTs: string, threadMsgs: Record<string, Json>[]): Promise<string> {
|
|
789
|
+
async function threadParentLine(token: string, threadTs: string, threadMsgs: Record<string, Json>[], cookie?: string): Promise<string> {
|
|
745
790
|
const parent = threadMsgs.find((m) => String(m.ts) === threadTs) ?? threadMsgs[0];
|
|
746
791
|
if (!parent) return threadTs;
|
|
747
|
-
return threadMsgLine(token, parent, 40);
|
|
792
|
+
return threadMsgLine(token, parent, 40, cookie);
|
|
748
793
|
}
|
|
749
794
|
|
|
750
795
|
/** Normalize a message for duplicate comparison: lowercase, collapse whitespace,
|
|
@@ -800,6 +845,9 @@ interface EditArgs {
|
|
|
800
845
|
code?: string;
|
|
801
846
|
channelId?: string;
|
|
802
847
|
mentions?: boolean;
|
|
848
|
+
// Session cookie (xoxd) for `token` — required for an xoxc- desktop session
|
|
849
|
+
// token to be accepted by the public Slack API at all.
|
|
850
|
+
cookie?: string;
|
|
803
851
|
}
|
|
804
852
|
async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
805
853
|
const { ref, ts } = splitRefTs(args.target);
|
|
@@ -810,10 +858,10 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
|
810
858
|
|
|
811
859
|
let channelId: string;
|
|
812
860
|
if (args.channelId) channelId = args.channelId;
|
|
813
|
-
else channelId = await resolveChannel(token, ref);
|
|
861
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
814
862
|
|
|
815
863
|
// Fetch the message to display the original text and compute the safety hash.
|
|
816
|
-
const resp = (await replies(token, channelId, ts, 1)) as Record<string, Json>;
|
|
864
|
+
const resp = (await replies(token, channelId, ts, 1, args.cookie)) as Record<string, Json>;
|
|
817
865
|
const msgs = asArray(resp.messages).map(asRecord);
|
|
818
866
|
const original = msgs.find((m) => String(m.ts) === ts);
|
|
819
867
|
if (!original) {
|
|
@@ -824,7 +872,7 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
|
824
872
|
|
|
825
873
|
// Convert @handle → <@USERID> before hashing/editing (unresolved stay as text).
|
|
826
874
|
const newText = args.mentions
|
|
827
|
-
? await encodeMentions(token, args.newText, channelId)
|
|
875
|
+
? await encodeMentions(token, args.newText, channelId, args.cookie ? { cookie: args.cookie } : {})
|
|
828
876
|
: args.newText;
|
|
829
877
|
|
|
830
878
|
const code = safetyCode(originalText, newText);
|
|
@@ -838,7 +886,7 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
|
838
886
|
]);
|
|
839
887
|
}
|
|
840
888
|
|
|
841
|
-
const newTs = await editMessage(token, channelId, ts, newText);
|
|
889
|
+
const newTs = await editMessage(token, channelId, ts, newText, args.cookie);
|
|
842
890
|
console.log(`✓ Edited (ts: ${newTs})`);
|
|
843
891
|
}
|
|
844
892
|
|
|
@@ -847,6 +895,7 @@ interface DeleteArgs {
|
|
|
847
895
|
target: string;
|
|
848
896
|
code?: string;
|
|
849
897
|
channelId?: string;
|
|
898
|
+
cookie?: string;
|
|
850
899
|
}
|
|
851
900
|
async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
852
901
|
const { ref, ts } = splitRefTs(args.target);
|
|
@@ -857,10 +906,10 @@ async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
|
857
906
|
|
|
858
907
|
let channelId: string;
|
|
859
908
|
if (args.channelId) channelId = args.channelId;
|
|
860
|
-
else channelId = await resolveChannel(token, ref);
|
|
909
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
861
910
|
|
|
862
911
|
// Fetch the message to display what is being deleted and compute the safety hash.
|
|
863
|
-
const resp = (await replies(token, channelId, ts, 1)) as Record<string, Json>;
|
|
912
|
+
const resp = (await replies(token, channelId, ts, 1, args.cookie)) as Record<string, Json>;
|
|
864
913
|
const msgs = asArray(resp.messages).map(asRecord);
|
|
865
914
|
const original = msgs.find((m) => String(m.ts) === ts);
|
|
866
915
|
if (!original) {
|
|
@@ -871,7 +920,7 @@ async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
|
871
920
|
|
|
872
921
|
const code = safetyCode(channelId, ts, originalText);
|
|
873
922
|
if (args.code !== code) {
|
|
874
|
-
const dest = await destLabel(token, channelId, ref);
|
|
923
|
+
const dest = await destLabel(token, channelId, ref, args.cookie);
|
|
875
924
|
requireCode(args.code, code, [
|
|
876
925
|
`--- Deleting message -------------------------`,
|
|
877
926
|
` → ${dest} at ${slackTsToIso(ts)}`,
|
|
@@ -880,7 +929,7 @@ async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
|
880
929
|
]);
|
|
881
930
|
}
|
|
882
931
|
|
|
883
|
-
await deleteMessage(token, channelId, ts);
|
|
932
|
+
await deleteMessage(token, channelId, ts, args.cookie);
|
|
884
933
|
console.log(`✓ Deleted (ts: ${ts})`);
|
|
885
934
|
}
|
|
886
935
|
|
|
@@ -890,6 +939,7 @@ interface ReactArgs {
|
|
|
890
939
|
emoji: string;
|
|
891
940
|
remove?: boolean;
|
|
892
941
|
channelId?: string;
|
|
942
|
+
cookie?: string;
|
|
893
943
|
}
|
|
894
944
|
// Add (or --remove) an emoji reaction. No confirm gate: a reaction is trivial
|
|
895
945
|
// and fully reversible (`--remove`), and the whole point is a lightweight ack
|
|
@@ -905,13 +955,13 @@ async function cmdReact(token: string, args: ReactArgs): Promise<void> {
|
|
|
905
955
|
|
|
906
956
|
let channelId: string;
|
|
907
957
|
if (args.channelId) channelId = args.channelId;
|
|
908
|
-
else channelId = await resolveChannel(token, ref);
|
|
958
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
909
959
|
|
|
910
960
|
if (args.remove) {
|
|
911
|
-
await reactionRemove(token, channelId, ts, emoji);
|
|
961
|
+
await reactionRemove(token, channelId, ts, emoji, args.cookie);
|
|
912
962
|
console.log(`✓ Removed :${emoji}: (ts: ${ts})`);
|
|
913
963
|
} else {
|
|
914
|
-
await reactionAdd(token, channelId, ts, emoji);
|
|
964
|
+
await reactionAdd(token, channelId, ts, emoji, args.cookie);
|
|
915
965
|
console.log(`✓ Reacted :${emoji}: (ts: ${ts})`);
|
|
916
966
|
}
|
|
917
967
|
}
|
|
@@ -933,18 +983,22 @@ interface SendArgs {
|
|
|
933
983
|
// Session cookie (xoxd) for the mention token — required for an xoxc- user
|
|
934
984
|
// token to call users.list on the public API (it is rejected without it).
|
|
935
985
|
mentionCookie?: string;
|
|
986
|
+
// Session cookie (xoxd) for `token` itself — required for an xoxc- desktop
|
|
987
|
+
// session token to be accepted by the public Slack API at all (chat.postMessage
|
|
988
|
+
// included). Not set when sending --as-bot (the bot token needs no cookie).
|
|
989
|
+
cookie?: string;
|
|
936
990
|
}
|
|
937
991
|
|
|
938
992
|
// Detect the silent-failure footgun: DMing yourself with your own user token.
|
|
939
993
|
// Slack does not notify you about messages you sent to yourself, so an
|
|
940
994
|
// escalation DM via `send @me` (or @your-own-handle) is delivered but never
|
|
941
995
|
// surfaces. Returns true when ref names the token's own user.
|
|
942
|
-
async function isSelfDm(token: string, ref: string): Promise<boolean> {
|
|
996
|
+
async function isSelfDm(token: string, ref: string, cookie?: string): Promise<boolean> {
|
|
943
997
|
if (!ref.startsWith("@")) return false;
|
|
944
998
|
const name = ref.slice(1).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
945
999
|
if (name === "me" || name === "you") return true;
|
|
946
1000
|
try {
|
|
947
|
-
const self = await authTest(token);
|
|
1001
|
+
const self = await authTest(token, cookie);
|
|
948
1002
|
const selfName = (self.user ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
949
1003
|
return selfName !== "" && selfName === name;
|
|
950
1004
|
} catch {
|
|
@@ -954,10 +1008,11 @@ async function isSelfDm(token: string, ref: string): Promise<boolean> {
|
|
|
954
1008
|
|
|
955
1009
|
async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
956
1010
|
const { ref, threadTs } = parseTargetThread(args.target);
|
|
1011
|
+
const cookie = args.cookie;
|
|
957
1012
|
|
|
958
1013
|
// Guard the self-DM footgun before doing anything else, unless sending as the
|
|
959
1014
|
// bot (which delivers a notifiable DM from a different identity).
|
|
960
|
-
if (!args.asBot && !args.channelId && !args.userId && await isSelfDm(token, ref)) {
|
|
1015
|
+
if (!args.asBot && !args.channelId && !args.userId && await isSelfDm(token, ref, cookie)) {
|
|
961
1016
|
console.error(
|
|
962
1017
|
`Warning: "${ref}" is a DM to yourself — Slack will NOT notify you of your own message.\n` +
|
|
963
1018
|
` To reach yourself with a notification, send as the bot: slack send '${ref}' '...' --as-bot`,
|
|
@@ -966,8 +1021,8 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
966
1021
|
|
|
967
1022
|
let channelId: string;
|
|
968
1023
|
if (args.channelId) channelId = args.channelId;
|
|
969
|
-
else if (args.userId) channelId = await openDm(token, args.userId);
|
|
970
|
-
else channelId = await resolveChannel(token, ref);
|
|
1024
|
+
else if (args.userId) channelId = await openDm(token, args.userId, cookie);
|
|
1025
|
+
else channelId = await resolveChannel(token, ref, cookie);
|
|
971
1026
|
|
|
972
1027
|
// Convert @handle / @名前 tokens to <@USERID> before hashing/sending so the
|
|
973
1028
|
// safety gate covers exactly what will be posted. Unresolved tokens stay as
|
|
@@ -1036,7 +1091,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1036
1091
|
// Fetch a generous window and preview its tail. conversations.replies is
|
|
1037
1092
|
// oldest-first from the parent, so a very long thread's true tail may lie
|
|
1038
1093
|
// beyond this window — 100 covers essentially all real threads.
|
|
1039
|
-
const resp = (await replies(token, channelId, threadTs, 100)) as Record<string, Json>;
|
|
1094
|
+
const resp = (await replies(token, channelId, threadTs, 100, cookie)) as Record<string, Json>;
|
|
1040
1095
|
threadMsgs = asArray(resp.messages).map(asRecord);
|
|
1041
1096
|
const lastMsg = threadMsgs[threadMsgs.length - 1];
|
|
1042
1097
|
// Bind the hash to the thread's most recent message: if someone replies
|
|
@@ -1050,7 +1105,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1050
1105
|
}
|
|
1051
1106
|
} else {
|
|
1052
1107
|
try {
|
|
1053
|
-
const ctx = (await history(token, channelId, 1)) as Record<string, Json>;
|
|
1108
|
+
const ctx = (await history(token, channelId, 1, undefined, undefined, cookie)) as Record<string, Json>;
|
|
1054
1109
|
// limit=1 already narrows this to exactly the channel's true last message
|
|
1055
1110
|
// (subtype or not) — take it raw for the ownership check below. The
|
|
1056
1111
|
// subtype filter is only for the human-readable preview text/author: it
|
|
@@ -1063,7 +1118,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1063
1118
|
// Resolve the author's user ID to a display name for the preview; fall back
|
|
1064
1119
|
// to the bot username, then the raw ID. userName() is fail-soft.
|
|
1065
1120
|
lastUser = typeof lastMsg?.user === "string"
|
|
1066
|
-
? await userName(token, lastMsg.user)
|
|
1121
|
+
? await userName(token, lastMsg.user, cookie)
|
|
1067
1122
|
: typeof lastMsg?.username === "string"
|
|
1068
1123
|
? lastMsg.username
|
|
1069
1124
|
: "?";
|
|
@@ -1086,7 +1141,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1086
1141
|
// shape `send --as-bot` produces. Opt out with SLACK_UNREPLIED_WARN=0.
|
|
1087
1142
|
if (process.env.SLACK_UNREPLIED_WARN !== "0" && lastMsgTs && (lastMsgUserId || lastMsgBotId)) {
|
|
1088
1143
|
try {
|
|
1089
|
-
const self = await authScopes(token);
|
|
1144
|
+
const self = await authScopes(token, cookie);
|
|
1090
1145
|
const isSelfAuthor =
|
|
1091
1146
|
(!!lastMsgUserId && !!self.userId && self.userId === lastMsgUserId) ||
|
|
1092
1147
|
(!!lastMsgBotId && !!self.botId && self.botId === lastMsgBotId);
|
|
@@ -1100,7 +1155,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1100
1155
|
// embedded thread_ts colliding with the appended message ts.
|
|
1101
1156
|
let editCmd = `slack edit "#${channelId}:${lastMsgTs}" "<新しい本文>" --channel-id ${channelId}`;
|
|
1102
1157
|
try {
|
|
1103
|
-
const permalink = await getPermalink(token, channelId, lastMsgTs);
|
|
1158
|
+
const permalink = await getPermalink(token, channelId, lastMsgTs, cookie);
|
|
1104
1159
|
if (permalink) editCmd = `slack edit "${permalink}" "<新しい本文>"`;
|
|
1105
1160
|
} catch {
|
|
1106
1161
|
// fall back to the --channel-id form above
|
|
@@ -1128,7 +1183,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1128
1183
|
}
|
|
1129
1184
|
if (best.msg && best.sim >= 0.82) {
|
|
1130
1185
|
const pct = Math.round(best.sim * 100);
|
|
1131
|
-
console.error(`⚠ possible duplicate: ${pct}% similar to an existing thread message — ${await threadMsgLine(token, best.msg)}`);
|
|
1186
|
+
console.error(`⚠ possible duplicate: ${pct}% similar to an existing thread message — ${await threadMsgLine(token, best.msg, 60, cookie)}`);
|
|
1132
1187
|
}
|
|
1133
1188
|
}
|
|
1134
1189
|
|
|
@@ -1137,14 +1192,14 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1137
1192
|
const code = safetyCode(channelId, threadTs ?? "", lastText, message);
|
|
1138
1193
|
|
|
1139
1194
|
if (args.code !== code) {
|
|
1140
|
-
const dest = await destLabel(token, channelId, ref);
|
|
1195
|
+
const dest = await destLabel(token, channelId, ref, cookie);
|
|
1141
1196
|
if (threadTs) {
|
|
1142
|
-
const parentLine = await threadParentLine(token, threadTs, threadMsgs);
|
|
1197
|
+
const parentLine = await threadParentLine(token, threadTs, threadMsgs, cookie);
|
|
1143
1198
|
// Preview the tail of the thread (up to 3 most recent messages) so the
|
|
1144
1199
|
// sender can see what's already been said and avoid repeating it.
|
|
1145
1200
|
const recent = threadMsgs.slice(-3);
|
|
1146
1201
|
const recentLines = recent.length
|
|
1147
|
-
? await Promise.all(recent.map(async (m) => ` ${await threadMsgLine(token, m)}`))
|
|
1202
|
+
? await Promise.all(recent.map(async (m) => ` ${await threadMsgLine(token, m, 60, cookie)}`))
|
|
1148
1203
|
: [" (thread context unavailable)"];
|
|
1149
1204
|
requireCode(args.code, code, [
|
|
1150
1205
|
`--- Recent messages in thread ----------------`,
|
|
@@ -1167,10 +1222,10 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1167
1222
|
]);
|
|
1168
1223
|
}
|
|
1169
1224
|
}
|
|
1170
|
-
const ts = await slackSend(token, channelId, message, threadTs, args.broadcast);
|
|
1225
|
+
const ts = await slackSend(token, channelId, message, threadTs, args.broadcast, cookie);
|
|
1171
1226
|
let permalink = "";
|
|
1172
1227
|
try {
|
|
1173
|
-
permalink = await getPermalink(token, channelId, ts);
|
|
1228
|
+
permalink = await getPermalink(token, channelId, ts, cookie);
|
|
1174
1229
|
} catch {
|
|
1175
1230
|
// fail-soft: getPermalink failure (rate limit, network, etc.) should not
|
|
1176
1231
|
// mask send success — fall back to ts-only output below.
|
|
@@ -1224,14 +1279,15 @@ interface ScheduleSendArgs {
|
|
|
1224
1279
|
code?: string;
|
|
1225
1280
|
channelId?: string;
|
|
1226
1281
|
userId?: string;
|
|
1282
|
+
cookie?: string;
|
|
1227
1283
|
}
|
|
1228
1284
|
async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<void> {
|
|
1229
1285
|
const { ref, threadTs } = parseTargetThread(args.target);
|
|
1230
1286
|
|
|
1231
1287
|
let channelId: string;
|
|
1232
1288
|
if (args.channelId) channelId = args.channelId;
|
|
1233
|
-
else if (args.userId) channelId = await openDm(token, args.userId);
|
|
1234
|
-
else channelId = await resolveChannel(token, ref);
|
|
1289
|
+
else if (args.userId) channelId = await openDm(token, args.userId, args.cookie);
|
|
1290
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
1235
1291
|
|
|
1236
1292
|
const postAt = parsePostAt(args.at);
|
|
1237
1293
|
const postAtDate = new Date(postAt * 1000).toISOString();
|
|
@@ -1246,18 +1302,18 @@ async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<v
|
|
|
1246
1302
|
`---------------------------------------------`,
|
|
1247
1303
|
]);
|
|
1248
1304
|
}
|
|
1249
|
-
const id = await scheduleMessage(token, channelId, args.message, postAt, threadTs);
|
|
1305
|
+
const id = await scheduleMessage(token, channelId, args.message, postAt, threadTs, args.cookie);
|
|
1250
1306
|
console.log(`✓ Scheduled (id: ${id}, at: ${postAtDate})`);
|
|
1251
1307
|
}
|
|
1252
1308
|
|
|
1253
|
-
async function cmdScheduleList(token: string, target?: string, channelId?: string): Promise<void> {
|
|
1309
|
+
async function cmdScheduleList(token: string, target?: string, channelId?: string, cookie?: string): Promise<void> {
|
|
1254
1310
|
let channel: string | undefined;
|
|
1255
1311
|
if (channelId) {
|
|
1256
1312
|
channel = channelId;
|
|
1257
1313
|
} else if (target) {
|
|
1258
|
-
channel = await resolveChannel(token, target);
|
|
1314
|
+
channel = await resolveChannel(token, target, cookie);
|
|
1259
1315
|
}
|
|
1260
|
-
const resp = (await listScheduledMessages(token, channel)) as {
|
|
1316
|
+
const resp = (await listScheduledMessages(token, channel, cookie)) as {
|
|
1261
1317
|
scheduled_messages?: { id: string; channel_id: string; post_at: number; text: string }[];
|
|
1262
1318
|
};
|
|
1263
1319
|
const msgs = resp.scheduled_messages ?? [];
|
|
@@ -1273,11 +1329,12 @@ interface ScheduleRmArgs {
|
|
|
1273
1329
|
id: string;
|
|
1274
1330
|
code?: string;
|
|
1275
1331
|
channelId?: string;
|
|
1332
|
+
cookie?: string;
|
|
1276
1333
|
}
|
|
1277
1334
|
async function cmdScheduleRm(token: string, args: ScheduleRmArgs): Promise<void> {
|
|
1278
1335
|
let channelId: string;
|
|
1279
1336
|
if (args.channelId) channelId = args.channelId;
|
|
1280
|
-
else channelId = await resolveChannel(token, args.target);
|
|
1337
|
+
else channelId = await resolveChannel(token, args.target, args.cookie);
|
|
1281
1338
|
|
|
1282
1339
|
const code = safetyCode(channelId, args.id);
|
|
1283
1340
|
if (args.code !== code) {
|
|
@@ -1288,7 +1345,7 @@ async function cmdScheduleRm(token: string, args: ScheduleRmArgs): Promise<void>
|
|
|
1288
1345
|
`---------------------------------------------`,
|
|
1289
1346
|
]);
|
|
1290
1347
|
}
|
|
1291
|
-
await deleteScheduledMessage(token, channelId, args.id);
|
|
1348
|
+
await deleteScheduledMessage(token, channelId, args.id, args.cookie);
|
|
1292
1349
|
console.log(`✓ Deleted scheduled message ${args.id}`);
|
|
1293
1350
|
}
|
|
1294
1351
|
|
|
@@ -1301,6 +1358,7 @@ interface UploadArgs {
|
|
|
1301
1358
|
code?: string;
|
|
1302
1359
|
channelId?: string;
|
|
1303
1360
|
userId?: string;
|
|
1361
|
+
cookie?: string;
|
|
1304
1362
|
}
|
|
1305
1363
|
async function cmdUpload(token: string, args: UploadArgs): Promise<void> {
|
|
1306
1364
|
const { statSync, existsSync } = await import("node:fs");
|
|
@@ -1317,8 +1375,8 @@ async function cmdUpload(token: string, args: UploadArgs): Promise<void> {
|
|
|
1317
1375
|
|
|
1318
1376
|
let channelId: string;
|
|
1319
1377
|
if (args.channelId) channelId = args.channelId;
|
|
1320
|
-
else if (args.userId) channelId = await openDm(token, args.userId);
|
|
1321
|
-
else channelId = await resolveChannel(token, ref);
|
|
1378
|
+
else if (args.userId) channelId = await openDm(token, args.userId, args.cookie);
|
|
1379
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
1322
1380
|
|
|
1323
1381
|
const isBatch = args.filePaths.length > 1;
|
|
1324
1382
|
const files = args.filePaths.map((fp) => {
|
|
@@ -1355,12 +1413,60 @@ async function cmdUpload(token: string, args: UploadArgs): Promise<void> {
|
|
|
1355
1413
|
const uploadOpts: { title?: string; threadTs?: string; initialComment?: string } = { title: f.title };
|
|
1356
1414
|
if (threadTs !== undefined) uploadOpts.threadTs = threadTs;
|
|
1357
1415
|
if (args.comment !== undefined && i === 0) uploadOpts.initialComment = args.comment;
|
|
1358
|
-
const { fileId, permalink } = await uploadFile(token, channelId, f.fp, uploadOpts);
|
|
1416
|
+
const { fileId, permalink } = await uploadFile(token, channelId, f.fp, uploadOpts, args.cookie);
|
|
1359
1417
|
const prefix = total > 1 ? `[${i + 1}/${total}] ` : "";
|
|
1360
1418
|
console.log(`${prefix}✓ Uploaded (file_id: ${fileId}${permalink ? `, url: ${permalink}` : ""})`);
|
|
1361
1419
|
}
|
|
1362
1420
|
}
|
|
1363
1421
|
|
|
1422
|
+
// --- files ls — list files visible to the token (files.list, read-only) ---
|
|
1423
|
+
// Walks files.list pages until it has `limit` rows or runs out. Canvases and
|
|
1424
|
+
// Slack Lists from the web "unified files" view mostly don't appear here (the
|
|
1425
|
+
// token can't reach those internal APIs) — this lists real file uploads.
|
|
1426
|
+
async function cmdFilesList(
|
|
1427
|
+
token: string,
|
|
1428
|
+
cookie: string | undefined,
|
|
1429
|
+
opts: { limit: number; types?: string; channel?: string; user?: string; format: string },
|
|
1430
|
+
): Promise<void> {
|
|
1431
|
+
const channelId = opts.channel ? await resolveChannel(token, opts.channel, cookie) : undefined;
|
|
1432
|
+
const files: Record<string, Json>[] = [];
|
|
1433
|
+
let page = 1;
|
|
1434
|
+
let pages = 1;
|
|
1435
|
+
do {
|
|
1436
|
+
const listOpts: { page: number; count: number; types?: string; channel?: string; user?: string } = { page, count: 200 };
|
|
1437
|
+
if (opts.types) listOpts.types = opts.types;
|
|
1438
|
+
if (channelId) listOpts.channel = channelId;
|
|
1439
|
+
if (opts.user) listOpts.user = opts.user;
|
|
1440
|
+
const resp = asRecord((await filesList(token, listOpts, cookie)) as Json);
|
|
1441
|
+
for (const f of asArray(resp.files)) files.push(asRecord(f));
|
|
1442
|
+
pages = Number(asRecord(resp.paging).pages ?? 1);
|
|
1443
|
+
page++;
|
|
1444
|
+
} while (files.length < opts.limit && page <= pages);
|
|
1445
|
+
const shown = files.slice(0, opts.limit);
|
|
1446
|
+
|
|
1447
|
+
if (opts.format === "jsonl") {
|
|
1448
|
+
for (const f of shown) {
|
|
1449
|
+
console.log(JSON.stringify({
|
|
1450
|
+
id: f.id ?? null,
|
|
1451
|
+
name: f.name ?? f.title ?? null,
|
|
1452
|
+
filetype: f.filetype ?? null,
|
|
1453
|
+
size: f.size ?? null,
|
|
1454
|
+
user: f.user ?? null,
|
|
1455
|
+
created: f.created ?? null,
|
|
1456
|
+
permalink: f.permalink ?? null,
|
|
1457
|
+
}));
|
|
1458
|
+
}
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
for (const f of shown) {
|
|
1462
|
+
const id = String(f.id ?? "");
|
|
1463
|
+
const name = typeof f.name === "string" ? f.name : typeof f.title === "string" ? f.title : id;
|
|
1464
|
+
const ft = typeof f.filetype === "string" ? f.filetype : "?";
|
|
1465
|
+
const size = typeof f.size === "number" ? fmtSize(f.size) : "";
|
|
1466
|
+
console.log(`${id} ${ft.padEnd(7)} ${size.padStart(9)} ${name}`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1364
1470
|
// --- download <ref> [dest] — fetch an attachment's bytes to disk (read-only) ---
|
|
1365
1471
|
// ref: a file ID (F…) or a Slack file permalink (…/files/<UID>/<FID>/<name>).
|
|
1366
1472
|
function parseFileId(ref: string): string | undefined {
|
|
@@ -1491,10 +1597,11 @@ async function main(): Promise<void> {
|
|
|
1491
1597
|
(y2) => y2.positional("channel", { type: "string", demandOption: true, describe: "#name or channel ID" }),
|
|
1492
1598
|
async (argv) => {
|
|
1493
1599
|
const token = tok(argv as W);
|
|
1600
|
+
const cookie = ck(argv as W);
|
|
1494
1601
|
const ref = argv.channel!;
|
|
1495
1602
|
const channelRef = ref.startsWith("#") || ref.startsWith("@") || ref.startsWith("C") || ref.startsWith("G") || ref.startsWith("D") ? ref : `#${ref}`;
|
|
1496
|
-
const channelId = await resolveChannel(token, channelRef);
|
|
1497
|
-
const resp = asRecord((await conversationInfo(token, channelId)) as Json);
|
|
1603
|
+
const channelId = await resolveChannel(token, channelRef, cookie);
|
|
1604
|
+
const resp = asRecord((await conversationInfo(token, channelId, cookie)) as Json);
|
|
1498
1605
|
const ch = asRecord(resp.channel);
|
|
1499
1606
|
const name = typeof ch.name === "string" ? ch.name : channelId;
|
|
1500
1607
|
const isIm = ch.is_im === true;
|
|
@@ -1519,6 +1626,7 @@ async function main(): Promise<void> {
|
|
|
1519
1626
|
.option("code", { type: "string", describe: "Safety hash to confirm create" }),
|
|
1520
1627
|
async (argv) => {
|
|
1521
1628
|
const token = tok(argv as W);
|
|
1629
|
+
const cookie = ck(argv as W);
|
|
1522
1630
|
// Slack lowercases and strips the leading #; normalize for preview + code.
|
|
1523
1631
|
const name = argv.name!.replace(/^#/, "").toLowerCase();
|
|
1524
1632
|
const isPrivate = argv.private === true;
|
|
@@ -1533,7 +1641,7 @@ async function main(): Promise<void> {
|
|
|
1533
1641
|
]);
|
|
1534
1642
|
let created: { id: string; name: string };
|
|
1535
1643
|
try {
|
|
1536
|
-
created = await createChannel(token, name, isPrivate);
|
|
1644
|
+
created = await createChannel(token, name, isPrivate, cookie);
|
|
1537
1645
|
} catch (e: unknown) {
|
|
1538
1646
|
console.error(e instanceof Error ? e.message : String(e));
|
|
1539
1647
|
process.exit(1);
|
|
@@ -1543,14 +1651,32 @@ async function main(): Promise<void> {
|
|
|
1543
1651
|
const ids: string[] = [];
|
|
1544
1652
|
for (const ref of invites) {
|
|
1545
1653
|
try {
|
|
1546
|
-
ids.push(ref.startsWith("U") || ref.startsWith("W") ? ref : await resolveUserId(token, ref));
|
|
1654
|
+
ids.push(ref.startsWith("U") || ref.startsWith("W") ? ref : await resolveUserId(token, ref, cookie));
|
|
1547
1655
|
} catch (e: unknown) {
|
|
1548
1656
|
console.error(` ! could not resolve ${ref}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1549
1657
|
}
|
|
1550
1658
|
}
|
|
1659
|
+
// conversations.invite returns ok:true but silently no-ops for a
|
|
1660
|
+
// single-channel guest (is_ultra_restricted) — they can only ever
|
|
1661
|
+
// belong to the one channel they were created in. Warn up front
|
|
1662
|
+
// (fail-soft, never blocks) so a no-op invite isn't mistaken for
|
|
1663
|
+
// success.
|
|
1664
|
+
for (const uid of ids) {
|
|
1665
|
+
try {
|
|
1666
|
+
const info = asRecord((await userInfo(token, uid, cookie)) as Json);
|
|
1667
|
+
const u = asRecord(info.user);
|
|
1668
|
+
if (u.is_ultra_restricted === true) {
|
|
1669
|
+
console.error(`⚠ ${uid} is a single-channel guest — Slack restricts them to their one existing channel; this invite will likely report success but not actually add them.`);
|
|
1670
|
+
} else if (u.is_restricted === true) {
|
|
1671
|
+
console.error(`⚠ ${uid} is a multi-channel guest — double-check they were actually added (guest invites can silently no-op depending on workspace restrictions).`);
|
|
1672
|
+
}
|
|
1673
|
+
} catch {
|
|
1674
|
+
// best-effort; a lookup failure must not block the invite
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1551
1677
|
if (ids.length) {
|
|
1552
1678
|
try {
|
|
1553
|
-
await inviteToChannel(token, created.id, ids);
|
|
1679
|
+
await inviteToChannel(token, created.id, ids, cookie);
|
|
1554
1680
|
console.log(`✓ Invited ${ids.length} member${ids.length === 1 ? "" : "s"}`);
|
|
1555
1681
|
} catch (e: unknown) {
|
|
1556
1682
|
console.error(` ! invite failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -1681,7 +1807,7 @@ async function main(): Promise<void> {
|
|
|
1681
1807
|
.option("count", { alias: "n", type: "number", default: 100 })
|
|
1682
1808
|
.option("json", { type: "boolean", default: false, describe: "Output raw JSON" }),
|
|
1683
1809
|
async (argv) => {
|
|
1684
|
-
await cmdSearch(tok(argv as W), argv.query!, argv.count, argv.json);
|
|
1810
|
+
await cmdSearch(tok(argv as W), argv.query!, argv.count, argv.json, ck(argv as W));
|
|
1685
1811
|
},
|
|
1686
1812
|
)
|
|
1687
1813
|
.command(
|
|
@@ -1730,7 +1856,7 @@ async function main(): Promise<void> {
|
|
|
1730
1856
|
// (not the user's own self-DM).
|
|
1731
1857
|
if (!args.channelId && !args.userId && args.target.startsWith("@")) {
|
|
1732
1858
|
try {
|
|
1733
|
-
args.userId = await resolveUserId(tok(argv as W), args.target);
|
|
1859
|
+
args.userId = await resolveUserId(tok(argv as W), args.target, ck(argv as W));
|
|
1734
1860
|
} catch (e: unknown) {
|
|
1735
1861
|
console.error(
|
|
1736
1862
|
`Error: could not resolve ${args.target} to a user for the bot DM.\n` +
|
|
@@ -1744,6 +1870,11 @@ async function main(): Promise<void> {
|
|
|
1744
1870
|
args.asBot = true;
|
|
1745
1871
|
} else {
|
|
1746
1872
|
sendToken = tok(argv as W);
|
|
1873
|
+
// Session cookie for `sendToken` itself — needed for an xoxc- desktop
|
|
1874
|
+
// token to be accepted by the public API at all. Not applicable to the
|
|
1875
|
+
// bot token above (bot tokens need no cookie).
|
|
1876
|
+
const sc = ck(argv as W);
|
|
1877
|
+
if (sc) args.cookie = sc;
|
|
1747
1878
|
}
|
|
1748
1879
|
// Print a clean error instead of yargs' usage dump when a send fails at
|
|
1749
1880
|
// runtime (e.g. missing scope, channel not found). The confirm gate
|
|
@@ -1791,6 +1922,8 @@ async function main(): Promise<void> {
|
|
|
1791
1922
|
if (argv.code) args.code = argv.code;
|
|
1792
1923
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1793
1924
|
if (argv["user-id"]) args.userId = argv["user-id"];
|
|
1925
|
+
const cookie = ck(argv as W);
|
|
1926
|
+
if (cookie) args.cookie = cookie;
|
|
1794
1927
|
await cmdScheduleSend(tok(argv as W), args);
|
|
1795
1928
|
},
|
|
1796
1929
|
)
|
|
@@ -1801,7 +1934,7 @@ async function main(): Promise<void> {
|
|
|
1801
1934
|
.positional("target", { type: "string", describe: "#channel to filter by" })
|
|
1802
1935
|
.option("channel-id", { type: "string", describe: "Raw channel ID" }),
|
|
1803
1936
|
async (argv) => {
|
|
1804
|
-
await cmdScheduleList(tok(argv as W), argv.target as string | undefined, argv["channel-id"]);
|
|
1937
|
+
await cmdScheduleList(tok(argv as W), argv.target as string | undefined, argv["channel-id"], ck(argv as W));
|
|
1805
1938
|
},
|
|
1806
1939
|
)
|
|
1807
1940
|
.command(
|
|
@@ -1815,6 +1948,8 @@ async function main(): Promise<void> {
|
|
|
1815
1948
|
async (argv) => {
|
|
1816
1949
|
const args: ScheduleRmArgs = { target: argv.target!, id: argv.id! };
|
|
1817
1950
|
if (argv.code) args.code = argv.code;
|
|
1951
|
+
const cookie = ck(argv as W);
|
|
1952
|
+
if (cookie) args.cookie = cookie;
|
|
1818
1953
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1819
1954
|
await cmdScheduleRm(tok(argv as W), args);
|
|
1820
1955
|
},
|
|
@@ -1837,6 +1972,8 @@ async function main(): Promise<void> {
|
|
|
1837
1972
|
if (argv.code) args.code = argv.code;
|
|
1838
1973
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1839
1974
|
if (argv.mentions !== false) args.mentions = true;
|
|
1975
|
+
const cookie = ck(argv as W);
|
|
1976
|
+
if (cookie) args.cookie = cookie;
|
|
1840
1977
|
await cmdEdit(tok(argv as W), args);
|
|
1841
1978
|
},
|
|
1842
1979
|
)
|
|
@@ -1851,6 +1988,8 @@ async function main(): Promise<void> {
|
|
|
1851
1988
|
const args: DeleteArgs = { target: argv.target! };
|
|
1852
1989
|
if (argv.code) args.code = argv.code;
|
|
1853
1990
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1991
|
+
const cookie = ck(argv as W);
|
|
1992
|
+
if (cookie) args.cookie = cookie;
|
|
1854
1993
|
await cmdDelete(tok(argv as W), args);
|
|
1855
1994
|
},
|
|
1856
1995
|
)
|
|
@@ -1866,6 +2005,8 @@ async function main(): Promise<void> {
|
|
|
1866
2005
|
const args: ReactArgs = { target: argv.target!, emoji: argv.emoji! };
|
|
1867
2006
|
if (argv.remove) args.remove = true;
|
|
1868
2007
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
2008
|
+
const cookie = ck(argv as W);
|
|
2009
|
+
if (cookie) args.cookie = cookie;
|
|
1869
2010
|
// Print a clean one-line error instead of yargs' usage dump + stack when
|
|
1870
2011
|
// the reaction fails at runtime (missing_scope, invalid_name, etc.).
|
|
1871
2012
|
try {
|
|
@@ -1894,9 +2035,40 @@ async function main(): Promise<void> {
|
|
|
1894
2035
|
if (argv.code) args.code = argv.code;
|
|
1895
2036
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1896
2037
|
if (argv["user-id"]) args.userId = argv["user-id"];
|
|
2038
|
+
const cookie = ck(argv as W);
|
|
2039
|
+
if (cookie) args.cookie = cookie;
|
|
1897
2040
|
await cmdUpload(tok(argv as W), args);
|
|
1898
2041
|
},
|
|
1899
2042
|
)
|
|
2043
|
+
.command(
|
|
2044
|
+
"files",
|
|
2045
|
+
"File commands",
|
|
2046
|
+
(y) => y
|
|
2047
|
+
.command(
|
|
2048
|
+
["ls", "list"],
|
|
2049
|
+
"List files visible to you (files.list)",
|
|
2050
|
+
(y2) => y2
|
|
2051
|
+
.option("limit", { alias: "l", type: "number", default: 100, describe: "Max files to list" })
|
|
2052
|
+
.option("type", { alias: "t", type: "string", describe: "Slack type filter: images, pdfs, gdocs, snippets, zips, spaces, all" })
|
|
2053
|
+
.option("channel", { alias: "c", type: "string", describe: "Only files in this channel (#name, @user, ID, or permalink)" })
|
|
2054
|
+
.option("user", { alias: "u", type: "string", describe: "Only files from this raw user ID" })
|
|
2055
|
+
.option("format", { type: "string", choices: ["text", "jsonl"], default: "text" })
|
|
2056
|
+
.option("json", { type: "boolean", default: false, describe: "Alias for --format=jsonl" }),
|
|
2057
|
+
async (argv) => {
|
|
2058
|
+
const o: { limit: number; types?: string; channel?: string; user?: string; format: string } = {
|
|
2059
|
+
limit: argv.limit as number,
|
|
2060
|
+
format: argv.json ? "jsonl" : (argv.format as string),
|
|
2061
|
+
};
|
|
2062
|
+
if (argv.type) o.types = argv.type as string;
|
|
2063
|
+
if (argv.channel) o.channel = argv.channel as string;
|
|
2064
|
+
if (argv.user) o.user = argv.user as string;
|
|
2065
|
+
await cmdFilesList(tok(argv as W), ck(argv as W), o);
|
|
2066
|
+
},
|
|
2067
|
+
)
|
|
2068
|
+
.demandCommand(1, "")
|
|
2069
|
+
.showHelpOnFail(true),
|
|
2070
|
+
() => {},
|
|
2071
|
+
)
|
|
1900
2072
|
.command(
|
|
1901
2073
|
"download <ref> [dest]",
|
|
1902
2074
|
"Download a file attachment (by file ID or file permalink) to disk",
|