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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-term",
3
- "version": "1.23.0",
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": "tsc --noEmit",
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
- async function cmdSearch(token: string, query: string, count: number, json: boolean): Promise<void> {
391
- const resp = await searchAll(token, query, count);
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(token, rawName);
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(token, m, cache, chLabel));
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
  )