slack-term 1.23.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 +5 -0
- package/dist/cli.js +77 -73
- package/package.json +4 -2
- package/ts/cli.ts +152 -58
- package/ts/slack.ts +71 -44
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slack-term",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/snomiao/slack-term.git"
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"@types/bun": "latest",
|
|
17
17
|
"@types/node": "^22",
|
|
18
18
|
"@types/yargs": "^17.0.35",
|
|
19
|
+
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|
|
19
20
|
"@vitest/coverage-v8": "^4.1.4",
|
|
20
21
|
"semantic-release": "^24",
|
|
21
22
|
"typescript": "^5.6",
|
|
@@ -57,7 +58,8 @@
|
|
|
57
58
|
"scripts": {
|
|
58
59
|
"dev": "bun run ts/cli.ts",
|
|
59
60
|
"build": "bun build ts/cli.ts --target=node --outfile dist/cli.js --minify",
|
|
60
|
-
"typecheck": "
|
|
61
|
+
"typecheck": "tsgo --noEmit",
|
|
62
|
+
"typecheck:tsc": "tsc --noEmit",
|
|
61
63
|
"test": "vitest run --coverage",
|
|
62
64
|
"test:watch": "vitest",
|
|
63
65
|
"test:parity": "bash tests/parity.sh",
|
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";
|
|
@@ -387,8 +387,52 @@ async function cmdChannels(token: string, limit: number, filter?: string, all?:
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
// --- search ---
|
|
390
|
-
|
|
391
|
-
|
|
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
|
+
}
|
|
392
436
|
if (json) {
|
|
393
437
|
console.log(JSON.stringify(resp, null, 2));
|
|
394
438
|
return;
|
|
@@ -403,7 +447,7 @@ async function cmdSearch(token: string, query: string, count: number, json: bool
|
|
|
403
447
|
if (isIm && rawName.startsWith("U")) {
|
|
404
448
|
const handleKey = "@" + rawName;
|
|
405
449
|
if (!cache.has(handleKey)) {
|
|
406
|
-
const [, h] = await userInfoPair(
|
|
450
|
+
const [, h] = await userInfoPair(searchToken, rawName, searchCookie);
|
|
407
451
|
cache.set(handleKey, h);
|
|
408
452
|
}
|
|
409
453
|
chLabel = `@${cache.get(handleKey) ?? rawName}`;
|
|
@@ -412,7 +456,7 @@ async function cmdSearch(token: string, query: string, count: number, json: bool
|
|
|
412
456
|
} else {
|
|
413
457
|
chLabel = `#${rawName}`;
|
|
414
458
|
}
|
|
415
|
-
console.log(await formatMsgLine(
|
|
459
|
+
console.log(await formatMsgLine(searchToken, m, cache, chLabel));
|
|
416
460
|
}
|
|
417
461
|
}
|
|
418
462
|
|
|
@@ -705,15 +749,15 @@ function parseTargetThread(s: string): { ref: string; threadTs?: string } {
|
|
|
705
749
|
/** Human-readable destination label for confirm gates, e.g. "#symval (C0B0L6SSAMD)".
|
|
706
750
|
* When ref is already a #channel/@user label, keep it; otherwise resolve the channel
|
|
707
751
|
* name via conversations.info (fail-soft: a raw ID is still unambiguous). */
|
|
708
|
-
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> {
|
|
709
753
|
let name = ref;
|
|
710
754
|
if (!ref.startsWith("#") && !ref.startsWith("@")) {
|
|
711
755
|
try {
|
|
712
|
-
const info = asRecord((await conversationInfo(token, channelId)) as Json);
|
|
756
|
+
const info = asRecord((await conversationInfo(token, channelId, cookie)) as Json);
|
|
713
757
|
const ch = asRecord(info.channel);
|
|
714
758
|
if (ch.is_im === true) {
|
|
715
759
|
const uid = typeof ch.user === "string" ? ch.user : "";
|
|
716
|
-
name = uid ? `@${await userName(token, uid)}` : channelId;
|
|
760
|
+
name = uid ? `@${await userName(token, uid, cookie)}` : channelId;
|
|
717
761
|
} else {
|
|
718
762
|
name = typeof ch.name === "string" ? `#${ch.name}` : channelId;
|
|
719
763
|
}
|
|
@@ -726,9 +770,9 @@ async function destLabel(token: string, channelId: string, ref: string): Promise
|
|
|
726
770
|
|
|
727
771
|
/** One-line description of a single thread message for confirm gates:
|
|
728
772
|
* "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> {
|
|
773
|
+
async function threadMsgLine(token: string, m: Record<string, Json>, headLen = 60, cookie?: string): Promise<string> {
|
|
730
774
|
const author = typeof m.user === "string"
|
|
731
|
-
? await userName(token, m.user)
|
|
775
|
+
? await userName(token, m.user, cookie)
|
|
732
776
|
: typeof m.username === "string"
|
|
733
777
|
? m.username
|
|
734
778
|
: "?";
|
|
@@ -741,10 +785,10 @@ async function threadMsgLine(token: string, m: Record<string, Json>, headLen = 6
|
|
|
741
785
|
|
|
742
786
|
/** One-line thread-parent description for confirm gates: "2026-06-11 08:05 @handle: head…".
|
|
743
787
|
* 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> {
|
|
788
|
+
async function threadParentLine(token: string, threadTs: string, threadMsgs: Record<string, Json>[], cookie?: string): Promise<string> {
|
|
745
789
|
const parent = threadMsgs.find((m) => String(m.ts) === threadTs) ?? threadMsgs[0];
|
|
746
790
|
if (!parent) return threadTs;
|
|
747
|
-
return threadMsgLine(token, parent, 40);
|
|
791
|
+
return threadMsgLine(token, parent, 40, cookie);
|
|
748
792
|
}
|
|
749
793
|
|
|
750
794
|
/** Normalize a message for duplicate comparison: lowercase, collapse whitespace,
|
|
@@ -800,6 +844,9 @@ interface EditArgs {
|
|
|
800
844
|
code?: string;
|
|
801
845
|
channelId?: string;
|
|
802
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;
|
|
803
850
|
}
|
|
804
851
|
async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
805
852
|
const { ref, ts } = splitRefTs(args.target);
|
|
@@ -810,10 +857,10 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
|
810
857
|
|
|
811
858
|
let channelId: string;
|
|
812
859
|
if (args.channelId) channelId = args.channelId;
|
|
813
|
-
else channelId = await resolveChannel(token, ref);
|
|
860
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
814
861
|
|
|
815
862
|
// 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>;
|
|
863
|
+
const resp = (await replies(token, channelId, ts, 1, args.cookie)) as Record<string, Json>;
|
|
817
864
|
const msgs = asArray(resp.messages).map(asRecord);
|
|
818
865
|
const original = msgs.find((m) => String(m.ts) === ts);
|
|
819
866
|
if (!original) {
|
|
@@ -824,7 +871,7 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
|
824
871
|
|
|
825
872
|
// Convert @handle → <@USERID> before hashing/editing (unresolved stay as text).
|
|
826
873
|
const newText = args.mentions
|
|
827
|
-
? await encodeMentions(token, args.newText, channelId)
|
|
874
|
+
? await encodeMentions(token, args.newText, channelId, args.cookie ? { cookie: args.cookie } : {})
|
|
828
875
|
: args.newText;
|
|
829
876
|
|
|
830
877
|
const code = safetyCode(originalText, newText);
|
|
@@ -838,7 +885,7 @@ async function cmdEdit(token: string, args: EditArgs): Promise<void> {
|
|
|
838
885
|
]);
|
|
839
886
|
}
|
|
840
887
|
|
|
841
|
-
const newTs = await editMessage(token, channelId, ts, newText);
|
|
888
|
+
const newTs = await editMessage(token, channelId, ts, newText, args.cookie);
|
|
842
889
|
console.log(`✓ Edited (ts: ${newTs})`);
|
|
843
890
|
}
|
|
844
891
|
|
|
@@ -847,6 +894,7 @@ interface DeleteArgs {
|
|
|
847
894
|
target: string;
|
|
848
895
|
code?: string;
|
|
849
896
|
channelId?: string;
|
|
897
|
+
cookie?: string;
|
|
850
898
|
}
|
|
851
899
|
async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
852
900
|
const { ref, ts } = splitRefTs(args.target);
|
|
@@ -857,10 +905,10 @@ async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
|
857
905
|
|
|
858
906
|
let channelId: string;
|
|
859
907
|
if (args.channelId) channelId = args.channelId;
|
|
860
|
-
else channelId = await resolveChannel(token, ref);
|
|
908
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
861
909
|
|
|
862
910
|
// 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>;
|
|
911
|
+
const resp = (await replies(token, channelId, ts, 1, args.cookie)) as Record<string, Json>;
|
|
864
912
|
const msgs = asArray(resp.messages).map(asRecord);
|
|
865
913
|
const original = msgs.find((m) => String(m.ts) === ts);
|
|
866
914
|
if (!original) {
|
|
@@ -871,7 +919,7 @@ async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
|
871
919
|
|
|
872
920
|
const code = safetyCode(channelId, ts, originalText);
|
|
873
921
|
if (args.code !== code) {
|
|
874
|
-
const dest = await destLabel(token, channelId, ref);
|
|
922
|
+
const dest = await destLabel(token, channelId, ref, args.cookie);
|
|
875
923
|
requireCode(args.code, code, [
|
|
876
924
|
`--- Deleting message -------------------------`,
|
|
877
925
|
` → ${dest} at ${slackTsToIso(ts)}`,
|
|
@@ -880,7 +928,7 @@ async function cmdDelete(token: string, args: DeleteArgs): Promise<void> {
|
|
|
880
928
|
]);
|
|
881
929
|
}
|
|
882
930
|
|
|
883
|
-
await deleteMessage(token, channelId, ts);
|
|
931
|
+
await deleteMessage(token, channelId, ts, args.cookie);
|
|
884
932
|
console.log(`✓ Deleted (ts: ${ts})`);
|
|
885
933
|
}
|
|
886
934
|
|
|
@@ -890,6 +938,7 @@ interface ReactArgs {
|
|
|
890
938
|
emoji: string;
|
|
891
939
|
remove?: boolean;
|
|
892
940
|
channelId?: string;
|
|
941
|
+
cookie?: string;
|
|
893
942
|
}
|
|
894
943
|
// Add (or --remove) an emoji reaction. No confirm gate: a reaction is trivial
|
|
895
944
|
// and fully reversible (`--remove`), and the whole point is a lightweight ack
|
|
@@ -905,13 +954,13 @@ async function cmdReact(token: string, args: ReactArgs): Promise<void> {
|
|
|
905
954
|
|
|
906
955
|
let channelId: string;
|
|
907
956
|
if (args.channelId) channelId = args.channelId;
|
|
908
|
-
else channelId = await resolveChannel(token, ref);
|
|
957
|
+
else channelId = await resolveChannel(token, ref, args.cookie);
|
|
909
958
|
|
|
910
959
|
if (args.remove) {
|
|
911
|
-
await reactionRemove(token, channelId, ts, emoji);
|
|
960
|
+
await reactionRemove(token, channelId, ts, emoji, args.cookie);
|
|
912
961
|
console.log(`✓ Removed :${emoji}: (ts: ${ts})`);
|
|
913
962
|
} else {
|
|
914
|
-
await reactionAdd(token, channelId, ts, emoji);
|
|
963
|
+
await reactionAdd(token, channelId, ts, emoji, args.cookie);
|
|
915
964
|
console.log(`✓ Reacted :${emoji}: (ts: ${ts})`);
|
|
916
965
|
}
|
|
917
966
|
}
|
|
@@ -933,18 +982,22 @@ interface SendArgs {
|
|
|
933
982
|
// Session cookie (xoxd) for the mention token — required for an xoxc- user
|
|
934
983
|
// token to call users.list on the public API (it is rejected without it).
|
|
935
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;
|
|
936
989
|
}
|
|
937
990
|
|
|
938
991
|
// Detect the silent-failure footgun: DMing yourself with your own user token.
|
|
939
992
|
// Slack does not notify you about messages you sent to yourself, so an
|
|
940
993
|
// escalation DM via `send @me` (or @your-own-handle) is delivered but never
|
|
941
994
|
// surfaces. Returns true when ref names the token's own user.
|
|
942
|
-
async function isSelfDm(token: string, ref: string): Promise<boolean> {
|
|
995
|
+
async function isSelfDm(token: string, ref: string, cookie?: string): Promise<boolean> {
|
|
943
996
|
if (!ref.startsWith("@")) return false;
|
|
944
997
|
const name = ref.slice(1).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
945
998
|
if (name === "me" || name === "you") return true;
|
|
946
999
|
try {
|
|
947
|
-
const self = await authTest(token);
|
|
1000
|
+
const self = await authTest(token, cookie);
|
|
948
1001
|
const selfName = (self.user ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
949
1002
|
return selfName !== "" && selfName === name;
|
|
950
1003
|
} catch {
|
|
@@ -954,10 +1007,11 @@ async function isSelfDm(token: string, ref: string): Promise<boolean> {
|
|
|
954
1007
|
|
|
955
1008
|
async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
956
1009
|
const { ref, threadTs } = parseTargetThread(args.target);
|
|
1010
|
+
const cookie = args.cookie;
|
|
957
1011
|
|
|
958
1012
|
// Guard the self-DM footgun before doing anything else, unless sending as the
|
|
959
1013
|
// bot (which delivers a notifiable DM from a different identity).
|
|
960
|
-
if (!args.asBot && !args.channelId && !args.userId && await isSelfDm(token, ref)) {
|
|
1014
|
+
if (!args.asBot && !args.channelId && !args.userId && await isSelfDm(token, ref, cookie)) {
|
|
961
1015
|
console.error(
|
|
962
1016
|
`Warning: "${ref}" is a DM to yourself — Slack will NOT notify you of your own message.\n` +
|
|
963
1017
|
` To reach yourself with a notification, send as the bot: slack send '${ref}' '...' --as-bot`,
|
|
@@ -966,8 +1020,8 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
966
1020
|
|
|
967
1021
|
let channelId: string;
|
|
968
1022
|
if (args.channelId) channelId = args.channelId;
|
|
969
|
-
else if (args.userId) channelId = await openDm(token, args.userId);
|
|
970
|
-
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);
|
|
971
1025
|
|
|
972
1026
|
// Convert @handle / @名前 tokens to <@USERID> before hashing/sending so the
|
|
973
1027
|
// safety gate covers exactly what will be posted. Unresolved tokens stay as
|
|
@@ -1036,7 +1090,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1036
1090
|
// Fetch a generous window and preview its tail. conversations.replies is
|
|
1037
1091
|
// oldest-first from the parent, so a very long thread's true tail may lie
|
|
1038
1092
|
// beyond this window — 100 covers essentially all real threads.
|
|
1039
|
-
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>;
|
|
1040
1094
|
threadMsgs = asArray(resp.messages).map(asRecord);
|
|
1041
1095
|
const lastMsg = threadMsgs[threadMsgs.length - 1];
|
|
1042
1096
|
// Bind the hash to the thread's most recent message: if someone replies
|
|
@@ -1050,7 +1104,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1050
1104
|
}
|
|
1051
1105
|
} else {
|
|
1052
1106
|
try {
|
|
1053
|
-
const ctx = (await history(token, channelId, 1)) as Record<string, Json>;
|
|
1107
|
+
const ctx = (await history(token, channelId, 1, undefined, undefined, cookie)) as Record<string, Json>;
|
|
1054
1108
|
// limit=1 already narrows this to exactly the channel's true last message
|
|
1055
1109
|
// (subtype or not) — take it raw for the ownership check below. The
|
|
1056
1110
|
// subtype filter is only for the human-readable preview text/author: it
|
|
@@ -1063,7 +1117,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1063
1117
|
// Resolve the author's user ID to a display name for the preview; fall back
|
|
1064
1118
|
// to the bot username, then the raw ID. userName() is fail-soft.
|
|
1065
1119
|
lastUser = typeof lastMsg?.user === "string"
|
|
1066
|
-
? await userName(token, lastMsg.user)
|
|
1120
|
+
? await userName(token, lastMsg.user, cookie)
|
|
1067
1121
|
: typeof lastMsg?.username === "string"
|
|
1068
1122
|
? lastMsg.username
|
|
1069
1123
|
: "?";
|
|
@@ -1086,7 +1140,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1086
1140
|
// shape `send --as-bot` produces. Opt out with SLACK_UNREPLIED_WARN=0.
|
|
1087
1141
|
if (process.env.SLACK_UNREPLIED_WARN !== "0" && lastMsgTs && (lastMsgUserId || lastMsgBotId)) {
|
|
1088
1142
|
try {
|
|
1089
|
-
const self = await authScopes(token);
|
|
1143
|
+
const self = await authScopes(token, cookie);
|
|
1090
1144
|
const isSelfAuthor =
|
|
1091
1145
|
(!!lastMsgUserId && !!self.userId && self.userId === lastMsgUserId) ||
|
|
1092
1146
|
(!!lastMsgBotId && !!self.botId && self.botId === lastMsgBotId);
|
|
@@ -1100,7 +1154,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1100
1154
|
// embedded thread_ts colliding with the appended message ts.
|
|
1101
1155
|
let editCmd = `slack edit "#${channelId}:${lastMsgTs}" "<新しい本文>" --channel-id ${channelId}`;
|
|
1102
1156
|
try {
|
|
1103
|
-
const permalink = await getPermalink(token, channelId, lastMsgTs);
|
|
1157
|
+
const permalink = await getPermalink(token, channelId, lastMsgTs, cookie);
|
|
1104
1158
|
if (permalink) editCmd = `slack edit "${permalink}" "<新しい本文>"`;
|
|
1105
1159
|
} catch {
|
|
1106
1160
|
// fall back to the --channel-id form above
|
|
@@ -1128,7 +1182,7 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1128
1182
|
}
|
|
1129
1183
|
if (best.msg && best.sim >= 0.82) {
|
|
1130
1184
|
const pct = Math.round(best.sim * 100);
|
|
1131
|
-
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)}`);
|
|
1132
1186
|
}
|
|
1133
1187
|
}
|
|
1134
1188
|
|
|
@@ -1137,14 +1191,14 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1137
1191
|
const code = safetyCode(channelId, threadTs ?? "", lastText, message);
|
|
1138
1192
|
|
|
1139
1193
|
if (args.code !== code) {
|
|
1140
|
-
const dest = await destLabel(token, channelId, ref);
|
|
1194
|
+
const dest = await destLabel(token, channelId, ref, cookie);
|
|
1141
1195
|
if (threadTs) {
|
|
1142
|
-
const parentLine = await threadParentLine(token, threadTs, threadMsgs);
|
|
1196
|
+
const parentLine = await threadParentLine(token, threadTs, threadMsgs, cookie);
|
|
1143
1197
|
// Preview the tail of the thread (up to 3 most recent messages) so the
|
|
1144
1198
|
// sender can see what's already been said and avoid repeating it.
|
|
1145
1199
|
const recent = threadMsgs.slice(-3);
|
|
1146
1200
|
const recentLines = recent.length
|
|
1147
|
-
? 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)}`))
|
|
1148
1202
|
: [" (thread context unavailable)"];
|
|
1149
1203
|
requireCode(args.code, code, [
|
|
1150
1204
|
`--- Recent messages in thread ----------------`,
|
|
@@ -1167,10 +1221,10 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1167
1221
|
]);
|
|
1168
1222
|
}
|
|
1169
1223
|
}
|
|
1170
|
-
const ts = await slackSend(token, channelId, message, threadTs, args.broadcast);
|
|
1224
|
+
const ts = await slackSend(token, channelId, message, threadTs, args.broadcast, cookie);
|
|
1171
1225
|
let permalink = "";
|
|
1172
1226
|
try {
|
|
1173
|
-
permalink = await getPermalink(token, channelId, ts);
|
|
1227
|
+
permalink = await getPermalink(token, channelId, ts, cookie);
|
|
1174
1228
|
} catch {
|
|
1175
1229
|
// fail-soft: getPermalink failure (rate limit, network, etc.) should not
|
|
1176
1230
|
// mask send success — fall back to ts-only output below.
|
|
@@ -1224,14 +1278,15 @@ interface ScheduleSendArgs {
|
|
|
1224
1278
|
code?: string;
|
|
1225
1279
|
channelId?: string;
|
|
1226
1280
|
userId?: string;
|
|
1281
|
+
cookie?: string;
|
|
1227
1282
|
}
|
|
1228
1283
|
async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<void> {
|
|
1229
1284
|
const { ref, threadTs } = parseTargetThread(args.target);
|
|
1230
1285
|
|
|
1231
1286
|
let channelId: string;
|
|
1232
1287
|
if (args.channelId) channelId = args.channelId;
|
|
1233
|
-
else if (args.userId) channelId = await openDm(token, args.userId);
|
|
1234
|
-
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);
|
|
1235
1290
|
|
|
1236
1291
|
const postAt = parsePostAt(args.at);
|
|
1237
1292
|
const postAtDate = new Date(postAt * 1000).toISOString();
|
|
@@ -1246,18 +1301,18 @@ async function cmdScheduleSend(token: string, args: ScheduleSendArgs): Promise<v
|
|
|
1246
1301
|
`---------------------------------------------`,
|
|
1247
1302
|
]);
|
|
1248
1303
|
}
|
|
1249
|
-
const id = await scheduleMessage(token, channelId, args.message, postAt, threadTs);
|
|
1304
|
+
const id = await scheduleMessage(token, channelId, args.message, postAt, threadTs, args.cookie);
|
|
1250
1305
|
console.log(`✓ Scheduled (id: ${id}, at: ${postAtDate})`);
|
|
1251
1306
|
}
|
|
1252
1307
|
|
|
1253
|
-
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> {
|
|
1254
1309
|
let channel: string | undefined;
|
|
1255
1310
|
if (channelId) {
|
|
1256
1311
|
channel = channelId;
|
|
1257
1312
|
} else if (target) {
|
|
1258
|
-
channel = await resolveChannel(token, target);
|
|
1313
|
+
channel = await resolveChannel(token, target, cookie);
|
|
1259
1314
|
}
|
|
1260
|
-
const resp = (await listScheduledMessages(token, channel)) as {
|
|
1315
|
+
const resp = (await listScheduledMessages(token, channel, cookie)) as {
|
|
1261
1316
|
scheduled_messages?: { id: string; channel_id: string; post_at: number; text: string }[];
|
|
1262
1317
|
};
|
|
1263
1318
|
const msgs = resp.scheduled_messages ?? [];
|
|
@@ -1273,11 +1328,12 @@ interface ScheduleRmArgs {
|
|
|
1273
1328
|
id: string;
|
|
1274
1329
|
code?: string;
|
|
1275
1330
|
channelId?: string;
|
|
1331
|
+
cookie?: string;
|
|
1276
1332
|
}
|
|
1277
1333
|
async function cmdScheduleRm(token: string, args: ScheduleRmArgs): Promise<void> {
|
|
1278
1334
|
let channelId: string;
|
|
1279
1335
|
if (args.channelId) channelId = args.channelId;
|
|
1280
|
-
else channelId = await resolveChannel(token, args.target);
|
|
1336
|
+
else channelId = await resolveChannel(token, args.target, args.cookie);
|
|
1281
1337
|
|
|
1282
1338
|
const code = safetyCode(channelId, args.id);
|
|
1283
1339
|
if (args.code !== code) {
|
|
@@ -1288,7 +1344,7 @@ async function cmdScheduleRm(token: string, args: ScheduleRmArgs): Promise<void>
|
|
|
1288
1344
|
`---------------------------------------------`,
|
|
1289
1345
|
]);
|
|
1290
1346
|
}
|
|
1291
|
-
await deleteScheduledMessage(token, channelId, args.id);
|
|
1347
|
+
await deleteScheduledMessage(token, channelId, args.id, args.cookie);
|
|
1292
1348
|
console.log(`✓ Deleted scheduled message ${args.id}`);
|
|
1293
1349
|
}
|
|
1294
1350
|
|
|
@@ -1301,6 +1357,7 @@ interface UploadArgs {
|
|
|
1301
1357
|
code?: string;
|
|
1302
1358
|
channelId?: string;
|
|
1303
1359
|
userId?: string;
|
|
1360
|
+
cookie?: string;
|
|
1304
1361
|
}
|
|
1305
1362
|
async function cmdUpload(token: string, args: UploadArgs): Promise<void> {
|
|
1306
1363
|
const { statSync, existsSync } = await import("node:fs");
|
|
@@ -1317,8 +1374,8 @@ async function cmdUpload(token: string, args: UploadArgs): Promise<void> {
|
|
|
1317
1374
|
|
|
1318
1375
|
let channelId: string;
|
|
1319
1376
|
if (args.channelId) channelId = args.channelId;
|
|
1320
|
-
else if (args.userId) channelId = await openDm(token, args.userId);
|
|
1321
|
-
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);
|
|
1322
1379
|
|
|
1323
1380
|
const isBatch = args.filePaths.length > 1;
|
|
1324
1381
|
const files = args.filePaths.map((fp) => {
|
|
@@ -1355,7 +1412,7 @@ async function cmdUpload(token: string, args: UploadArgs): Promise<void> {
|
|
|
1355
1412
|
const uploadOpts: { title?: string; threadTs?: string; initialComment?: string } = { title: f.title };
|
|
1356
1413
|
if (threadTs !== undefined) uploadOpts.threadTs = threadTs;
|
|
1357
1414
|
if (args.comment !== undefined && i === 0) uploadOpts.initialComment = args.comment;
|
|
1358
|
-
const { fileId, permalink } = await uploadFile(token, channelId, f.fp, uploadOpts);
|
|
1415
|
+
const { fileId, permalink } = await uploadFile(token, channelId, f.fp, uploadOpts, args.cookie);
|
|
1359
1416
|
const prefix = total > 1 ? `[${i + 1}/${total}] ` : "";
|
|
1360
1417
|
console.log(`${prefix}✓ Uploaded (file_id: ${fileId}${permalink ? `, url: ${permalink}` : ""})`);
|
|
1361
1418
|
}
|
|
@@ -1491,10 +1548,11 @@ async function main(): Promise<void> {
|
|
|
1491
1548
|
(y2) => y2.positional("channel", { type: "string", demandOption: true, describe: "#name or channel ID" }),
|
|
1492
1549
|
async (argv) => {
|
|
1493
1550
|
const token = tok(argv as W);
|
|
1551
|
+
const cookie = ck(argv as W);
|
|
1494
1552
|
const ref = argv.channel!;
|
|
1495
1553
|
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);
|
|
1554
|
+
const channelId = await resolveChannel(token, channelRef, cookie);
|
|
1555
|
+
const resp = asRecord((await conversationInfo(token, channelId, cookie)) as Json);
|
|
1498
1556
|
const ch = asRecord(resp.channel);
|
|
1499
1557
|
const name = typeof ch.name === "string" ? ch.name : channelId;
|
|
1500
1558
|
const isIm = ch.is_im === true;
|
|
@@ -1519,6 +1577,7 @@ async function main(): Promise<void> {
|
|
|
1519
1577
|
.option("code", { type: "string", describe: "Safety hash to confirm create" }),
|
|
1520
1578
|
async (argv) => {
|
|
1521
1579
|
const token = tok(argv as W);
|
|
1580
|
+
const cookie = ck(argv as W);
|
|
1522
1581
|
// Slack lowercases and strips the leading #; normalize for preview + code.
|
|
1523
1582
|
const name = argv.name!.replace(/^#/, "").toLowerCase();
|
|
1524
1583
|
const isPrivate = argv.private === true;
|
|
@@ -1533,7 +1592,7 @@ async function main(): Promise<void> {
|
|
|
1533
1592
|
]);
|
|
1534
1593
|
let created: { id: string; name: string };
|
|
1535
1594
|
try {
|
|
1536
|
-
created = await createChannel(token, name, isPrivate);
|
|
1595
|
+
created = await createChannel(token, name, isPrivate, cookie);
|
|
1537
1596
|
} catch (e: unknown) {
|
|
1538
1597
|
console.error(e instanceof Error ? e.message : String(e));
|
|
1539
1598
|
process.exit(1);
|
|
@@ -1543,14 +1602,32 @@ async function main(): Promise<void> {
|
|
|
1543
1602
|
const ids: string[] = [];
|
|
1544
1603
|
for (const ref of invites) {
|
|
1545
1604
|
try {
|
|
1546
|
-
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));
|
|
1547
1606
|
} catch (e: unknown) {
|
|
1548
1607
|
console.error(` ! could not resolve ${ref}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1549
1608
|
}
|
|
1550
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
|
+
}
|
|
1551
1628
|
if (ids.length) {
|
|
1552
1629
|
try {
|
|
1553
|
-
await inviteToChannel(token, created.id, ids);
|
|
1630
|
+
await inviteToChannel(token, created.id, ids, cookie);
|
|
1554
1631
|
console.log(`✓ Invited ${ids.length} member${ids.length === 1 ? "" : "s"}`);
|
|
1555
1632
|
} catch (e: unknown) {
|
|
1556
1633
|
console.error(` ! invite failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -1681,7 +1758,7 @@ async function main(): Promise<void> {
|
|
|
1681
1758
|
.option("count", { alias: "n", type: "number", default: 100 })
|
|
1682
1759
|
.option("json", { type: "boolean", default: false, describe: "Output raw JSON" }),
|
|
1683
1760
|
async (argv) => {
|
|
1684
|
-
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));
|
|
1685
1762
|
},
|
|
1686
1763
|
)
|
|
1687
1764
|
.command(
|
|
@@ -1730,7 +1807,7 @@ async function main(): Promise<void> {
|
|
|
1730
1807
|
// (not the user's own self-DM).
|
|
1731
1808
|
if (!args.channelId && !args.userId && args.target.startsWith("@")) {
|
|
1732
1809
|
try {
|
|
1733
|
-
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));
|
|
1734
1811
|
} catch (e: unknown) {
|
|
1735
1812
|
console.error(
|
|
1736
1813
|
`Error: could not resolve ${args.target} to a user for the bot DM.\n` +
|
|
@@ -1744,6 +1821,11 @@ async function main(): Promise<void> {
|
|
|
1744
1821
|
args.asBot = true;
|
|
1745
1822
|
} else {
|
|
1746
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;
|
|
1747
1829
|
}
|
|
1748
1830
|
// Print a clean error instead of yargs' usage dump when a send fails at
|
|
1749
1831
|
// runtime (e.g. missing scope, channel not found). The confirm gate
|
|
@@ -1791,6 +1873,8 @@ async function main(): Promise<void> {
|
|
|
1791
1873
|
if (argv.code) args.code = argv.code;
|
|
1792
1874
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1793
1875
|
if (argv["user-id"]) args.userId = argv["user-id"];
|
|
1876
|
+
const cookie = ck(argv as W);
|
|
1877
|
+
if (cookie) args.cookie = cookie;
|
|
1794
1878
|
await cmdScheduleSend(tok(argv as W), args);
|
|
1795
1879
|
},
|
|
1796
1880
|
)
|
|
@@ -1801,7 +1885,7 @@ async function main(): Promise<void> {
|
|
|
1801
1885
|
.positional("target", { type: "string", describe: "#channel to filter by" })
|
|
1802
1886
|
.option("channel-id", { type: "string", describe: "Raw channel ID" }),
|
|
1803
1887
|
async (argv) => {
|
|
1804
|
-
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));
|
|
1805
1889
|
},
|
|
1806
1890
|
)
|
|
1807
1891
|
.command(
|
|
@@ -1815,6 +1899,8 @@ async function main(): Promise<void> {
|
|
|
1815
1899
|
async (argv) => {
|
|
1816
1900
|
const args: ScheduleRmArgs = { target: argv.target!, id: argv.id! };
|
|
1817
1901
|
if (argv.code) args.code = argv.code;
|
|
1902
|
+
const cookie = ck(argv as W);
|
|
1903
|
+
if (cookie) args.cookie = cookie;
|
|
1818
1904
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1819
1905
|
await cmdScheduleRm(tok(argv as W), args);
|
|
1820
1906
|
},
|
|
@@ -1837,6 +1923,8 @@ async function main(): Promise<void> {
|
|
|
1837
1923
|
if (argv.code) args.code = argv.code;
|
|
1838
1924
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1839
1925
|
if (argv.mentions !== false) args.mentions = true;
|
|
1926
|
+
const cookie = ck(argv as W);
|
|
1927
|
+
if (cookie) args.cookie = cookie;
|
|
1840
1928
|
await cmdEdit(tok(argv as W), args);
|
|
1841
1929
|
},
|
|
1842
1930
|
)
|
|
@@ -1851,6 +1939,8 @@ async function main(): Promise<void> {
|
|
|
1851
1939
|
const args: DeleteArgs = { target: argv.target! };
|
|
1852
1940
|
if (argv.code) args.code = argv.code;
|
|
1853
1941
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1942
|
+
const cookie = ck(argv as W);
|
|
1943
|
+
if (cookie) args.cookie = cookie;
|
|
1854
1944
|
await cmdDelete(tok(argv as W), args);
|
|
1855
1945
|
},
|
|
1856
1946
|
)
|
|
@@ -1866,6 +1956,8 @@ async function main(): Promise<void> {
|
|
|
1866
1956
|
const args: ReactArgs = { target: argv.target!, emoji: argv.emoji! };
|
|
1867
1957
|
if (argv.remove) args.remove = true;
|
|
1868
1958
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1959
|
+
const cookie = ck(argv as W);
|
|
1960
|
+
if (cookie) args.cookie = cookie;
|
|
1869
1961
|
// Print a clean one-line error instead of yargs' usage dump + stack when
|
|
1870
1962
|
// the reaction fails at runtime (missing_scope, invalid_name, etc.).
|
|
1871
1963
|
try {
|
|
@@ -1894,6 +1986,8 @@ async function main(): Promise<void> {
|
|
|
1894
1986
|
if (argv.code) args.code = argv.code;
|
|
1895
1987
|
if (argv["channel-id"]) args.channelId = argv["channel-id"];
|
|
1896
1988
|
if (argv["user-id"]) args.userId = argv["user-id"];
|
|
1989
|
+
const cookie = ck(argv as W);
|
|
1990
|
+
if (cookie) args.cookie = cookie;
|
|
1897
1991
|
await cmdUpload(tok(argv as W), args);
|
|
1898
1992
|
},
|
|
1899
1993
|
)
|