slack-term 1.22.0 → 1.23.1
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 +10 -0
- package/dist/cli.js +85 -81
- package/package.json +4 -2
- package/ts/cli.ts +214 -58
- package/ts/slack.ts +71 -44
package/ts/cli.ts
CHANGED
|
@@ -8,13 +8,14 @@ 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";
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
authTest,
|
|
18
|
+
authScopes,
|
|
18
19
|
authTestSession,
|
|
19
20
|
conversationInfoSession,
|
|
20
21
|
createChannel,
|
|
@@ -386,8 +387,52 @@ async function cmdChannels(token: string, limit: number, filter?: string, all?:
|
|
|
386
387
|
}
|
|
387
388
|
|
|
388
389
|
// --- search ---
|
|
389
|
-
|
|
390
|
-
|
|
390
|
+
|
|
391
|
+
/** search.messages (and most read/write Web API methods) reject a bot token
|
|
392
|
+
* (xoxb-) with not_allowed_token_type — it's a user-only endpoint. Find a
|
|
393
|
+
* sibling profile that holds a user token (xoxp-/xoxc-) for the *same*
|
|
394
|
+
* workspace (teamId) as the bot profile currently in use, so the caller can
|
|
395
|
+
* retry with it automatically instead of just erroring. Deliberately does
|
|
396
|
+
* NOT fall back to a user-token profile for a different workspace — that
|
|
397
|
+
* would silently run the search against the wrong (possibly private)
|
|
398
|
+
* workspace instead of failing loudly. Returns undefined (no fallback) when
|
|
399
|
+
* the current token isn't a known profile at all, since its workspace can't
|
|
400
|
+
* be verified. */
|
|
401
|
+
function findUserTokenProfile(currentToken: string): { name: string; profile: Profile } | undefined {
|
|
402
|
+
const isUserToken = (t: string) => t.startsWith("xoxp-") || t.startsWith("xoxc-");
|
|
403
|
+
const profiles = listProfiles();
|
|
404
|
+
const current = profiles.find((p) => p.profile.token === currentToken);
|
|
405
|
+
if (!current) return undefined;
|
|
406
|
+
return profiles.find((p) => isUserToken(p.profile.token) && p.profile.teamId === current.profile.teamId);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function cmdSearch(token: string, query: string, count: number, json: boolean, cookie?: string): Promise<void> {
|
|
410
|
+
let searchToken = token;
|
|
411
|
+
let searchCookie = cookie;
|
|
412
|
+
let resp: Json;
|
|
413
|
+
try {
|
|
414
|
+
resp = await searchAll(searchToken, query, count, searchCookie);
|
|
415
|
+
} catch (e: unknown) {
|
|
416
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
417
|
+
if (token.startsWith("xoxb-") && msg.includes("not_allowed_token_type")) {
|
|
418
|
+
const fallback = findUserTokenProfile(token);
|
|
419
|
+
if (fallback) {
|
|
420
|
+
console.error(`Note: search.messages needs a user token — the active profile is a bot token (xoxb-); falling back to profile "${fallback.name}".`);
|
|
421
|
+
searchToken = fallback.profile.token;
|
|
422
|
+
searchCookie = fallback.profile.cookie;
|
|
423
|
+
resp = await searchAll(searchToken, query, count, searchCookie);
|
|
424
|
+
} else {
|
|
425
|
+
console.error(
|
|
426
|
+
"Error: search requires a user token (xoxp-/xoxc-) — the active profile is a bot token (xoxb-), which Slack rejects for search.messages.\n" +
|
|
427
|
+
" Add a user-token profile: slack auth login (or slack auth token)\n" +
|
|
428
|
+
" Then select it: slack auth use <name> (or pass --workspace <name>)",
|
|
429
|
+
);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
throw e;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
391
436
|
if (json) {
|
|
392
437
|
console.log(JSON.stringify(resp, null, 2));
|
|
393
438
|
return;
|
|
@@ -402,7 +447,7 @@ async function cmdSearch(token: string, query: string, count: number, json: bool
|
|
|
402
447
|
if (isIm && rawName.startsWith("U")) {
|
|
403
448
|
const handleKey = "@" + rawName;
|
|
404
449
|
if (!cache.has(handleKey)) {
|
|
405
|
-
const [, h] = await userInfoPair(
|
|
450
|
+
const [, h] = await userInfoPair(searchToken, rawName, searchCookie);
|
|
406
451
|
cache.set(handleKey, h);
|
|
407
452
|
}
|
|
408
453
|
chLabel = `@${cache.get(handleKey) ?? rawName}`;
|
|
@@ -411,7 +456,7 @@ async function cmdSearch(token: string, query: string, count: number, json: bool
|
|
|
411
456
|
} else {
|
|
412
457
|
chLabel = `#${rawName}`;
|
|
413
458
|
}
|
|
414
|
-
console.log(await formatMsgLine(
|
|
459
|
+
console.log(await formatMsgLine(searchToken, m, cache, chLabel));
|
|
415
460
|
}
|
|
416
461
|
}
|
|
417
462
|
|
|
@@ -704,15 +749,15 @@ function parseTargetThread(s: string): { ref: string; threadTs?: string } {
|
|
|
704
749
|
/** Human-readable destination label for confirm gates, e.g. "#symval (C0B0L6SSAMD)".
|
|
705
750
|
* When ref is already a #channel/@user label, keep it; otherwise resolve the channel
|
|
706
751
|
* name via conversations.info (fail-soft: a raw ID is still unambiguous). */
|
|
707
|
-
async function destLabel(token: string, channelId: string, ref: string): Promise<string> {
|
|
752
|
+
async function destLabel(token: string, channelId: string, ref: string, cookie?: string): Promise<string> {
|
|
708
753
|
let name = ref;
|
|
709
754
|
if (!ref.startsWith("#") && !ref.startsWith("@")) {
|
|
710
755
|
try {
|
|
711
|
-
const info = asRecord((await conversationInfo(token, channelId)) as Json);
|
|
756
|
+
const info = asRecord((await conversationInfo(token, channelId, cookie)) as Json);
|
|
712
757
|
const ch = asRecord(info.channel);
|
|
713
758
|
if (ch.is_im === true) {
|
|
714
759
|
const uid = typeof ch.user === "string" ? ch.user : "";
|
|
715
|
-
name = uid ? `@${await userName(token, uid)}` : channelId;
|
|
760
|
+
name = uid ? `@${await userName(token, uid, cookie)}` : channelId;
|
|
716
761
|
} else {
|
|
717
762
|
name = typeof ch.name === "string" ? `#${ch.name}` : channelId;
|
|
718
763
|
}
|
|
@@ -725,9 +770,9 @@ async function destLabel(token: string, channelId: string, ref: string): Promise
|
|
|
725
770
|
|
|
726
771
|
/** One-line description of a single thread message for confirm gates:
|
|
727
772
|
* "2026-06-11 08:05 @handle: head…". `headLen` bounds the text preview. */
|
|
728
|
-
async function threadMsgLine(token: string, m: Record<string, Json>, headLen = 60): Promise<string> {
|
|
773
|
+
async function threadMsgLine(token: string, m: Record<string, Json>, headLen = 60, cookie?: string): Promise<string> {
|
|
729
774
|
const author = typeof m.user === "string"
|
|
730
|
-
? await userName(token, m.user)
|
|
775
|
+
? await userName(token, m.user, cookie)
|
|
731
776
|
: typeof m.username === "string"
|
|
732
777
|
? m.username
|
|
733
778
|
: "?";
|
|
@@ -740,10 +785,10 @@ async function threadMsgLine(token: string, m: Record<string, Json>, headLen = 6
|
|
|
740
785
|
|
|
741
786
|
/** One-line thread-parent description for confirm gates: "2026-06-11 08:05 @handle: head…".
|
|
742
787
|
* Fail-soft: returns the raw ts if the parent cannot be found. */
|
|
743
|
-
async function threadParentLine(token: string, threadTs: string, threadMsgs: Record<string, Json>[]): Promise<string> {
|
|
788
|
+
async function threadParentLine(token: string, threadTs: string, threadMsgs: Record<string, Json>[], cookie?: string): Promise<string> {
|
|
744
789
|
const parent = threadMsgs.find((m) => String(m.ts) === threadTs) ?? threadMsgs[0];
|
|
745
790
|
if (!parent) return threadTs;
|
|
746
|
-
return threadMsgLine(token, parent, 40);
|
|
791
|
+
return threadMsgLine(token, parent, 40, cookie);
|
|
747
792
|
}
|
|
748
793
|
|
|
749
794
|
/** Normalize a message for duplicate comparison: lowercase, collapse whitespace,
|
|
@@ -799,6 +844,9 @@ interface EditArgs {
|
|
|
799
844
|
code?: string;
|
|
800
845
|
channelId?: string;
|
|
801
846
|
mentions?: boolean;
|
|
847
|
+
// Session cookie (xoxd) for `token` — required for an xoxc- desktop session
|
|
848
|
+
// token to be accepted by the public Slack API at all.
|
|
849
|
+
cookie?: string;
|
|
802
850
|
}
|
|
803
851
|
async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
804
852
|
const { ref, ts } = splitRefTs(args.target);
|
|
@@ -809,10 +857,10 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
|
809
857
|
|
|
810
858
|
let channelId: string;
|
|
811
859
|
if (args.channelId) channelId = args.channelId;
|
|
812
|
-
else channelId = await resolveChannel(token, ref);
|
|
860
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
813
861
|
|
|
814
862
|
// Fetch the message to display the original text and compute the safety hash.
|
|
815
|
-
const resp = (await replies(token, channelId, ts, 1)) as Record<string, Json>;
|
|
863
|
+
const resp = (await replies(token, channelId, ts, 1, args.cookie)) as Record<string, Json>;
|
|
816
864
|
const msgs = asArray(resp.messages).map(asRecord);
|
|
817
865
|
const original = msgs.find((m) => String(m.ts) === ts);
|
|
818
866
|
if (!original) {
|
|
@@ -823,7 +871,7 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
|
823
871
|
|
|
824
872
|
// Convert @handle → <@USERID> before hashing/editing (unresolved stay as text).
|
|
825
873
|
const newText = args.mentions
|
|
826
|
-
? await encodeMentions(token, args.newText, channelId)
|
|
874
|
+
? await encodeMentions(token, args.newText, channelId, args.cookie ? { cookie: args.cookie } : {})
|
|
827
875
|
: args.newText;
|
|
828
876
|
|
|
829
877
|
const code = safetyCode(originalText, newText);
|
|
@@ -837,7 +885,7 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
|
837
885
|
]);
|
|
838
886
|
}
|
|
839
887
|
|
|
840
|
-
const newTs = await editMessage(token, channelId, ts, newText);
|
|
888
|
+
const newTs = await editMessage(token, channelId, ts, newText, args.cookie);
|
|
841
889
|
console.log(`✓ Edited (ts: ${newTs})`);
|
|
842
890
|
}
|
|
843
891
|
|
|
@@ -846,6 +894,7 @@ interface DeleteArgs {
|
|
|
846
894
|
target: string;
|
|
847
895
|
code?: string;
|
|
848
896
|
channelId?: string;
|
|
897
|
+
cookie?: string;
|
|
849
898
|
}
|
|
850
899
|
async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
851
900
|
const { ref, ts } = splitRefTs(args.target);
|
|
@@ -856,10 +905,10 @@ async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
|
856
905
|
|
|
857
906
|
let channelId: string;
|
|
858
907
|
if (args.channelId) channelId = args.channelId;
|
|
859
|
-
else channelId = await resolveChannel(token, ref);
|
|
908
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
860
909
|
|
|
861
910
|
// Fetch the message to display what is being deleted and compute the safety hash.
|
|
862
|
-
const resp = (await replies(token, channelId, ts, 1)) as Record<string, Json>;
|
|
911
|
+
const resp = (await replies(token, channelId, ts, 1, args.cookie)) as Record<string, Json>;
|
|
863
912
|
const msgs = asArray(resp.messages).map(asRecord);
|
|
864
913
|
const original = msgs.find((m) => String(m.ts) === ts);
|
|
865
914
|
if (!original) {
|
|
@@ -870,7 +919,7 @@ async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
|
870
919
|
|
|
871
920
|
const code = safetyCode(channelId, ts, originalText);
|
|
872
921
|
if (args.code !== code) {
|
|
873
|
-
const dest = await destLabel(token, channelId, ref);
|
|
922
|
+
const dest = await destLabel(token, channelId, ref, args.cookie);
|
|
874
923
|
requireCode(args.code, code, [
|
|
875
924
|
`--- Deleting message -------------------------`,
|
|
876
925
|
` → ${dest} at ${slackTsToIso(ts)}`,
|
|
@@ -879,7 +928,7 @@ async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
|
879
928
|
]);
|
|
880
929
|
}
|
|
881
930
|
|
|
882
|
-
await deleteMessage(token, channelId, ts);
|
|
931
|
+
await deleteMessage(token, channelId, ts, args.cookie);
|
|
883
932
|
console.log(`✓ Deleted (ts: ${ts})`);
|
|
884
933
|
}
|
|
885
934
|
|
|
@@ -889,6 +938,7 @@ interface ReactArgs {
|
|
|
889
938
|
emoji: string;
|
|
890
939
|
remove?: boolean;
|
|
891
940
|
channelId?: string;
|
|
941
|
+
cookie?: string;
|
|
892
942
|
}
|
|
893
943
|
// Add (or --remove) an emoji reaction. No confirm gate: a reaction is trivial
|
|
894
944
|
// and fully reversible (`--remove`), and the whole point is a lightweight ack
|
|
@@ -904,13 +954,13 @@ async function cmdReact(token: string, args: ReactArgs): Promise<void> {
|
|
|
904
954
|
|
|
905
955
|
let channelId: string;
|
|
906
956
|
if (args.channelId) channelId = args.channelId;
|
|
907
|
-
else channelId = await resolveChannel(token, ref);
|
|
957
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
908
958
|
|
|
909
959
|
if (args.remove) {
|
|
910
|
-
await reactionRemove(token, channelId, ts, emoji);
|
|
960
|
+
await reactionRemove(token, channelId, ts, emoji, args.cookie);
|
|
911
961
|
console.log(`✓ Removed :${emoji}: (ts: ${ts})`);
|
|
912
962
|
} else {
|
|
913
|
-
await reactionAdd(token, channelId, ts, emoji);
|
|
963
|
+
await reactionAdd(token, channelId, ts, emoji, args.cookie);
|
|
914
964
|
console.log(`✓ Reacted :${emoji}: (ts: ${ts})`);
|
|
915
965
|
}
|
|
916
966
|
}
|
|
@@ -932,18 +982,22 @@ interface SendArgs {
|
|
|
932
982
|
// Session cookie (xoxd) for the mention token — required for an xoxc- user
|
|
933
983
|
// token to call users.list on the public API (it is rejected without it).
|
|
934
984
|
mentionCookie?: string;
|
|
985
|
+
// Session cookie (xoxd) for `token` itself — required for an xoxc- desktop
|
|
986
|
+
// session token to be accepted by the public Slack API at all (chat.postMessage
|
|
987
|
+
// included). Not set when sending --as-bot (the bot token needs no cookie).
|
|
988
|
+
cookie?: string;
|
|
935
989
|
}
|
|
936
990
|
|
|
937
991
|
// Detect the silent-failure footgun: DMing yourself with your own user token.
|
|
938
992
|
// Slack does not notify you about messages you sent to yourself, so an
|
|
939
993
|
// escalation DM via `send @me` (or @your-own-handle) is delivered but never
|
|
940
994
|
// surfaces. Returns true when ref names the token's own user.
|
|
941
|
-
async function isSelfDm(token: string, ref: string): Promise<boolean> {
|
|
995
|
+
async function isSelfDm(token: string, ref: string, cookie?: string): Promise<boolean> {
|
|
942
996
|
if (!ref.startsWith("@")) return false;
|
|
943
997
|
const name = ref.slice(1).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
944
998
|
if (name === "me" || name === "you") return true;
|
|
945
999
|
try {
|
|
946
|
-
const self = await authTest(token);
|
|
1000
|
+
const self = await authTest(token, cookie);
|
|
947
1001
|
const selfName = (self.user ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
948
1002
|
return selfName !== "" && selfName === name;
|
|
949
1003
|
} catch {
|
|
@@ -953,10 +1007,11 @@ async function isSelfDm(token: string, ref: string): Promise<boolean> {
|
|
|
953
1007
|
|
|
954
1008
|
async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
955
1009
|
const { ref, threadTs } = parseTargetThread(args.target);
|
|
1010
|
+
const cookie = args.cookie;
|
|
956
1011
|
|
|
957
1012
|
// Guard the self-DM footgun before doing anything else, unless sending as the
|
|
958
1013
|
// bot (which delivers a notifiable DM from a different identity).
|
|
959
|
-
if (!args.asBot && !args.channelId && !args.userId && await isSelfDm(token, ref)) {
|
|
1014
|
+
if (!args.asBot && !args.channelId && !args.userId && await isSelfDm(token, ref, cookie)) {
|
|
960
1015
|
console.error(
|
|
961
1016
|
`Warning: "${ref}" is a DM to yourself — Slack will NOT notify you of your own message.\n` +
|
|
962
1017
|
` To reach yourself with a notification, send as the bot: slack send '${ref}' '...' --as-bot`,
|
|
@@ -965,8 +1020,8 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
965
1020
|
|
|
966
1021
|
let channelId: string;
|
|
967
1022
|
if (args.channelId) channelId = args.channelId;
|
|
968
|
-
else if (args.userId) channelId = await openDm(token, args.userId);
|
|
969
|
-
else channelId = await resolveChannel(token, ref);
|
|
1023
|
+
else if (args.userId) channelId = await openDm(token, args.userId, cookie);
|
|
1024
|
+
else channelId = await resolveChannel(token, ref, cookie);
|
|
970
1025
|
|
|
971
1026
|
// Convert @handle / @名前 tokens to <@USERID> before hashing/sending so the
|
|
972
1027
|
// safety gate covers exactly what will be posted. Unresolved tokens stay as
|
|
@@ -1021,38 +1076,99 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1021
1076
|
let lastText = "";
|
|
1022
1077
|
let lastUser = "?";
|
|
1023
1078
|
let threadMsgs: Record<string, Json>[] = [];
|
|
1079
|
+
// Author of the destination's TRUE most recent message (thread-scoped for a
|
|
1080
|
+
// thread reply, channel-scoped for a top-level send) — feeds the unreplied-warn
|
|
1081
|
+
// check below. Tracks `user` (normal message) and `bot_id` (a `bot_message`
|
|
1082
|
+
// subtype post — e.g. `send --as-bot` — which carries bot_id/username but no
|
|
1083
|
+
// `user`) so ownership can be determined either way. Undefined when the
|
|
1084
|
+
// preview fetch failed (fail-soft).
|
|
1085
|
+
let lastMsgUserId: string | undefined;
|
|
1086
|
+
let lastMsgBotId: string | undefined;
|
|
1087
|
+
let lastMsgTs: string | undefined;
|
|
1024
1088
|
if (threadTs) {
|
|
1025
1089
|
try {
|
|
1026
1090
|
// Fetch a generous window and preview its tail. conversations.replies is
|
|
1027
1091
|
// oldest-first from the parent, so a very long thread's true tail may lie
|
|
1028
1092
|
// beyond this window — 100 covers essentially all real threads.
|
|
1029
|
-
const resp = (await replies(token, channelId, threadTs, 100)) as Record<string, Json>;
|
|
1093
|
+
const resp = (await replies(token, channelId, threadTs, 100, cookie)) as Record<string, Json>;
|
|
1030
1094
|
threadMsgs = asArray(resp.messages).map(asRecord);
|
|
1031
1095
|
const lastMsg = threadMsgs[threadMsgs.length - 1];
|
|
1032
1096
|
// Bind the hash to the thread's most recent message: if someone replies
|
|
1033
1097
|
// between preview and confirm, the code invalidates and re-previews.
|
|
1034
1098
|
lastText = typeof lastMsg?.text === "string" ? lastMsg.text : "";
|
|
1099
|
+
lastMsgUserId = typeof lastMsg?.user === "string" ? lastMsg.user : undefined;
|
|
1100
|
+
lastMsgBotId = typeof lastMsg?.bot_id === "string" ? lastMsg.bot_id : undefined;
|
|
1101
|
+
lastMsgTs = typeof lastMsg?.ts === "string" ? lastMsg.ts : undefined;
|
|
1035
1102
|
} catch {
|
|
1036
1103
|
// best-effort preview only
|
|
1037
1104
|
}
|
|
1038
1105
|
} else {
|
|
1039
1106
|
try {
|
|
1040
|
-
const ctx = (await history(token, channelId, 1)) as Record<string, Json>;
|
|
1041
|
-
|
|
1042
|
-
|
|
1107
|
+
const ctx = (await history(token, channelId, 1, undefined, undefined, cookie)) as Record<string, Json>;
|
|
1108
|
+
// limit=1 already narrows this to exactly the channel's true last message
|
|
1109
|
+
// (subtype or not) — take it raw for the ownership check below. The
|
|
1110
|
+
// subtype filter is only for the human-readable preview text/author: it
|
|
1111
|
+
// deliberately hides system events (channel_join etc.), but a bot_message
|
|
1112
|
+
// post is a real message and must NOT be dropped here, or a bot's own
|
|
1113
|
+
// last post would silently disable the unreplied-warn for --as-bot sends.
|
|
1114
|
+
const rawMsgs = asArray(ctx.messages).map(asRecord);
|
|
1115
|
+
const lastMsg = rawMsgs.filter((m) => m.subtype === undefined || m.subtype === null)[0];
|
|
1043
1116
|
lastText = typeof lastMsg?.text === "string" ? lastMsg.text : "";
|
|
1044
1117
|
// Resolve the author's user ID to a display name for the preview; fall back
|
|
1045
1118
|
// to the bot username, then the raw ID. userName() is fail-soft.
|
|
1046
1119
|
lastUser = typeof lastMsg?.user === "string"
|
|
1047
|
-
? await userName(token, lastMsg.user)
|
|
1120
|
+
? await userName(token, lastMsg.user, cookie)
|
|
1048
1121
|
: typeof lastMsg?.username === "string"
|
|
1049
1122
|
? lastMsg.username
|
|
1050
1123
|
: "?";
|
|
1124
|
+
const rawLast = rawMsgs[0];
|
|
1125
|
+
lastMsgUserId = typeof rawLast?.user === "string" ? rawLast.user : undefined;
|
|
1126
|
+
lastMsgBotId = typeof rawLast?.bot_id === "string" ? rawLast.bot_id : undefined;
|
|
1127
|
+
lastMsgTs = typeof rawLast?.ts === "string" ? rawLast.ts : undefined;
|
|
1051
1128
|
} catch {
|
|
1052
1129
|
// best-effort preview only
|
|
1053
1130
|
}
|
|
1054
1131
|
}
|
|
1055
1132
|
|
|
1133
|
+
// Unreplied guard: the destination's last message is our own and no one has
|
|
1134
|
+
// replied since (by definition, since it's still the most recent message) —
|
|
1135
|
+
// sending another new message risks spamming the same person twice. Warn
|
|
1136
|
+
// (never block) and point at `slack edit` on that message instead. Reply-status
|
|
1137
|
+
// based, not time-based, so it still fires even long after the last send.
|
|
1138
|
+
// Self-identity covers both a normal post (`user` = our user id) and a
|
|
1139
|
+
// `bot_message`-subtype post (`bot_id` = our app's bot id, no `user`), the
|
|
1140
|
+
// shape `send --as-bot` produces. Opt out with SLACK_UNREPLIED_WARN=0.
|
|
1141
|
+
if (process.env.SLACK_UNREPLIED_WARN !== "0" && lastMsgTs && (lastMsgUserId || lastMsgBotId)) {
|
|
1142
|
+
try {
|
|
1143
|
+
const self = await authScopes(token, cookie);
|
|
1144
|
+
const isSelfAuthor =
|
|
1145
|
+
(!!lastMsgUserId && !!self.userId && self.userId === lastMsgUserId) ||
|
|
1146
|
+
(!!lastMsgBotId && !!self.botId && self.botId === lastMsgBotId);
|
|
1147
|
+
if (isSelfAuthor) {
|
|
1148
|
+
// Prefer a real permalink (exact, copy-pasteable). If that lookup fails,
|
|
1149
|
+
// fall back to a form guaranteed parseable by `slack edit` regardless of
|
|
1150
|
+
// what shape `ref` is in (a bare channel/user name, a raw ID resolved
|
|
1151
|
+
// from a permalink, or a thread ref) — `#<channelId>:<ts>` always yields
|
|
1152
|
+
// a ts split, and `--channel-id` makes the leading "#name" irrelevant to
|
|
1153
|
+
// resolution, so it never depends on ref's original prefix or an
|
|
1154
|
+
// embedded thread_ts colliding with the appended message ts.
|
|
1155
|
+
let editCmd = `slack edit "#${channelId}:${lastMsgTs}" "<新しい本文>" --channel-id ${channelId}`;
|
|
1156
|
+
try {
|
|
1157
|
+
const permalink = await getPermalink(token, channelId, lastMsgTs, cookie);
|
|
1158
|
+
if (permalink) editCmd = `slack edit "${permalink}" "<新しい本文>"`;
|
|
1159
|
+
} catch {
|
|
1160
|
+
// fall back to the --channel-id form above
|
|
1161
|
+
}
|
|
1162
|
+
console.error(`⚠ 相手はまだ返信していません(最後のメッセージはあなたのものです)。`);
|
|
1163
|
+
console.error(` 連投を避けるため、新規送信でなく直前のメッセージの edit を検討してください:`);
|
|
1164
|
+
console.error(` ${editCmd}`);
|
|
1165
|
+
console.error(` このまま送信する場合は --code=XXXX で確定。`);
|
|
1166
|
+
}
|
|
1167
|
+
} catch {
|
|
1168
|
+
// best-effort; a failed self-identity lookup must not block the send
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1056
1172
|
// Duplicate guard: if the outgoing message closely matches something already
|
|
1057
1173
|
// in the thread, warn (never block). Catches re-posting a reply you already
|
|
1058
1174
|
// sent. Threshold is deliberately high so only near-identical text trips it.
|
|
@@ -1066,7 +1182,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1066
1182
|
}
|
|
1067
1183
|
if (best.msg && best.sim >= 0.82) {
|
|
1068
1184
|
const pct = Math.round(best.sim * 100);
|
|
1069
|
-
console.error(`⚠ possible duplicate: ${pct}% similar to an existing thread message — ${await threadMsgLine(token, best.msg)}`);
|
|
1185
|
+
console.error(`⚠ possible duplicate: ${pct}% similar to an existing thread message — ${await threadMsgLine(token, best.msg, 60, cookie)}`);
|
|
1070
1186
|
}
|
|
1071
1187
|
}
|
|
1072
1188
|
|
|
@@ -1075,14 +1191,14 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1075
1191
|
const code = safetyCode(channelId, threadTs ?? "", lastText, message);
|
|
1076
1192
|
|
|
1077
1193
|
if (args.code !== code) {
|
|
1078
|
-
const dest = await destLabel(token, channelId, ref);
|
|
1194
|
+
const dest = await destLabel(token, channelId, ref, cookie);
|
|
1079
1195
|
if (threadTs) {
|
|
1080
|
-
const parentLine = await threadParentLine(token, threadTs, threadMsgs);
|
|
1196
|
+
const parentLine = await threadParentLine(token, threadTs, threadMsgs, cookie);
|
|
1081
1197
|
// Preview the tail of the thread (up to 3 most recent messages) so the
|
|
1082
1198
|
// sender can see what's already been said and avoid repeating it.
|
|
1083
1199
|
const recent = threadMsgs.slice(-3);
|
|
1084
1200
|
const recentLines = recent.length
|
|
1085
|
-
? await Promise.all(recent.map(async (m) => ` ${await threadMsgLine(token, m)}`))
|
|
1201
|
+
? await Promise.all(recent.map(async (m) => ` ${await threadMsgLine(token, m, 60, cookie)}`))
|
|
1086
1202
|
: [" (thread context unavailable)"];
|
|
1087
1203
|
requireCode(args.code, code, [
|
|
1088
1204
|
`--- Recent messages in thread ----------------`,
|
|
@@ -1105,10 +1221,10 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1105
1221
|
]);
|
|
1106
1222
|
}
|
|
1107
1223
|
}
|
|
1108
|
-
const ts = await slackSend(token, channelId, message, threadTs, args.broadcast);
|
|
1224
|
+
const ts = await slackSend(token, channelId, message, threadTs, args.broadcast, cookie);
|
|
1109
1225
|
let permalink = "";
|
|
1110
1226
|
try {
|
|
1111
|
-
permalink = await getPermalink(token, channelId, ts);
|
|
1227
|
+
permalink = await getPermalink(token, channelId, ts, cookie);
|
|
1112
1228
|
} catch {
|
|
1113
1229
|
// fail-soft: getPermalink failure (rate limit, network, etc.) should not
|
|
1114
1230
|
// mask send success — fall back to ts-only output below.
|
|
@@ -1162,14 +1278,15 @@ interface ScheduleSendArgs {
|
|
|
1162
1278
|
code?: string;
|
|
1163
1279
|
channelId?: string;
|
|
1164
1280
|
userId?: string;
|
|
1281
|
+
cookie?: string;
|
|
1165
1282
|
}
|
|
1166
1283
|
async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<void> {
|
|
1167
1284
|
const { ref, threadTs } = parseTargetThread(args.target);
|
|
1168
1285
|
|
|
1169
1286
|
let channelId: string;
|
|
1170
1287
|
if (args.channelId) channelId = args.channelId;
|
|
1171
|
-
else if (args.userId) channelId = await openDm(token, args.userId);
|
|
1172
|
-
else channelId = await resolveChannel(token, ref);
|
|
1288
|
+
else if (args.userId) channelId = await openDm(token, args.userId, args.cookie);
|
|
1289
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
1173
1290
|
|
|
1174
1291
|
const postAt = parsePostAt(args.at);
|
|
1175
1292
|
const postAtDate = new Date(postAt * 1000).toISOString();
|
|
@@ -1184,18 +1301,18 @@ async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<v
|
|
|
1184
1301
|
`---------------------------------------------`,
|
|
1185
1302
|
]);
|
|
1186
1303
|
}
|
|
1187
|
-
const id = await scheduleMessage(token, channelId, args.message, postAt, threadTs);
|
|
1304
|
+
const id = await scheduleMessage(token, channelId, args.message, postAt, threadTs, args.cookie);
|
|
1188
1305
|
console.log(`✓ Scheduled (id: ${id}, at: ${postAtDate})`);
|
|
1189
1306
|
}
|
|
1190
1307
|
|
|
1191
|
-
async function cmdScheduleList(token: string, target?: string, channelId?: string): Promise<void> {
|
|
1308
|
+
async function cmdScheduleList(token: string, target?: string, channelId?: string, cookie?: string): Promise<void> {
|
|
1192
1309
|
let channel: string | undefined;
|
|
1193
1310
|
if (channelId) {
|
|
1194
1311
|
channel = channelId;
|
|
1195
1312
|
} else if (target) {
|
|
1196
|
-
channel = await resolveChannel(token, target);
|
|
1313
|
+
channel = await resolveChannel(token, target, cookie);
|
|
1197
1314
|
}
|
|
1198
|
-
const resp = (await listScheduledMessages(token, channel)) as {
|
|
1315
|
+
const resp = (await listScheduledMessages(token, channel, cookie)) as {
|
|
1199
1316
|
scheduled_messages?: { id: string; channel_id: string; post_at: number; text: string }[];
|
|
1200
1317
|
};
|
|
1201
1318
|
const msgs = resp.scheduled_messages ?? [];
|
|
@@ -1211,11 +1328,12 @@ interface ScheduleRmArgs {
|
|
|
1211
1328
|
id: string;
|
|
1212
1329
|
code?: string;
|
|
1213
1330
|
channelId?: string;
|
|
1331
|
+
cookie?: string;
|
|
1214
1332
|
}
|
|
1215
1333
|
async function cmdScheduleRm(token: string, args: ScheduleRmArgs): Promise<void> {
|
|
1216
1334
|
let channelId: string;
|
|
1217
1335
|
if (args.channelId) channelId = args.channelId;
|
|
1218
|
-
else channelId = await resolveChannel(token, args.target);
|
|
1336
|
+
else channelId = await resolveChannel(token, args.target, args.cookie);
|
|
1219
1337
|
|
|
1220
1338
|
const code = safetyCode(channelId, args.id);
|
|
1221
1339
|
if (args.code !== code) {
|
|
@@ -1226,7 +1344,7 @@ async function cmdScheduleRm(token: string, args: ScheduleRmArgs): Promise<void>
|
|
|
1226
1344
|
`---------------------------------------------`,
|
|
1227
1345
|
]);
|
|
1228
1346
|
}
|
|
1229
|
-
await deleteScheduledMessage(token, channelId, args.id);
|
|
1347
|
+
await deleteScheduledMessage(token, channelId, args.id, args.cookie);
|
|
1230
1348
|
console.log(`✓ Deleted scheduled message ${args.id}`);
|
|
1231
1349
|
}
|
|
1232
1350
|
|
|
@@ -1239,6 +1357,7 @@ interface UploadArgs {
|
|
|
1239
1357
|
code?: string;
|
|
1240
1358
|
channelId?: string;
|
|
1241
1359
|
userId?: string;
|
|
1360
|
+
cookie?: string;
|
|
1242
1361
|
}
|
|
1243
1362
|
async function cmdUpload(token: string, args: UploadArgs): Promise<void> {
|
|
1244
1363
|
const { statSync, existsSync } = await import("node:fs");
|
|
@@ -1255,8 +1374,8 @@ async function cmdUpload(token: string, args: UploadArgs): Promise<void> {
|
|
|
1255
1374
|
|
|
1256
1375
|
let channelId: string;
|
|
1257
1376
|
if (args.channelId) channelId = args.channelId;
|
|
1258
|
-
else if (args.userId) channelId = await openDm(token, args.userId);
|
|
1259
|
-
else channelId = await resolveChannel(token, ref);
|
|
1377
|
+
else if (args.userId) channelId = await openDm(token, args.userId, args.cookie);
|
|
1378
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
1260
1379
|
|
|
1261
1380
|
const isBatch = args.filePaths.length > 1;
|
|
1262
1381
|
const files = args.filePaths.map((fp) => {
|
|
@@ -1293,7 +1412,7 @@ async function cmdUpload(token: string, args: UploadArgs): Promise<void> {
|
|
|
1293
1412
|
const uploadOpts: { title?: string; threadTs?: string; initialComment?: string } = { title: f.title };
|
|
1294
1413
|
if (threadTs !== undefined) uploadOpts.threadTs = threadTs;
|
|
1295
1414
|
if (args.comment !== undefined && i === 0) uploadOpts.initialComment = args.comment;
|
|
1296
|
-
const { fileId, permalink } = await uploadFile(token, channelId, f.fp, uploadOpts);
|
|
1415
|
+
const { fileId, permalink } = await uploadFile(token, channelId, f.fp, uploadOpts, args.cookie);
|
|
1297
1416
|
const prefix = total > 1 ? `[${i + 1}/${total}] ` : "";
|
|
1298
1417
|
console.log(`${prefix}✓ Uploaded (file_id: ${fileId}${permalink ? `, url: ${permalink}` : ""})`);
|
|
1299
1418
|
}
|
|
@@ -1429,10 +1548,11 @@ async function main(): Promise<void> {
|
|
|
1429
1548
|
(y2) => y2.positional("channel", { type: "string", demandOption: true, describe: "#name or channel ID" }),
|
|
1430
1549
|
async (argv) => {
|
|
1431
1550
|
const token = tok(argv as W);
|
|
1551
|
+
const cookie = ck(argv as W);
|
|
1432
1552
|
const ref = argv.channel!;
|
|
1433
1553
|
const channelRef = ref.startsWith("#") || ref.startsWith("@") || ref.startsWith("C") || ref.startsWith("G") || ref.startsWith("D") ? ref : `#${ref}`;
|
|
1434
|
-
const channelId = await resolveChannel(token, channelRef);
|
|
1435
|
-
const resp = asRecord((await conversationInfo(token, channelId)) as Json);
|
|
1554
|
+
const channelId = await resolveChannel(token, channelRef, cookie);
|
|
1555
|
+
const resp = asRecord((await conversationInfo(token, channelId, cookie)) as Json);
|
|
1436
1556
|
const ch = asRecord(resp.channel);
|
|
1437
1557
|
const name = typeof ch.name === "string" ? ch.name : channelId;
|
|
1438
1558
|
const isIm = ch.is_im === true;
|
|
@@ -1457,6 +1577,7 @@ async function main(): Promise<void> {
|
|
|
1457
1577
|
.option("code", { type: "string", describe: "Safety hash to confirm create" }),
|
|
1458
1578
|
async (argv) => {
|
|
1459
1579
|
const token = tok(argv as W);
|
|
1580
|
+
const cookie = ck(argv as W);
|
|
1460
1581
|
// Slack lowercases and strips the leading #; normalize for preview + code.
|
|
1461
1582
|
const name = argv.name!.replace(/^#/, "").toLowerCase();
|
|
1462
1583
|
const isPrivate = argv.private === true;
|
|
@@ -1471,7 +1592,7 @@ async function main(): Promise<void> {
|
|
|
1471
1592
|
]);
|
|
1472
1593
|
let created: { id: string; name: string };
|
|
1473
1594
|
try {
|
|
1474
|
-
created = await createChannel(token, name, isPrivate);
|
|
1595
|
+
created = await createChannel(token, name, isPrivate, cookie);
|
|
1475
1596
|
} catch (e: unknown) {
|
|
1476
1597
|
console.error(e instanceof Error ? e.message : String(e));
|
|
1477
1598
|
process.exit(1);
|
|
@@ -1481,14 +1602,32 @@ async function main(): Promise<void> {
|
|
|
1481
1602
|
const ids: string[] = [];
|
|
1482
1603
|
for (const ref of invites) {
|
|
1483
1604
|
try {
|
|
1484
|
-
ids.push(ref.startsWith("U") || ref.startsWith("W") ? ref : await resolveUserId(token, ref));
|
|
1605
|
+
ids.push(ref.startsWith("U") || ref.startsWith("W") ? ref : await resolveUserId(token, ref, cookie));
|
|
1485
1606
|
} catch (e: unknown) {
|
|
1486
1607
|
console.error(` ! could not resolve ${ref}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1487
1608
|
}
|
|
1488
1609
|
}
|
|
1610
|
+
// conversations.invite returns ok:true but silently no-ops for a
|
|
1611
|
+
// single-channel guest (is_ultra_restricted) — they can only ever
|
|
1612
|
+
// belong to the one channel they were created in. Warn up front
|
|
1613
|
+
// (fail-soft, never blocks) so a no-op invite isn't mistaken for
|
|
1614
|
+
// success.
|
|
1615
|
+
for (const uid of ids) {
|
|
1616
|
+
try {
|
|
1617
|
+
const info = asRecord((await userInfo(token, uid, cookie)) as Json);
|
|
1618
|
+
const u = asRecord(info.user);
|
|
1619
|
+
if (u.is_ultra_restricted === true) {
|
|
1620
|
+
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.`);
|
|
1621
|
+
} else if (u.is_restricted === true) {
|
|
1622
|
+
console.error(`⚠ ${uid} is a multi-channel guest — double-check they were actually added (guest invites can silently no-op depending on workspace restrictions).`);
|
|
1623
|
+
}
|
|
1624
|
+
} catch {
|
|
1625
|
+
// best-effort; a lookup failure must not block the invite
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1489
1628
|
if (ids.length) {
|
|
1490
1629
|
try {
|
|
1491
|
-
await inviteToChannel(token, created.id, ids);
|
|
1630
|
+
await inviteToChannel(token, created.id, ids, cookie);
|
|
1492
1631
|
console.log(`✓ Invited ${ids.length} member${ids.length === 1 ? "" : "s"}`);
|
|
1493
1632
|
} catch (e: unknown) {
|
|
1494
1633
|
console.error(` ! invite failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -1619,7 +1758,7 @@ async function main(): Promise<void> {
|
|
|
1619
1758
|
.option("count", { alias: "n", type: "number", default: 100 })
|
|
1620
1759
|
.option("json", { type: "boolean", default: false, describe: "Output raw JSON" }),
|
|
1621
1760
|
async (argv) => {
|
|
1622
|
-
await cmdSearch(tok(argv as W), argv.query!, argv.count, argv.json);
|
|
1761
|
+
await cmdSearch(tok(argv as W), argv.query!, argv.count, argv.json, ck(argv as W));
|
|
1623
1762
|
},
|
|
1624
1763
|
)
|
|
1625
1764
|
.command(
|
|
@@ -1668,7 +1807,7 @@ async function main(): Promise<void> {
|
|
|
1668
1807
|
// (not the user's own self-DM).
|
|
1669
1808
|
if (!args.channelId && !args.userId && args.target.startsWith("@")) {
|
|
1670
1809
|
try {
|
|
1671
|
-
args.userId = await resolveUserId(tok(argv as W), args.target);
|
|
1810
|
+
args.userId = await resolveUserId(tok(argv as W), args.target, ck(argv as W));
|
|
1672
1811
|
} catch (e: unknown) {
|
|
1673
1812
|
console.error(
|
|
1674
1813
|
`Error: could not resolve ${args.target} to a user for the bot DM.\n` +
|
|
@@ -1682,6 +1821,11 @@ async function main(): Promise<void> {
|
|
|
1682
1821
|
args.asBot = true;
|
|
1683
1822
|
} else {
|
|
1684
1823
|
sendToken = tok(argv as W);
|
|
1824
|
+
// Session cookie for `sendToken` itself — needed for an xoxc- desktop
|
|
1825
|
+
// token to be accepted by the public API at all. Not applicable to the
|
|
1826
|
+
// bot token above (bot tokens need no cookie).
|
|
1827
|
+
const sc = ck(argv as W);
|
|
1828
|
+
if (sc) args.cookie = sc;
|
|
1685
1829
|
}
|
|
1686
1830
|
// Print a clean error instead of yargs' usage dump when a send fails at
|
|
1687
1831
|
// runtime (e.g. missing scope, channel not found). The confirm gate
|
|
@@ -1729,6 +1873,8 @@ async function main(): Promise<void> {
|
|
|
1729
1873
|
if (argv.code) args.code = argv.code;
|
|
1730
1874
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1731
1875
|
if (argv["user-id"]) args.userId = argv["user-id"];
|
|
1876
|
+
const cookie = ck(argv as W);
|
|
1877
|
+
if (cookie) args.cookie = cookie;
|
|
1732
1878
|
await cmdScheduleSend(tok(argv as W), args);
|
|
1733
1879
|
},
|
|
1734
1880
|
)
|
|
@@ -1739,7 +1885,7 @@ async function main(): Promise<void> {
|
|
|
1739
1885
|
.positional("target", { type: "string", describe: "#channel to filter by" })
|
|
1740
1886
|
.option("channel-id", { type: "string", describe: "Raw channel ID" }),
|
|
1741
1887
|
async (argv) => {
|
|
1742
|
-
await cmdScheduleList(tok(argv as W), argv.target as string | undefined, argv["channel-id"]);
|
|
1888
|
+
await cmdScheduleList(tok(argv as W), argv.target as string | undefined, argv["channel-id"], ck(argv as W));
|
|
1743
1889
|
},
|
|
1744
1890
|
)
|
|
1745
1891
|
.command(
|
|
@@ -1753,6 +1899,8 @@ async function main(): Promise<void> {
|
|
|
1753
1899
|
async (argv) => {
|
|
1754
1900
|
const args: ScheduleRmArgs = { target: argv.target!, id: argv.id! };
|
|
1755
1901
|
if (argv.code) args.code = argv.code;
|
|
1902
|
+
const cookie = ck(argv as W);
|
|
1903
|
+
if (cookie) args.cookie = cookie;
|
|
1756
1904
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1757
1905
|
await cmdScheduleRm(tok(argv as W), args);
|
|
1758
1906
|
},
|
|
@@ -1775,6 +1923,8 @@ async function main(): Promise<void> {
|
|
|
1775
1923
|
if (argv.code) args.code = argv.code;
|
|
1776
1924
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1777
1925
|
if (argv.mentions !== false) args.mentions = true;
|
|
1926
|
+
const cookie = ck(argv as W);
|
|
1927
|
+
if (cookie) args.cookie = cookie;
|
|
1778
1928
|
await cmdEdit(tok(argv as W), args);
|
|
1779
1929
|
},
|
|
1780
1930
|
)
|
|
@@ -1789,6 +1939,8 @@ async function main(): Promise<void> {
|
|
|
1789
1939
|
const args: DeleteArgs = { target: argv.target! };
|
|
1790
1940
|
if (argv.code) args.code = argv.code;
|
|
1791
1941
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1942
|
+
const cookie = ck(argv as W);
|
|
1943
|
+
if (cookie) args.cookie = cookie;
|
|
1792
1944
|
await cmdDelete(tok(argv as W), args);
|
|
1793
1945
|
},
|
|
1794
1946
|
)
|
|
@@ -1804,6 +1956,8 @@ async function main(): Promise<void> {
|
|
|
1804
1956
|
const args: ReactArgs = { target: argv.target!, emoji: argv.emoji! };
|
|
1805
1957
|
if (argv.remove) args.remove = true;
|
|
1806
1958
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1959
|
+
const cookie = ck(argv as W);
|
|
1960
|
+
if (cookie) args.cookie = cookie;
|
|
1807
1961
|
// Print a clean one-line error instead of yargs' usage dump + stack when
|
|
1808
1962
|
// the reaction fails at runtime (missing_scope, invalid_name, etc.).
|
|
1809
1963
|
try {
|
|
@@ -1832,6 +1986,8 @@ async function main(): Promise<void> {
|
|
|
1832
1986
|
if (argv.code) args.code = argv.code;
|
|
1833
1987
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1834
1988
|
if (argv["user-id"]) args.userId = argv["user-id"];
|
|
1989
|
+
const cookie = ck(argv as W);
|
|
1990
|
+
if (cookie) args.cookie = cookie;
|
|
1835
1991
|
await cmdUpload(tok(argv as W), args);
|
|
1836
1992
|
},
|
|
1837
1993
|
)
|