slack-term 1.22.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +5 -0
- package/dist/cli.js +72 -72
- package/package.json +1 -1
- package/ts/cli.ts +64 -2
package/package.json
CHANGED
package/ts/cli.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { cmdTail } from "./tail.ts";
|
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
authTest,
|
|
18
|
+
authScopes,
|
|
18
19
|
authTestSession,
|
|
19
20
|
conversationInfoSession,
|
|
20
21
|
createChannel,
|
|
@@ -1021,6 +1022,15 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1021
1022
|
let lastText = "";
|
|
1022
1023
|
let lastUser = "?";
|
|
1023
1024
|
let threadMsgs: Record<string, Json>[] = [];
|
|
1025
|
+
// Author of the destination's TRUE most recent message (thread-scoped for a
|
|
1026
|
+
// thread reply, channel-scoped for a top-level send) — feeds the unreplied-warn
|
|
1027
|
+
// check below. Tracks `user` (normal message) and `bot_id` (a `bot_message`
|
|
1028
|
+
// subtype post — e.g. `send --as-bot` — which carries bot_id/username but no
|
|
1029
|
+
// `user`) so ownership can be determined either way. Undefined when the
|
|
1030
|
+
// preview fetch failed (fail-soft).
|
|
1031
|
+
let lastMsgUserId: string | undefined;
|
|
1032
|
+
let lastMsgBotId: string | undefined;
|
|
1033
|
+
let lastMsgTs: string | undefined;
|
|
1024
1034
|
if (threadTs) {
|
|
1025
1035
|
try {
|
|
1026
1036
|
// Fetch a generous window and preview its tail. conversations.replies is
|
|
@@ -1032,14 +1042,23 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1032
1042
|
// Bind the hash to the thread's most recent message: if someone replies
|
|
1033
1043
|
// between preview and confirm, the code invalidates and re-previews.
|
|
1034
1044
|
lastText = typeof lastMsg?.text === "string" ? lastMsg.text : "";
|
|
1045
|
+
lastMsgUserId = typeof lastMsg?.user === "string" ? lastMsg.user : undefined;
|
|
1046
|
+
lastMsgBotId = typeof lastMsg?.bot_id === "string" ? lastMsg.bot_id : undefined;
|
|
1047
|
+
lastMsgTs = typeof lastMsg?.ts === "string" ? lastMsg.ts : undefined;
|
|
1035
1048
|
} catch {
|
|
1036
1049
|
// best-effort preview only
|
|
1037
1050
|
}
|
|
1038
1051
|
} else {
|
|
1039
1052
|
try {
|
|
1040
1053
|
const ctx = (await history(token, channelId, 1)) as Record<string, Json>;
|
|
1041
|
-
|
|
1042
|
-
|
|
1054
|
+
// limit=1 already narrows this to exactly the channel's true last message
|
|
1055
|
+
// (subtype or not) — take it raw for the ownership check below. The
|
|
1056
|
+
// subtype filter is only for the human-readable preview text/author: it
|
|
1057
|
+
// deliberately hides system events (channel_join etc.), but a bot_message
|
|
1058
|
+
// post is a real message and must NOT be dropped here, or a bot's own
|
|
1059
|
+
// last post would silently disable the unreplied-warn for --as-bot sends.
|
|
1060
|
+
const rawMsgs = asArray(ctx.messages).map(asRecord);
|
|
1061
|
+
const lastMsg = rawMsgs.filter((m) => m.subtype === undefined || m.subtype === null)[0];
|
|
1043
1062
|
lastText = typeof lastMsg?.text === "string" ? lastMsg.text : "";
|
|
1044
1063
|
// Resolve the author's user ID to a display name for the preview; fall back
|
|
1045
1064
|
// to the bot username, then the raw ID. userName() is fail-soft.
|
|
@@ -1048,11 +1067,54 @@ async function cmdSend(token: string, args: SendArgs): Promise<void> {
|
|
|
1048
1067
|
: typeof lastMsg?.username === "string"
|
|
1049
1068
|
? lastMsg.username
|
|
1050
1069
|
: "?";
|
|
1070
|
+
const rawLast = rawMsgs[0];
|
|
1071
|
+
lastMsgUserId = typeof rawLast?.user === "string" ? rawLast.user : undefined;
|
|
1072
|
+
lastMsgBotId = typeof rawLast?.bot_id === "string" ? rawLast.bot_id : undefined;
|
|
1073
|
+
lastMsgTs = typeof rawLast?.ts === "string" ? rawLast.ts : undefined;
|
|
1051
1074
|
} catch {
|
|
1052
1075
|
// best-effort preview only
|
|
1053
1076
|
}
|
|
1054
1077
|
}
|
|
1055
1078
|
|
|
1079
|
+
// Unreplied guard: the destination's last message is our own and no one has
|
|
1080
|
+
// replied since (by definition, since it's still the most recent message) —
|
|
1081
|
+
// sending another new message risks spamming the same person twice. Warn
|
|
1082
|
+
// (never block) and point at `slack edit` on that message instead. Reply-status
|
|
1083
|
+
// based, not time-based, so it still fires even long after the last send.
|
|
1084
|
+
// Self-identity covers both a normal post (`user` = our user id) and a
|
|
1085
|
+
// `bot_message`-subtype post (`bot_id` = our app's bot id, no `user`), the
|
|
1086
|
+
// shape `send --as-bot` produces. Opt out with SLACK_UNREPLIED_WARN=0.
|
|
1087
|
+
if (process.env.SLACK_UNREPLIED_WARN !== "0" && lastMsgTs && (lastMsgUserId || lastMsgBotId)) {
|
|
1088
|
+
try {
|
|
1089
|
+
const self = await authScopes(token);
|
|
1090
|
+
const isSelfAuthor =
|
|
1091
|
+
(!!lastMsgUserId && !!self.userId && self.userId === lastMsgUserId) ||
|
|
1092
|
+
(!!lastMsgBotId && !!self.botId && self.botId === lastMsgBotId);
|
|
1093
|
+
if (isSelfAuthor) {
|
|
1094
|
+
// Prefer a real permalink (exact, copy-pasteable). If that lookup fails,
|
|
1095
|
+
// fall back to a form guaranteed parseable by `slack edit` regardless of
|
|
1096
|
+
// what shape `ref` is in (a bare channel/user name, a raw ID resolved
|
|
1097
|
+
// from a permalink, or a thread ref) — `#<channelId>:<ts>` always yields
|
|
1098
|
+
// a ts split, and `--channel-id` makes the leading "#name" irrelevant to
|
|
1099
|
+
// resolution, so it never depends on ref's original prefix or an
|
|
1100
|
+
// embedded thread_ts colliding with the appended message ts.
|
|
1101
|
+
let editCmd = `slack edit "#${channelId}:${lastMsgTs}" "<新しい本文>" --channel-id ${channelId}`;
|
|
1102
|
+
try {
|
|
1103
|
+
const permalink = await getPermalink(token, channelId, lastMsgTs);
|
|
1104
|
+
if (permalink) editCmd = `slack edit "${permalink}" "<新しい本文>"`;
|
|
1105
|
+
} catch {
|
|
1106
|
+
// fall back to the --channel-id form above
|
|
1107
|
+
}
|
|
1108
|
+
console.error(`⚠ 相手はまだ返信していません(最後のメッセージはあなたのものです)。`);
|
|
1109
|
+
console.error(` 連投を避けるため、新規送信でなく直前のメッセージの edit を検討してください:`);
|
|
1110
|
+
console.error(` ${editCmd}`);
|
|
1111
|
+
console.error(` このまま送信する場合は --code=XXXX で確定。`);
|
|
1112
|
+
}
|
|
1113
|
+
} catch {
|
|
1114
|
+
// best-effort; a failed self-identity lookup must not block the send
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1056
1118
|
// Duplicate guard: if the outgoing message closely matches something already
|
|
1057
1119
|
// in the thread, warn (never block). Catches re-posting a reply you already
|
|
1058
1120
|
// sent. Threshold is deliberately high so only near-identical text trips it.
|