agent-slack 0.6.1 → 0.8.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/README.md +5 -0
- package/dist/index.js +1692 -488
- package/dist/index.js.map +21 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1819,6 +1819,41 @@ class SlackApiClient {
|
|
|
1819
1819
|
this.web = new WebClient(auth.token);
|
|
1820
1820
|
}
|
|
1821
1821
|
}
|
|
1822
|
+
async apiMultipart(method, params = {}) {
|
|
1823
|
+
if (this.auth.auth_type === "standard") {
|
|
1824
|
+
return this.api(method, params);
|
|
1825
|
+
}
|
|
1826
|
+
if (!this.workspaceUrl) {
|
|
1827
|
+
throw new Error("Browser auth requires workspace URL.");
|
|
1828
|
+
}
|
|
1829
|
+
const auth = this.auth;
|
|
1830
|
+
const url = `${this.workspaceUrl.replace(/\/$/, "")}/api/${method}`;
|
|
1831
|
+
const fd = new FormData;
|
|
1832
|
+
fd.append("token", auth.xoxc_token);
|
|
1833
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1834
|
+
if (v !== undefined) {
|
|
1835
|
+
fd.append(k, typeof v === "object" ? JSON.stringify(v) : String(v));
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const response = await fetch(url, {
|
|
1839
|
+
method: "POST",
|
|
1840
|
+
headers: {
|
|
1841
|
+
Cookie: `d=${encodeURIComponent(auth.xoxd_cookie)}`,
|
|
1842
|
+
Origin: "https://app.slack.com",
|
|
1843
|
+
"User-Agent": getUserAgent()
|
|
1844
|
+
},
|
|
1845
|
+
body: fd
|
|
1846
|
+
});
|
|
1847
|
+
const data = await response.json().catch(() => ({}));
|
|
1848
|
+
if (!response.ok) {
|
|
1849
|
+
throw new Error(`Slack HTTP ${response.status} calling ${method}`);
|
|
1850
|
+
}
|
|
1851
|
+
if (!isRecord3(data) || data.ok !== true) {
|
|
1852
|
+
const error = isRecord3(data) && typeof data.error === "string" ? data.error : null;
|
|
1853
|
+
throw new Error(error || `Slack API error calling ${method}`);
|
|
1854
|
+
}
|
|
1855
|
+
return data;
|
|
1856
|
+
}
|
|
1822
1857
|
async api(method, params = {}) {
|
|
1823
1858
|
if (this.auth.auth_type === "standard") {
|
|
1824
1859
|
if (!this.web) {
|
|
@@ -3892,191 +3927,661 @@ function toThreadListMessage(m) {
|
|
|
3892
3927
|
return rest;
|
|
3893
3928
|
}
|
|
3894
3929
|
|
|
3895
|
-
// src/
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
const
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
const
|
|
3910
|
-
const
|
|
3911
|
-
const
|
|
3912
|
-
const
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
});
|
|
3925
|
-
const includeReactions = Boolean(input.options.includeReactions);
|
|
3926
|
-
const { client, auth, workspace_url } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
3927
|
-
const channelId = await resolveChannelId(client, target.channel);
|
|
3928
|
-
const ref = {
|
|
3929
|
-
workspace_url: workspace_url ?? workspaceUrl ?? "",
|
|
3930
|
-
channel_id: channelId,
|
|
3931
|
-
message_ts: ts,
|
|
3932
|
-
thread_ts_hint: input.options.threadTs?.trim() || undefined,
|
|
3933
|
-
raw: input.targetInput
|
|
3934
|
-
};
|
|
3935
|
-
const msg = await fetchMessage(client, { ref, includeReactions });
|
|
3936
|
-
const thread = await getThreadSummary(client, { channelId, msg });
|
|
3937
|
-
const downloadedPaths = await downloadMessageFiles({ auth, messages: [msg] });
|
|
3938
|
-
const maxBodyChars = Number.parseInt(input.options.maxBodyChars, 10);
|
|
3939
|
-
const message = toCompactMessage(msg, { maxBodyChars, includeReactions, downloadedPaths });
|
|
3940
|
-
return pruneEmpty({ message, thread });
|
|
3941
|
-
}
|
|
3942
|
-
});
|
|
3943
|
-
}
|
|
3944
|
-
async function handleMessageList(input) {
|
|
3945
|
-
const target = parseMsgTarget(input.targetInput);
|
|
3946
|
-
if (target.kind === "user") {
|
|
3947
|
-
throw new Error("message list does not support user ID targets. Use a channel name, channel ID, or message URL.");
|
|
3948
|
-
}
|
|
3949
|
-
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
|
|
3950
|
-
return input.ctx.withAutoRefresh({
|
|
3951
|
-
workspaceUrl: target.kind === "url" ? target.ref.workspace_url : workspaceUrl,
|
|
3952
|
-
work: async () => {
|
|
3953
|
-
const withReactions = parseReactionFilters(input.options.withReaction);
|
|
3954
|
-
const withoutReactions = parseReactionFilters(input.options.withoutReaction);
|
|
3955
|
-
const hasReactionFilters = withReactions.length > 0 || withoutReactions.length > 0;
|
|
3956
|
-
if (target.kind === "url") {
|
|
3957
|
-
if (hasReactionFilters) {
|
|
3958
|
-
throw new Error("Reaction filters are only supported for channel history mode (not message URL thread mode)");
|
|
3930
|
+
// src/slack/user-cache.ts
|
|
3931
|
+
import { createHash } from "node:crypto";
|
|
3932
|
+
import { join as join13 } from "node:path";
|
|
3933
|
+
|
|
3934
|
+
// src/slack/users.ts
|
|
3935
|
+
async function listUsers(client, options) {
|
|
3936
|
+
const limit = Math.min(Math.max(options?.limit ?? 200, 1), 1000);
|
|
3937
|
+
const includeBots = options?.includeBots ?? false;
|
|
3938
|
+
let next_cursor;
|
|
3939
|
+
const [out, dmMap] = await Promise.all([
|
|
3940
|
+
(async () => {
|
|
3941
|
+
const users = [];
|
|
3942
|
+
let cursor = options?.cursor;
|
|
3943
|
+
while (users.length < limit) {
|
|
3944
|
+
const pageSize = Math.min(200, limit - users.length);
|
|
3945
|
+
const resp = await client.api("users.list", { limit: pageSize, cursor });
|
|
3946
|
+
const members = asArray(resp.members).filter(isRecord);
|
|
3947
|
+
for (const m of members) {
|
|
3948
|
+
const id = getString(m.id);
|
|
3949
|
+
if (!id) {
|
|
3950
|
+
continue;
|
|
3951
|
+
}
|
|
3952
|
+
if (!includeBots && m.is_bot) {
|
|
3953
|
+
continue;
|
|
3954
|
+
}
|
|
3955
|
+
users.push(toCompactUser(m));
|
|
3956
|
+
if (users.length >= limit) {
|
|
3957
|
+
break;
|
|
3958
|
+
}
|
|
3959
3959
|
}
|
|
3960
|
-
const
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
channelId: ref.channel_id,
|
|
3968
|
-
threadTs: rootTs2,
|
|
3969
|
-
includeReactions: includeReactions2
|
|
3970
|
-
});
|
|
3971
|
-
const downloadedPaths2 = await downloadMessageFiles({ auth: auth2, messages: threadMessages2 });
|
|
3972
|
-
const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
|
|
3973
|
-
return pruneEmpty({
|
|
3974
|
-
messages: threadMessages2.map((m) => toCompactMessage(m, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 })).map(toThreadListMessage)
|
|
3975
|
-
});
|
|
3976
|
-
}
|
|
3977
|
-
const { client, auth, workspace_url } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
3978
|
-
await input.ctx.assertWorkspaceSpecifiedForChannelNames({
|
|
3979
|
-
workspaceUrl,
|
|
3980
|
-
channels: [target.channel]
|
|
3981
|
-
});
|
|
3982
|
-
const channelId = await resolveChannelId(client, target.channel);
|
|
3983
|
-
const threadTs = input.options.threadTs?.trim();
|
|
3984
|
-
const ts = input.options.ts?.trim();
|
|
3985
|
-
if (!threadTs && !ts) {
|
|
3986
|
-
const includeReactions2 = Boolean(input.options.includeReactions);
|
|
3987
|
-
const limit = parseLimit(input.options.limit);
|
|
3988
|
-
const oldest = requireOldestWhenReactionFiltersUsed({
|
|
3989
|
-
oldest: input.options.oldest,
|
|
3990
|
-
withReactions,
|
|
3991
|
-
withoutReactions
|
|
3992
|
-
});
|
|
3993
|
-
const channelMessages = await fetchChannelHistory(client, {
|
|
3994
|
-
channelId,
|
|
3995
|
-
limit,
|
|
3996
|
-
latest: input.options.latest?.trim(),
|
|
3997
|
-
oldest,
|
|
3998
|
-
includeReactions: includeReactions2 || hasReactionFilters,
|
|
3999
|
-
withReactions,
|
|
4000
|
-
withoutReactions
|
|
4001
|
-
});
|
|
4002
|
-
const downloadedPaths2 = await downloadMessageFiles({ auth, messages: channelMessages });
|
|
4003
|
-
const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
|
|
4004
|
-
return pruneEmpty({
|
|
4005
|
-
channel_id: channelId,
|
|
4006
|
-
messages: channelMessages.map((m) => toCompactMessage(m, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 }))
|
|
4007
|
-
});
|
|
4008
|
-
}
|
|
4009
|
-
if (hasReactionFilters) {
|
|
4010
|
-
throw new Error("Reaction filters are only supported for channel history mode (without --thread-ts/--ts)");
|
|
3960
|
+
const meta = isRecord(resp.response_metadata) ? resp.response_metadata : null;
|
|
3961
|
+
const next = meta ? getString(meta.next_cursor) : undefined;
|
|
3962
|
+
if (!next) {
|
|
3963
|
+
break;
|
|
3964
|
+
}
|
|
3965
|
+
cursor = next;
|
|
3966
|
+
next_cursor = next;
|
|
4011
3967
|
}
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
const msg = await fetchMessage(client, { ref, includeReactions: includeReactions2 });
|
|
4021
|
-
return msg.thread_ts ?? msg.ts;
|
|
4022
|
-
})();
|
|
4023
|
-
const includeReactions = Boolean(input.options.includeReactions);
|
|
4024
|
-
const threadMessages = await fetchThread(client, {
|
|
4025
|
-
channelId,
|
|
4026
|
-
threadTs: rootTs,
|
|
4027
|
-
includeReactions
|
|
4028
|
-
});
|
|
4029
|
-
const downloadedPaths = await downloadMessageFiles({ auth, messages: threadMessages });
|
|
4030
|
-
const maxBodyChars = Number.parseInt(input.options.maxBodyChars, 10);
|
|
4031
|
-
return pruneEmpty({
|
|
4032
|
-
messages: threadMessages.map((m) => toCompactMessage(m, { maxBodyChars, includeReactions, downloadedPaths })).map(toThreadListMessage)
|
|
4033
|
-
});
|
|
3968
|
+
return users;
|
|
3969
|
+
})(),
|
|
3970
|
+
fetchDmMap(client)
|
|
3971
|
+
]);
|
|
3972
|
+
for (const u of out) {
|
|
3973
|
+
const dmId = dmMap.get(u.id);
|
|
3974
|
+
if (dmId) {
|
|
3975
|
+
u.dm_id = dmId;
|
|
4034
3976
|
}
|
|
4035
|
-
});
|
|
4036
|
-
}
|
|
4037
|
-
|
|
4038
|
-
// src/cli/message-actions.ts
|
|
4039
|
-
function parseLimit(raw) {
|
|
4040
|
-
if (raw === undefined) {
|
|
4041
|
-
return;
|
|
4042
3977
|
}
|
|
4043
|
-
|
|
4044
|
-
if (!Number.isFinite(n) || n < 1) {
|
|
4045
|
-
throw new Error(`Invalid --limit value "${raw}": must be a positive integer`);
|
|
4046
|
-
}
|
|
4047
|
-
return n;
|
|
3978
|
+
return { users: out, next_cursor };
|
|
4048
3979
|
}
|
|
4049
|
-
function
|
|
4050
|
-
const
|
|
4051
|
-
if (!
|
|
4052
|
-
throw new Error(
|
|
3980
|
+
async function getUser(client, input) {
|
|
3981
|
+
const trimmed = input.trim();
|
|
3982
|
+
if (!trimmed) {
|
|
3983
|
+
throw new Error("User is empty");
|
|
4053
3984
|
}
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
if (!Array.isArray(raw) || raw.length === 0) {
|
|
4058
|
-
return [];
|
|
3985
|
+
const userId = await resolveUserId(client, trimmed);
|
|
3986
|
+
if (!userId) {
|
|
3987
|
+
throw new Error(`Could not resolve user: ${input}`);
|
|
4059
3988
|
}
|
|
4060
|
-
const
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
out.push(normalized);
|
|
4065
|
-
}
|
|
3989
|
+
const resp = await client.api("users.info", { user: userId });
|
|
3990
|
+
const u = isRecord(resp.user) ? resp.user : null;
|
|
3991
|
+
if (!u || !getString(u.id)) {
|
|
3992
|
+
throw new Error("users.info returned no user");
|
|
4066
3993
|
}
|
|
4067
|
-
return
|
|
3994
|
+
return toCompactUser(u);
|
|
4068
3995
|
}
|
|
4069
|
-
function
|
|
4070
|
-
const
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
return oldest;
|
|
3996
|
+
async function resolveUserId(client, input) {
|
|
3997
|
+
const trimmed = input.trim();
|
|
3998
|
+
if (/^U[A-Z0-9]{8,}$/.test(trimmed)) {
|
|
3999
|
+
return trimmed;
|
|
4074
4000
|
}
|
|
4075
|
-
|
|
4076
|
-
|
|
4001
|
+
const looksLikeEmail = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(trimmed) && !trimmed.startsWith("@");
|
|
4002
|
+
if (looksLikeEmail) {
|
|
4003
|
+
try {
|
|
4004
|
+
const byEmail = await client.api("users.lookupByEmail", { email: trimmed });
|
|
4005
|
+
const user = isRecord(byEmail.user) ? byEmail.user : null;
|
|
4006
|
+
const userId = user ? getString(user.id) : undefined;
|
|
4007
|
+
if (userId) {
|
|
4008
|
+
return userId;
|
|
4009
|
+
}
|
|
4010
|
+
} catch {}
|
|
4077
4011
|
}
|
|
4078
|
-
|
|
4079
|
-
|
|
4012
|
+
const handle = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
4013
|
+
if (!handle) {
|
|
4014
|
+
return null;
|
|
4015
|
+
}
|
|
4016
|
+
let cursor;
|
|
4017
|
+
for (;; ) {
|
|
4018
|
+
const resp = await client.api("users.list", { limit: 200, cursor });
|
|
4019
|
+
const members = asArray(resp.members).filter(isRecord);
|
|
4020
|
+
const found = members.find((m) => {
|
|
4021
|
+
if (getString(m.name) === handle) {
|
|
4022
|
+
return true;
|
|
4023
|
+
}
|
|
4024
|
+
if (looksLikeEmail) {
|
|
4025
|
+
const profile = isRecord(m.profile) ? m.profile : null;
|
|
4026
|
+
const email = profile ? getString(profile.email) : undefined;
|
|
4027
|
+
return Boolean(email) && email?.toLowerCase() === trimmed.toLowerCase();
|
|
4028
|
+
}
|
|
4029
|
+
return false;
|
|
4030
|
+
});
|
|
4031
|
+
if (found) {
|
|
4032
|
+
const id = getString(found.id);
|
|
4033
|
+
if (id) {
|
|
4034
|
+
return id;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
const meta = isRecord(resp.response_metadata) ? resp.response_metadata : null;
|
|
4038
|
+
const next = meta ? getString(meta.next_cursor) : undefined;
|
|
4039
|
+
if (!next) {
|
|
4040
|
+
break;
|
|
4041
|
+
}
|
|
4042
|
+
cursor = next;
|
|
4043
|
+
}
|
|
4044
|
+
return null;
|
|
4045
|
+
}
|
|
4046
|
+
async function fetchDmMap(client) {
|
|
4047
|
+
const map = new Map;
|
|
4048
|
+
let cursor;
|
|
4049
|
+
for (;; ) {
|
|
4050
|
+
const resp = await client.api("conversations.list", {
|
|
4051
|
+
types: "im",
|
|
4052
|
+
limit: 200,
|
|
4053
|
+
cursor
|
|
4054
|
+
});
|
|
4055
|
+
const channels = asArray(resp.channels).filter(isRecord);
|
|
4056
|
+
for (const ch of channels) {
|
|
4057
|
+
const id = getString(ch.id);
|
|
4058
|
+
const user = getString(ch.user);
|
|
4059
|
+
if (id && user) {
|
|
4060
|
+
map.set(user, id);
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
const meta = isRecord(resp.response_metadata) ? resp.response_metadata : null;
|
|
4064
|
+
const next = meta ? getString(meta.next_cursor) : undefined;
|
|
4065
|
+
if (!next) {
|
|
4066
|
+
break;
|
|
4067
|
+
}
|
|
4068
|
+
cursor = next;
|
|
4069
|
+
}
|
|
4070
|
+
return map;
|
|
4071
|
+
}
|
|
4072
|
+
async function getDmChannelForUsers(client, inputs) {
|
|
4073
|
+
if (!inputs || inputs.length === 0) {
|
|
4074
|
+
throw new Error("At least one user is required");
|
|
4075
|
+
}
|
|
4076
|
+
if (inputs.length > 8) {
|
|
4077
|
+
throw new Error("Slack supports a maximum of 8 users in a group DM");
|
|
4078
|
+
}
|
|
4079
|
+
const userIds = [];
|
|
4080
|
+
for (const input of inputs) {
|
|
4081
|
+
const trimmed = input.trim();
|
|
4082
|
+
if (!trimmed) {
|
|
4083
|
+
continue;
|
|
4084
|
+
}
|
|
4085
|
+
const userId = await resolveUserId(client, trimmed);
|
|
4086
|
+
if (!userId) {
|
|
4087
|
+
throw new Error(`Could not resolve user: ${input}`);
|
|
4088
|
+
}
|
|
4089
|
+
userIds.push(userId);
|
|
4090
|
+
}
|
|
4091
|
+
if (userIds.length === 0) {
|
|
4092
|
+
throw new Error("No valid users provided");
|
|
4093
|
+
}
|
|
4094
|
+
const resp = await client.api("conversations.open", { users: userIds.join(",") });
|
|
4095
|
+
const channel = isRecord(resp.channel) ? resp.channel : null;
|
|
4096
|
+
const channelId = channel ? getString(channel.id) : null;
|
|
4097
|
+
if (!channelId) {
|
|
4098
|
+
throw new Error("conversations.open returned no channel");
|
|
4099
|
+
}
|
|
4100
|
+
const channelType = channelId.startsWith("D") ? "dm" : "group_dm";
|
|
4101
|
+
return {
|
|
4102
|
+
user_ids: userIds,
|
|
4103
|
+
dm_channel_id: channelId,
|
|
4104
|
+
channel_type: channelType
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
function toCompactUser(u) {
|
|
4108
|
+
const profile = isRecord(u.profile) ? u.profile : {};
|
|
4109
|
+
return {
|
|
4110
|
+
id: getString(u.id) ?? "",
|
|
4111
|
+
name: getString(u.name) ?? undefined,
|
|
4112
|
+
real_name: getString(u.real_name) ?? getString(profile.real_name) ?? undefined,
|
|
4113
|
+
display_name: getString(profile.display_name) ?? undefined,
|
|
4114
|
+
email: getString(profile.email) ?? undefined,
|
|
4115
|
+
title: getString(profile.title) ?? undefined,
|
|
4116
|
+
tz: getString(u.tz) ?? undefined,
|
|
4117
|
+
is_bot: typeof u.is_bot === "boolean" ? u.is_bot : undefined,
|
|
4118
|
+
deleted: typeof u.deleted === "boolean" ? u.deleted : undefined
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
// src/slack/user-cache.ts
|
|
4123
|
+
var CACHE_VERSION = 1;
|
|
4124
|
+
var USER_TTL_MS = 24 * 60 * 60 * 1000;
|
|
4125
|
+
var USER_ID_PATTERN = /^(U|W)[A-Z0-9]{8,}$/;
|
|
4126
|
+
var USER_MENTION_PATTERN = /<@((U|W)[A-Z0-9]{8,})(?:\|[^>]+)?>/g;
|
|
4127
|
+
async function resolveUsersById(input) {
|
|
4128
|
+
const uniqueIds = dedupeUserIds(input.userIds);
|
|
4129
|
+
if (uniqueIds.length === 0) {
|
|
4130
|
+
return new Map;
|
|
4131
|
+
}
|
|
4132
|
+
const forceRefresh = input.forceRefresh ?? false;
|
|
4133
|
+
const now = Date.now();
|
|
4134
|
+
const workspaceKey = hashWorkspaceUrl(input.workspaceUrl);
|
|
4135
|
+
const isUnknownWorkspace = workspaceKey === "unknown";
|
|
4136
|
+
const cachePath = isUnknownWorkspace ? "" : join13(getAppDir(), `users-cache-${workspaceKey}.json`);
|
|
4137
|
+
const diskCache = cachePath ? await loadCache(cachePath) : { version: CACHE_VERSION, entries: {} };
|
|
4138
|
+
const out = new Map;
|
|
4139
|
+
const missing = [];
|
|
4140
|
+
for (const userId of uniqueIds) {
|
|
4141
|
+
const cached = diskCache.entries[userId];
|
|
4142
|
+
if (!forceRefresh && cached && now - cached.fetched_at < USER_TTL_MS) {
|
|
4143
|
+
out.set(userId, cached.user);
|
|
4144
|
+
continue;
|
|
4145
|
+
}
|
|
4146
|
+
missing.push(userId);
|
|
4147
|
+
}
|
|
4148
|
+
let cacheChanged = false;
|
|
4149
|
+
if (missing.length > 0) {
|
|
4150
|
+
const fetched = [];
|
|
4151
|
+
const concurrency = 5;
|
|
4152
|
+
for (let i = 0;i < missing.length; i += concurrency) {
|
|
4153
|
+
const chunk = missing.slice(i, i + concurrency);
|
|
4154
|
+
const results = await Promise.all(chunk.map(async (userId) => ({ userId, user: await fetchUserById(input.client, userId) })));
|
|
4155
|
+
fetched.push(...results);
|
|
4156
|
+
}
|
|
4157
|
+
for (const item of fetched) {
|
|
4158
|
+
if (!item.user) {
|
|
4159
|
+
continue;
|
|
4160
|
+
}
|
|
4161
|
+
const entry = {
|
|
4162
|
+
fetched_at: now,
|
|
4163
|
+
user: item.user
|
|
4164
|
+
};
|
|
4165
|
+
diskCache.entries[item.userId] = entry;
|
|
4166
|
+
out.set(item.userId, item.user);
|
|
4167
|
+
cacheChanged = true;
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
if (cachePath) {
|
|
4171
|
+
const prunedCache = pruneExpiredEntries(diskCache, now);
|
|
4172
|
+
if (Object.keys(diskCache.entries).length !== Object.keys(prunedCache.entries).length) {
|
|
4173
|
+
cacheChanged = true;
|
|
4174
|
+
}
|
|
4175
|
+
if (cacheChanged) {
|
|
4176
|
+
await writeCache(cachePath, prunedCache);
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
return out;
|
|
4180
|
+
}
|
|
4181
|
+
function collectReferencedUserIds(messages, options) {
|
|
4182
|
+
const ids = new Set;
|
|
4183
|
+
const includeReactions = options?.includeReactions ?? false;
|
|
4184
|
+
for (const message of messages) {
|
|
4185
|
+
collectUserIdsFromMessage(message, ids, { includeReactions });
|
|
4186
|
+
}
|
|
4187
|
+
return Array.from(ids);
|
|
4188
|
+
}
|
|
4189
|
+
function toReferencedUsers(userIds, usersById) {
|
|
4190
|
+
const out = {};
|
|
4191
|
+
for (const userId of dedupeUserIds(userIds)) {
|
|
4192
|
+
const user = usersById.get(userId);
|
|
4193
|
+
if (!user) {
|
|
4194
|
+
continue;
|
|
4195
|
+
}
|
|
4196
|
+
out[userId] = user;
|
|
4197
|
+
}
|
|
4198
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
4199
|
+
}
|
|
4200
|
+
function dedupeUserIds(ids) {
|
|
4201
|
+
const seen = new Set;
|
|
4202
|
+
for (const raw of ids) {
|
|
4203
|
+
const userId = String(raw).trim();
|
|
4204
|
+
if (!USER_ID_PATTERN.test(userId)) {
|
|
4205
|
+
continue;
|
|
4206
|
+
}
|
|
4207
|
+
seen.add(userId);
|
|
4208
|
+
}
|
|
4209
|
+
return Array.from(seen);
|
|
4210
|
+
}
|
|
4211
|
+
function hashWorkspaceUrl(workspaceUrl) {
|
|
4212
|
+
const trimmed = workspaceUrl.trim();
|
|
4213
|
+
if (!trimmed) {
|
|
4214
|
+
return "unknown";
|
|
4215
|
+
}
|
|
4216
|
+
let source = trimmed;
|
|
4217
|
+
try {
|
|
4218
|
+
source = new URL(trimmed).hostname.toLowerCase();
|
|
4219
|
+
} catch {
|
|
4220
|
+
source = trimmed.toLowerCase();
|
|
4221
|
+
}
|
|
4222
|
+
if (!source || source === "unknown") {
|
|
4223
|
+
return "unknown";
|
|
4224
|
+
}
|
|
4225
|
+
return createHash("sha256").update(source).digest("hex").slice(0, 16);
|
|
4226
|
+
}
|
|
4227
|
+
async function loadCache(path) {
|
|
4228
|
+
const file = await readJsonFile(path);
|
|
4229
|
+
if (!file || file.version !== CACHE_VERSION || !isRecord(file.entries)) {
|
|
4230
|
+
return { version: CACHE_VERSION, entries: {} };
|
|
4231
|
+
}
|
|
4232
|
+
const entries = {};
|
|
4233
|
+
for (const [userId, rawEntry] of Object.entries(file.entries)) {
|
|
4234
|
+
if (!USER_ID_PATTERN.test(userId) || !isRecord(rawEntry)) {
|
|
4235
|
+
continue;
|
|
4236
|
+
}
|
|
4237
|
+
const fetchedAt = typeof rawEntry.fetched_at === "number" ? rawEntry.fetched_at : undefined;
|
|
4238
|
+
const user = isRecord(rawEntry.user) ? toCompactUser(rawEntry.user) : null;
|
|
4239
|
+
if (!fetchedAt || !user) {
|
|
4240
|
+
continue;
|
|
4241
|
+
}
|
|
4242
|
+
entries[userId] = { fetched_at: fetchedAt, user };
|
|
4243
|
+
}
|
|
4244
|
+
return {
|
|
4245
|
+
version: CACHE_VERSION,
|
|
4246
|
+
entries
|
|
4247
|
+
};
|
|
4248
|
+
}
|
|
4249
|
+
async function writeCache(path, file) {
|
|
4250
|
+
try {
|
|
4251
|
+
await writeJsonFile(path, file);
|
|
4252
|
+
} catch {}
|
|
4253
|
+
}
|
|
4254
|
+
function pruneExpiredEntries(file, now) {
|
|
4255
|
+
const next = {};
|
|
4256
|
+
for (const [userId, entry] of Object.entries(file.entries)) {
|
|
4257
|
+
if (now - entry.fetched_at >= USER_TTL_MS) {
|
|
4258
|
+
continue;
|
|
4259
|
+
}
|
|
4260
|
+
next[userId] = entry;
|
|
4261
|
+
}
|
|
4262
|
+
return { version: CACHE_VERSION, entries: next };
|
|
4263
|
+
}
|
|
4264
|
+
async function fetchUserById(client, userId) {
|
|
4265
|
+
try {
|
|
4266
|
+
const resp = await client.api("users.info", { user: userId });
|
|
4267
|
+
const user = isRecord(resp.user) ? resp.user : null;
|
|
4268
|
+
if (!user) {
|
|
4269
|
+
return;
|
|
4270
|
+
}
|
|
4271
|
+
return toCompactUser(user);
|
|
4272
|
+
} catch {
|
|
4273
|
+
return;
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
function collectUserIdsFromUnknown(value, out) {
|
|
4277
|
+
if (typeof value === "string") {
|
|
4278
|
+
collectMentionUserIds(value, out);
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
if (Array.isArray(value)) {
|
|
4282
|
+
for (const item of value) {
|
|
4283
|
+
collectUserIdsFromUnknown(item, out);
|
|
4284
|
+
}
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
if (!isRecord(value)) {
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
for (const [key, child] of Object.entries(value)) {
|
|
4291
|
+
if ((key === "user" || key === "user_id") && typeof child === "string") {
|
|
4292
|
+
if (USER_ID_PATTERN.test(child)) {
|
|
4293
|
+
out.add(child);
|
|
4294
|
+
}
|
|
4295
|
+
continue;
|
|
4296
|
+
}
|
|
4297
|
+
if (key === "users") {
|
|
4298
|
+
for (const maybeUserId of asArray(child)) {
|
|
4299
|
+
const userId = String(maybeUserId);
|
|
4300
|
+
if (USER_ID_PATTERN.test(userId)) {
|
|
4301
|
+
out.add(userId);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
continue;
|
|
4305
|
+
}
|
|
4306
|
+
collectUserIdsFromUnknown(child, out);
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
function collectUserIdsFromMessage(message, out, options) {
|
|
4310
|
+
if (message.user && USER_ID_PATTERN.test(message.user)) {
|
|
4311
|
+
out.add(message.user);
|
|
4312
|
+
}
|
|
4313
|
+
if (typeof message.text === "string") {
|
|
4314
|
+
collectMentionUserIds(message.text, out);
|
|
4315
|
+
}
|
|
4316
|
+
collectUserIdsFromUnknown(message.blocks, out);
|
|
4317
|
+
collectUserIdsFromUnknown(message.attachments, out);
|
|
4318
|
+
if (options.includeReactions) {
|
|
4319
|
+
collectUserIdsFromUnknown(message.reactions, out);
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
function collectMentionUserIds(text, out) {
|
|
4323
|
+
USER_MENTION_PATTERN.lastIndex = 0;
|
|
4324
|
+
for (;; ) {
|
|
4325
|
+
const match = USER_MENTION_PATTERN.exec(text);
|
|
4326
|
+
if (!match) {
|
|
4327
|
+
break;
|
|
4328
|
+
}
|
|
4329
|
+
const userId = match[1] ?? "";
|
|
4330
|
+
if (USER_ID_PATTERN.test(userId)) {
|
|
4331
|
+
out.add(userId);
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
// src/cli/message-read-actions.ts
|
|
4337
|
+
async function handleMessageGet(input) {
|
|
4338
|
+
const target = parseMsgTarget(input.targetInput);
|
|
4339
|
+
if (target.kind === "user") {
|
|
4340
|
+
throw new Error("message get does not support user ID targets. Use a channel name, channel ID, or message URL.");
|
|
4341
|
+
}
|
|
4342
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
|
|
4343
|
+
return input.ctx.withAutoRefresh({
|
|
4344
|
+
workspaceUrl: target.kind === "url" ? target.ref.workspace_url : workspaceUrl,
|
|
4345
|
+
work: async () => {
|
|
4346
|
+
if (target.kind === "url") {
|
|
4347
|
+
const { ref: ref2 } = target;
|
|
4348
|
+
warnOnTruncatedSlackUrl(ref2);
|
|
4349
|
+
const { client: client2, auth: auth2 } = await input.ctx.getClientForWorkspace(ref2.workspace_url);
|
|
4350
|
+
const includeReactions2 = Boolean(input.options.includeReactions);
|
|
4351
|
+
const msg2 = await fetchMessage(client2, { ref: ref2, includeReactions: includeReactions2 });
|
|
4352
|
+
const thread2 = await getThreadSummary(client2, { channelId: ref2.channel_id, msg: msg2 });
|
|
4353
|
+
const downloadedPaths2 = await downloadMessageFiles({ auth: auth2, messages: [msg2] });
|
|
4354
|
+
const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
|
|
4355
|
+
const referencedUserIds2 = collectReferencedUserIds([msg2], {
|
|
4356
|
+
includeReactions: includeReactions2
|
|
4357
|
+
});
|
|
4358
|
+
const usersById2 = input.options.resolveUsers || input.options.refreshUsers ? await resolveUsersById({
|
|
4359
|
+
client: client2,
|
|
4360
|
+
workspaceUrl: ref2.workspace_url,
|
|
4361
|
+
userIds: referencedUserIds2,
|
|
4362
|
+
forceRefresh: Boolean(input.options.refreshUsers)
|
|
4363
|
+
}) : new Map;
|
|
4364
|
+
const message2 = toCompactMessage(msg2, {
|
|
4365
|
+
maxBodyChars: maxBodyChars2,
|
|
4366
|
+
includeReactions: includeReactions2,
|
|
4367
|
+
downloadedPaths: downloadedPaths2
|
|
4368
|
+
});
|
|
4369
|
+
return pruneEmpty({
|
|
4370
|
+
message: message2,
|
|
4371
|
+
thread: thread2,
|
|
4372
|
+
referenced_users: toReferencedUsers(referencedUserIds2, usersById2)
|
|
4373
|
+
});
|
|
4374
|
+
}
|
|
4375
|
+
const ts = input.options.ts?.trim();
|
|
4376
|
+
if (!ts) {
|
|
4377
|
+
throw new Error('When targeting a channel, you must pass --ts "<seconds>.<micros>"');
|
|
4378
|
+
}
|
|
4379
|
+
await input.ctx.assertWorkspaceSpecifiedForChannelNames({
|
|
4380
|
+
workspaceUrl,
|
|
4381
|
+
channels: [target.channel]
|
|
4382
|
+
});
|
|
4383
|
+
const includeReactions = Boolean(input.options.includeReactions);
|
|
4384
|
+
const { client, auth, workspace_url } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
4385
|
+
const channelId = await resolveChannelId(client, target.channel);
|
|
4386
|
+
const ref = {
|
|
4387
|
+
workspace_url: workspace_url ?? workspaceUrl ?? "",
|
|
4388
|
+
channel_id: channelId,
|
|
4389
|
+
message_ts: ts,
|
|
4390
|
+
thread_ts_hint: input.options.threadTs?.trim() || undefined,
|
|
4391
|
+
raw: input.targetInput
|
|
4392
|
+
};
|
|
4393
|
+
const msg = await fetchMessage(client, { ref, includeReactions });
|
|
4394
|
+
const thread = await getThreadSummary(client, { channelId, msg });
|
|
4395
|
+
const downloadedPaths = await downloadMessageFiles({ auth, messages: [msg] });
|
|
4396
|
+
const maxBodyChars = Number.parseInt(input.options.maxBodyChars, 10);
|
|
4397
|
+
const referencedUserIds = collectReferencedUserIds([msg], {
|
|
4398
|
+
includeReactions
|
|
4399
|
+
});
|
|
4400
|
+
const usersById = input.options.resolveUsers || input.options.refreshUsers ? await resolveUsersById({
|
|
4401
|
+
client,
|
|
4402
|
+
workspaceUrl: ref.workspace_url,
|
|
4403
|
+
userIds: referencedUserIds,
|
|
4404
|
+
forceRefresh: Boolean(input.options.refreshUsers)
|
|
4405
|
+
}) : new Map;
|
|
4406
|
+
const message = toCompactMessage(msg, {
|
|
4407
|
+
maxBodyChars,
|
|
4408
|
+
includeReactions,
|
|
4409
|
+
downloadedPaths
|
|
4410
|
+
});
|
|
4411
|
+
return pruneEmpty({
|
|
4412
|
+
message,
|
|
4413
|
+
thread,
|
|
4414
|
+
referenced_users: toReferencedUsers(referencedUserIds, usersById)
|
|
4415
|
+
});
|
|
4416
|
+
}
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
4419
|
+
async function handleMessageList(input) {
|
|
4420
|
+
const target = parseMsgTarget(input.targetInput);
|
|
4421
|
+
if (target.kind === "user") {
|
|
4422
|
+
throw new Error("message list does not support user ID targets. Use a channel name, channel ID, or message URL.");
|
|
4423
|
+
}
|
|
4424
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
|
|
4425
|
+
return input.ctx.withAutoRefresh({
|
|
4426
|
+
workspaceUrl: target.kind === "url" ? target.ref.workspace_url : workspaceUrl,
|
|
4427
|
+
work: async () => {
|
|
4428
|
+
const withReactions = parseReactionFilters(input.options.withReaction);
|
|
4429
|
+
const withoutReactions = parseReactionFilters(input.options.withoutReaction);
|
|
4430
|
+
const hasReactionFilters = withReactions.length > 0 || withoutReactions.length > 0;
|
|
4431
|
+
if (target.kind === "url") {
|
|
4432
|
+
if (hasReactionFilters) {
|
|
4433
|
+
throw new Error("Reaction filters are only supported for channel history mode (not message URL thread mode)");
|
|
4434
|
+
}
|
|
4435
|
+
const { ref } = target;
|
|
4436
|
+
warnOnTruncatedSlackUrl(ref);
|
|
4437
|
+
const { client: client2, auth: auth2 } = await input.ctx.getClientForWorkspace(ref.workspace_url);
|
|
4438
|
+
const includeReactions2 = Boolean(input.options.includeReactions);
|
|
4439
|
+
const msg = await fetchMessage(client2, { ref, includeReactions: includeReactions2 });
|
|
4440
|
+
const rootTs2 = msg.thread_ts ?? msg.ts;
|
|
4441
|
+
const threadMessages2 = await fetchThread(client2, {
|
|
4442
|
+
channelId: ref.channel_id,
|
|
4443
|
+
threadTs: rootTs2,
|
|
4444
|
+
includeReactions: includeReactions2
|
|
4445
|
+
});
|
|
4446
|
+
const downloadedPaths2 = await downloadMessageFiles({ auth: auth2, messages: threadMessages2 });
|
|
4447
|
+
const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
|
|
4448
|
+
const referencedUserIds2 = collectReferencedUserIds(threadMessages2, {
|
|
4449
|
+
includeReactions: includeReactions2
|
|
4450
|
+
});
|
|
4451
|
+
const usersById2 = input.options.resolveUsers || input.options.refreshUsers ? await resolveUsersById({
|
|
4452
|
+
client: client2,
|
|
4453
|
+
workspaceUrl: ref.workspace_url,
|
|
4454
|
+
userIds: referencedUserIds2,
|
|
4455
|
+
forceRefresh: Boolean(input.options.refreshUsers)
|
|
4456
|
+
}) : new Map;
|
|
4457
|
+
return pruneEmpty({
|
|
4458
|
+
messages: threadMessages2.map((m) => toCompactMessage(m, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 })).map(toThreadListMessage),
|
|
4459
|
+
referenced_users: toReferencedUsers(referencedUserIds2, usersById2)
|
|
4460
|
+
});
|
|
4461
|
+
}
|
|
4462
|
+
const { client, auth, workspace_url } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
4463
|
+
await input.ctx.assertWorkspaceSpecifiedForChannelNames({
|
|
4464
|
+
workspaceUrl,
|
|
4465
|
+
channels: [target.channel]
|
|
4466
|
+
});
|
|
4467
|
+
const channelId = await resolveChannelId(client, target.channel);
|
|
4468
|
+
const threadTs = input.options.threadTs?.trim();
|
|
4469
|
+
const ts = input.options.ts?.trim();
|
|
4470
|
+
if (!threadTs && !ts) {
|
|
4471
|
+
const includeReactions2 = Boolean(input.options.includeReactions);
|
|
4472
|
+
const limit = parseLimit(input.options.limit);
|
|
4473
|
+
const oldest = requireOldestWhenReactionFiltersUsed({
|
|
4474
|
+
oldest: input.options.oldest,
|
|
4475
|
+
withReactions,
|
|
4476
|
+
withoutReactions
|
|
4477
|
+
});
|
|
4478
|
+
const channelMessages = await fetchChannelHistory(client, {
|
|
4479
|
+
channelId,
|
|
4480
|
+
limit,
|
|
4481
|
+
latest: input.options.latest?.trim(),
|
|
4482
|
+
oldest,
|
|
4483
|
+
includeReactions: includeReactions2 || hasReactionFilters,
|
|
4484
|
+
withReactions,
|
|
4485
|
+
withoutReactions
|
|
4486
|
+
});
|
|
4487
|
+
const downloadedPaths2 = await downloadMessageFiles({ auth, messages: channelMessages });
|
|
4488
|
+
const maxBodyChars2 = Number.parseInt(input.options.maxBodyChars, 10);
|
|
4489
|
+
const referencedUserIds2 = collectReferencedUserIds(channelMessages, {
|
|
4490
|
+
includeReactions: includeReactions2
|
|
4491
|
+
});
|
|
4492
|
+
const usersById2 = input.options.resolveUsers || input.options.refreshUsers ? await resolveUsersById({
|
|
4493
|
+
client,
|
|
4494
|
+
workspaceUrl: workspace_url ?? workspaceUrl ?? "",
|
|
4495
|
+
userIds: referencedUserIds2,
|
|
4496
|
+
forceRefresh: Boolean(input.options.refreshUsers)
|
|
4497
|
+
}) : new Map;
|
|
4498
|
+
return pruneEmpty({
|
|
4499
|
+
channel_id: channelId,
|
|
4500
|
+
messages: channelMessages.map((m) => toCompactMessage(m, { maxBodyChars: maxBodyChars2, includeReactions: includeReactions2, downloadedPaths: downloadedPaths2 })),
|
|
4501
|
+
referenced_users: toReferencedUsers(referencedUserIds2, usersById2)
|
|
4502
|
+
});
|
|
4503
|
+
}
|
|
4504
|
+
if (hasReactionFilters) {
|
|
4505
|
+
throw new Error("Reaction filters are only supported for channel history mode (without --thread-ts/--ts)");
|
|
4506
|
+
}
|
|
4507
|
+
const rootTs = threadTs ?? await (async () => {
|
|
4508
|
+
const ref = {
|
|
4509
|
+
workspace_url: workspace_url ?? workspaceUrl ?? "",
|
|
4510
|
+
channel_id: channelId,
|
|
4511
|
+
message_ts: ts,
|
|
4512
|
+
raw: input.targetInput
|
|
4513
|
+
};
|
|
4514
|
+
const includeReactions2 = Boolean(input.options.includeReactions);
|
|
4515
|
+
const msg = await fetchMessage(client, { ref, includeReactions: includeReactions2 });
|
|
4516
|
+
return msg.thread_ts ?? msg.ts;
|
|
4517
|
+
})();
|
|
4518
|
+
const includeReactions = Boolean(input.options.includeReactions);
|
|
4519
|
+
const threadMessages = await fetchThread(client, {
|
|
4520
|
+
channelId,
|
|
4521
|
+
threadTs: rootTs,
|
|
4522
|
+
includeReactions
|
|
4523
|
+
});
|
|
4524
|
+
const downloadedPaths = await downloadMessageFiles({ auth, messages: threadMessages });
|
|
4525
|
+
const maxBodyChars = Number.parseInt(input.options.maxBodyChars, 10);
|
|
4526
|
+
const referencedUserIds = collectReferencedUserIds(threadMessages, {
|
|
4527
|
+
includeReactions
|
|
4528
|
+
});
|
|
4529
|
+
const usersById = input.options.resolveUsers || input.options.refreshUsers ? await resolveUsersById({
|
|
4530
|
+
client,
|
|
4531
|
+
workspaceUrl: workspace_url ?? workspaceUrl ?? "",
|
|
4532
|
+
userIds: referencedUserIds,
|
|
4533
|
+
forceRefresh: Boolean(input.options.refreshUsers)
|
|
4534
|
+
}) : new Map;
|
|
4535
|
+
return pruneEmpty({
|
|
4536
|
+
messages: threadMessages.map((m) => toCompactMessage(m, { maxBodyChars, includeReactions, downloadedPaths })).map(toThreadListMessage),
|
|
4537
|
+
referenced_users: toReferencedUsers(referencedUserIds, usersById)
|
|
4538
|
+
});
|
|
4539
|
+
}
|
|
4540
|
+
});
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
// src/cli/message-actions.ts
|
|
4544
|
+
function parseLimit(raw) {
|
|
4545
|
+
if (raw === undefined) {
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
4548
|
+
const n = Number.parseInt(raw, 10);
|
|
4549
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
4550
|
+
throw new Error(`Invalid --limit value "${raw}": must be a positive integer`);
|
|
4551
|
+
}
|
|
4552
|
+
return n;
|
|
4553
|
+
}
|
|
4554
|
+
function requireMessageTs(raw) {
|
|
4555
|
+
const ts = raw?.trim();
|
|
4556
|
+
if (!ts) {
|
|
4557
|
+
throw new Error('When targeting a channel, you must pass --ts "<seconds>.<micros>"');
|
|
4558
|
+
}
|
|
4559
|
+
return ts;
|
|
4560
|
+
}
|
|
4561
|
+
function parseReactionFilters(raw) {
|
|
4562
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
4563
|
+
return [];
|
|
4564
|
+
}
|
|
4565
|
+
const out = [];
|
|
4566
|
+
for (const value of raw) {
|
|
4567
|
+
const normalized = normalizeSlackReactionName(String(value));
|
|
4568
|
+
if (!out.includes(normalized)) {
|
|
4569
|
+
out.push(normalized);
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
return out;
|
|
4573
|
+
}
|
|
4574
|
+
function requireOldestWhenReactionFiltersUsed(input) {
|
|
4575
|
+
const hasReactionFilters = input.withReactions.length > 0 || input.withoutReactions.length > 0;
|
|
4576
|
+
const oldest = input.oldest?.trim();
|
|
4577
|
+
if (!hasReactionFilters) {
|
|
4578
|
+
return oldest;
|
|
4579
|
+
}
|
|
4580
|
+
if (!oldest) {
|
|
4581
|
+
throw new Error('Reaction filters require --oldest "<seconds>.<micros>" to bound scan size. Example: --oldest "1770165109.628379"');
|
|
4582
|
+
}
|
|
4583
|
+
return oldest;
|
|
4584
|
+
}
|
|
4080
4585
|
async function sendMessage(input) {
|
|
4081
4586
|
const target = parseMsgTarget(String(input.targetInput));
|
|
4082
4587
|
const blocks = input.text ? textToRichTextBlocks(input.text) : null;
|
|
@@ -6322,7 +6827,7 @@ function collectOptionValue(value, previous = []) {
|
|
|
6322
6827
|
}
|
|
6323
6828
|
function registerMessageCommand(input) {
|
|
6324
6829
|
const messageCmd = input.program.command("message").description("Read/write Slack messages (token-efficient JSON)");
|
|
6325
|
-
messageCmd.command("get", { isDefault: true }).description("Fetch a single Slack message (with thread summary if any)").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").option("--thread-ts <ts>", "Thread root ts hint (useful for thread permalinks)").option("--max-body-chars <n>", "Max content characters to include (default 8000, -1 for unlimited)", "8000").option("--include-reactions", "Include reactions + reacting users").action(async (...args) => {
|
|
6830
|
+
messageCmd.command("get", { isDefault: true }).description("Fetch a single Slack message (with thread summary if any)").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--ts <ts>", "Message ts (required when using #channel/channel id)").option("--thread-ts <ts>", "Thread root ts hint (useful for thread permalinks)").option("--max-body-chars <n>", "Max content characters to include (default 8000, -1 for unlimited)", "8000").option("--include-reactions", "Include reactions + reacting users").option("--resolve-users", "Resolve user IDs to user profiles").option("--refresh-users", "Refresh user profile cache before resolving user IDs (implies --resolve-users)").action(async (...args) => {
|
|
6326
6831
|
const [targetInput, options] = args;
|
|
6327
6832
|
try {
|
|
6328
6833
|
const payload = await handleMessageGet({ ctx: input.ctx, targetInput, options });
|
|
@@ -6332,7 +6837,7 @@ function registerMessageCommand(input) {
|
|
|
6332
6837
|
process.exitCode = 1;
|
|
6333
6838
|
}
|
|
6334
6839
|
});
|
|
6335
|
-
messageCmd.command("list").description("List recent channel messages, or fetch a full thread").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--thread-ts <ts>", "Thread root ts (lists thread replies instead of channel history)").option("--ts <ts>", "Message ts (resolve message to its thread)").option("--limit <n>", "Max messages to return for channel history (default 25, max 200)").option("--oldest <ts>", "Only messages after this ts (channel history mode)").option("--latest <ts>", "Only messages before this ts (channel history mode)").option("--with-reaction <emoji>", "Only include messages with this reaction (repeatable; channel history mode; requires --oldest)", collectOptionValue, []).option("--without-reaction <emoji>", "Only include messages without this reaction (repeatable; channel history mode; requires --oldest)", collectOptionValue, []).option("--max-body-chars <n>", "Max content characters to include (default 8000, -1 for unlimited)", "8000").option("--include-reactions", "Include reactions + reacting users").action(async (...args) => {
|
|
6840
|
+
messageCmd.command("list").description("List recent channel messages, or fetch a full thread").argument("<target>", "Slack message URL, #channel, or channel ID").option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when using #channel/channel id across multiple workspaces)").option("--thread-ts <ts>", "Thread root ts (lists thread replies instead of channel history)").option("--ts <ts>", "Message ts (resolve message to its thread)").option("--limit <n>", "Max messages to return for channel history (default 25, max 200)").option("--oldest <ts>", "Only messages after this ts (channel history mode)").option("--latest <ts>", "Only messages before this ts (channel history mode)").option("--with-reaction <emoji>", "Only include messages with this reaction (repeatable; channel history mode; requires --oldest)", collectOptionValue, []).option("--without-reaction <emoji>", "Only include messages without this reaction (repeatable; channel history mode; requires --oldest)", collectOptionValue, []).option("--max-body-chars <n>", "Max content characters to include (default 8000, -1 for unlimited)", "8000").option("--include-reactions", "Include reactions + reacting users").option("--resolve-users", "Resolve user IDs to user profiles").option("--refresh-users", "Refresh user profile cache before resolving user IDs (implies --resolve-users)").action(async (...args) => {
|
|
6336
6841
|
const [targetInput, options] = args;
|
|
6337
6842
|
try {
|
|
6338
6843
|
const payload = await handleMessageList({ ctx: input.ctx, targetInput, options });
|
|
@@ -6524,7 +7029,7 @@ async function channelTokenForSearch(client, channel) {
|
|
|
6524
7029
|
} catch {}
|
|
6525
7030
|
return null;
|
|
6526
7031
|
}
|
|
6527
|
-
async function
|
|
7032
|
+
async function resolveUserId2(client, input) {
|
|
6528
7033
|
const trimmed = input.trim();
|
|
6529
7034
|
if (!trimmed) {
|
|
6530
7035
|
return;
|
|
@@ -6712,7 +7217,7 @@ async function searchFilesViaSearchApi(client, input) {
|
|
|
6712
7217
|
}
|
|
6713
7218
|
async function searchFilesInChannelsFallback(client, input) {
|
|
6714
7219
|
const channelIds = await Promise.all(input.channels.map((c) => resolveChannelId(client, c)));
|
|
6715
|
-
const userId = input.user ? await
|
|
7220
|
+
const userId = input.user ? await resolveUserId2(client, input.user) : undefined;
|
|
6716
7221
|
const queryLower = input.query.trim().toLowerCase();
|
|
6717
7222
|
const ts_from = input.after ? dateToUnixSeconds(input.after, "start") : undefined;
|
|
6718
7223
|
const ts_to = input.before ? dateToUnixSeconds(input.before, "end") : undefined;
|
|
@@ -6810,7 +7315,7 @@ function passesFileContentTypeFilter(f, contentType) {
|
|
|
6810
7315
|
async function searchMessagesViaSearchApi(client, input) {
|
|
6811
7316
|
const matches = input.rawMatches;
|
|
6812
7317
|
if (matches.length === 0) {
|
|
6813
|
-
return [];
|
|
7318
|
+
return { messages: [] };
|
|
6814
7319
|
}
|
|
6815
7320
|
const messageRefs = [];
|
|
6816
7321
|
for (const m of matches) {
|
|
@@ -6834,6 +7339,7 @@ async function searchMessagesViaSearchApi(client, input) {
|
|
|
6834
7339
|
}
|
|
6835
7340
|
const downloadedPaths = {};
|
|
6836
7341
|
const downloadsDir = input.download ? await ensureDownloadsDir() : null;
|
|
7342
|
+
const resolvedMessages = [];
|
|
6837
7343
|
const out = [];
|
|
6838
7344
|
for (const ref of messageRefs) {
|
|
6839
7345
|
let full = null;
|
|
@@ -6872,21 +7378,36 @@ async function searchMessagesViaSearchApi(client, input) {
|
|
|
6872
7378
|
if (!passesContentTypeFilter(compact, input.contentType)) {
|
|
6873
7379
|
continue;
|
|
6874
7380
|
}
|
|
6875
|
-
|
|
7381
|
+
resolvedMessages.push(full);
|
|
7382
|
+
out.push(toSearchCompactMessage(compact, ref.permalink));
|
|
6876
7383
|
if (out.length >= input.limit) {
|
|
6877
7384
|
break;
|
|
6878
7385
|
}
|
|
6879
7386
|
}
|
|
6880
|
-
|
|
7387
|
+
const referencedUserIds = collectReferencedUserIds(resolvedMessages, {
|
|
7388
|
+
includeReactions: false
|
|
7389
|
+
});
|
|
7390
|
+
const shouldResolveUsers = input.resolveUsers || input.refreshUsers;
|
|
7391
|
+
const usersById = shouldResolveUsers ? await resolveUsersById({
|
|
7392
|
+
client,
|
|
7393
|
+
workspaceUrl: input.workspace_url ?? "",
|
|
7394
|
+
userIds: referencedUserIds,
|
|
7395
|
+
forceRefresh: Boolean(input.refreshUsers)
|
|
7396
|
+
}) : new Map;
|
|
7397
|
+
return {
|
|
7398
|
+
messages: out,
|
|
7399
|
+
referenced_users: toReferencedUsers(referencedUserIds, usersById)
|
|
7400
|
+
};
|
|
6881
7401
|
}
|
|
6882
7402
|
async function searchMessagesInChannelsFallback(client, input) {
|
|
6883
7403
|
const channelIds = await Promise.all(input.channels.map((c) => resolveChannelId(client, c)));
|
|
6884
7404
|
const queryLower = input.query.trim().toLowerCase();
|
|
6885
|
-
const userId = input.user ? await
|
|
7405
|
+
const userId = input.user ? await resolveUserId2(client, input.user) : undefined;
|
|
6886
7406
|
const afterSec = input.after ? dateToUnixSeconds(input.after, "start") : null;
|
|
6887
7407
|
const beforeSec = input.before ? dateToUnixSeconds(input.before, "end") : null;
|
|
6888
7408
|
const downloadsDir = input.download ? await ensureDownloadsDir() : null;
|
|
6889
7409
|
const downloadedPaths = {};
|
|
7410
|
+
const matchedSummaries = [];
|
|
6890
7411
|
const results = [];
|
|
6891
7412
|
for (const channelId of channelIds) {
|
|
6892
7413
|
let cursorLatest;
|
|
@@ -6934,9 +7455,22 @@ async function searchMessagesInChannelsFallback(client, input) {
|
|
|
6934
7455
|
if (!passesContentTypeFilter(compact, input.contentType)) {
|
|
6935
7456
|
continue;
|
|
6936
7457
|
}
|
|
6937
|
-
|
|
7458
|
+
matchedSummaries.push(summary);
|
|
7459
|
+
results.push(toSearchCompactMessage(compact));
|
|
6938
7460
|
if (results.length >= input.limit) {
|
|
6939
|
-
|
|
7461
|
+
const referencedUserIds2 = collectReferencedUserIds(matchedSummaries, {
|
|
7462
|
+
includeReactions: false
|
|
7463
|
+
});
|
|
7464
|
+
const usersById2 = await resolveUsersById({
|
|
7465
|
+
client,
|
|
7466
|
+
workspaceUrl: input.workspace_url ?? "",
|
|
7467
|
+
userIds: referencedUserIds2,
|
|
7468
|
+
forceRefresh: Boolean(input.refreshUsers)
|
|
7469
|
+
});
|
|
7470
|
+
return {
|
|
7471
|
+
messages: results,
|
|
7472
|
+
referenced_users: toReferencedUsers(referencedUserIds2, usersById2)
|
|
7473
|
+
};
|
|
6940
7474
|
}
|
|
6941
7475
|
}
|
|
6942
7476
|
if (!cursorLatest) {
|
|
@@ -6949,7 +7483,20 @@ async function searchMessagesInChannelsFallback(client, input) {
|
|
|
6949
7483
|
}
|
|
6950
7484
|
}
|
|
6951
7485
|
}
|
|
6952
|
-
|
|
7486
|
+
const referencedUserIds = collectReferencedUserIds(matchedSummaries, {
|
|
7487
|
+
includeReactions: false
|
|
7488
|
+
});
|
|
7489
|
+
const shouldResolveUsers = input.resolveUsers || input.refreshUsers;
|
|
7490
|
+
const usersById = shouldResolveUsers ? await resolveUsersById({
|
|
7491
|
+
client,
|
|
7492
|
+
workspaceUrl: input.workspace_url ?? "",
|
|
7493
|
+
userIds: referencedUserIds,
|
|
7494
|
+
forceRefresh: Boolean(input.refreshUsers)
|
|
7495
|
+
}) : new Map;
|
|
7496
|
+
return {
|
|
7497
|
+
messages: results,
|
|
7498
|
+
referenced_users: toReferencedUsers(referencedUserIds, usersById)
|
|
7499
|
+
};
|
|
6953
7500
|
}
|
|
6954
7501
|
function passesContentTypeFilter(m, contentType) {
|
|
6955
7502
|
if (contentType === "any") {
|
|
@@ -6973,9 +7520,9 @@ function passesContentTypeFilter(m, contentType) {
|
|
|
6973
7520
|
}
|
|
6974
7521
|
return true;
|
|
6975
7522
|
}
|
|
6976
|
-
function
|
|
6977
|
-
const {
|
|
6978
|
-
return rest;
|
|
7523
|
+
function toSearchCompactMessage(m, permalink) {
|
|
7524
|
+
const { thread_ts: _threadTs, ...rest } = m;
|
|
7525
|
+
return permalink ? { ...rest, permalink } : rest;
|
|
6979
7526
|
}
|
|
6980
7527
|
async function downloadFilesForMessage(input) {
|
|
6981
7528
|
for (const f of input.message.files ?? []) {
|
|
@@ -7067,8 +7614,9 @@ async function searchSlack(input) {
|
|
|
7067
7614
|
const out = {};
|
|
7068
7615
|
if (input.options.kind === "messages" || input.options.kind === "all") {
|
|
7069
7616
|
if (input.options.channels?.length) {
|
|
7070
|
-
|
|
7617
|
+
const messageResult = await searchMessagesInChannelsFallback(input.client, {
|
|
7071
7618
|
auth: input.auth,
|
|
7619
|
+
workspace_url: input.options.workspace_url,
|
|
7072
7620
|
query: input.options.query,
|
|
7073
7621
|
channels: input.options.channels,
|
|
7074
7622
|
user: input.options.user,
|
|
@@ -7077,11 +7625,15 @@ async function searchSlack(input) {
|
|
|
7077
7625
|
limit,
|
|
7078
7626
|
maxContentChars,
|
|
7079
7627
|
contentType,
|
|
7080
|
-
download
|
|
7628
|
+
download,
|
|
7629
|
+
resolveUsers: input.options.resolve_users,
|
|
7630
|
+
refreshUsers: input.options.refresh_users
|
|
7081
7631
|
});
|
|
7632
|
+
out.messages = messageResult.messages;
|
|
7633
|
+
out.referenced_users = messageResult.referenced_users;
|
|
7082
7634
|
} else {
|
|
7083
7635
|
const rawMatches = await searchMessagesRaw(input.client, { query: slackQuery, limit });
|
|
7084
|
-
|
|
7636
|
+
const messageResult = await searchMessagesViaSearchApi(input.client, {
|
|
7085
7637
|
auth: input.auth,
|
|
7086
7638
|
workspace_url: input.options.workspace_url,
|
|
7087
7639
|
slack_query: slackQuery,
|
|
@@ -7089,8 +7641,12 @@ async function searchSlack(input) {
|
|
|
7089
7641
|
maxContentChars,
|
|
7090
7642
|
contentType,
|
|
7091
7643
|
download,
|
|
7092
|
-
rawMatches
|
|
7644
|
+
rawMatches,
|
|
7645
|
+
resolveUsers: input.options.resolve_users,
|
|
7646
|
+
refreshUsers: input.options.refresh_users
|
|
7093
7647
|
});
|
|
7648
|
+
out.messages = messageResult.messages;
|
|
7649
|
+
out.referenced_users = messageResult.referenced_users;
|
|
7094
7650
|
}
|
|
7095
7651
|
}
|
|
7096
7652
|
if (input.options.kind === "files" || input.options.kind === "all") {
|
|
@@ -7121,7 +7677,7 @@ async function searchSlack(input) {
|
|
|
7121
7677
|
|
|
7122
7678
|
// src/cli/search-command.ts
|
|
7123
7679
|
function addSearchOptions(cmd) {
|
|
7124
|
-
return cmd.option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when searching across multiple workspaces)").option("--channel <channel...>", "Channel filter (#name, name, or id). Repeatable.").option("--user <user>", "User filter (@name, name, or user id U...)").option("--after <date>", "Only results after YYYY-MM-DD").option("--before <date>", "Only results before YYYY-MM-DD").option("--content-type <type>", "Filter content type: any|text|image|snippet|file (default any)").option("--limit <n>", "Max results (default 20)", "20").option("--max-content-chars <n>", "Max message content characters (default 4000, -1 for unlimited)", "4000");
|
|
7680
|
+
return cmd.option("--workspace <url>", "Workspace selector (full URL or unique substring; needed when searching across multiple workspaces)").option("--channel <channel...>", "Channel filter (#name, name, or id). Repeatable.").option("--user <user>", "User filter (@name, name, or user id U...)").option("--after <date>", "Only results after YYYY-MM-DD").option("--before <date>", "Only results before YYYY-MM-DD").option("--content-type <type>", "Filter content type: any|text|image|snippet|file (default any)").option("--limit <n>", "Max results (default 20)", "20").option("--max-content-chars <n>", "Max message content characters (default 4000, -1 for unlimited)", "4000").option("--resolve-users", "Resolve user IDs to user profiles").option("--refresh-users", "Refresh user profile cache before resolving referenced users (implies --resolve-users)");
|
|
7125
7681
|
}
|
|
7126
7682
|
async function runSearch(input) {
|
|
7127
7683
|
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(input.options.workspace);
|
|
@@ -7148,39 +7704,604 @@ async function runSearch(input) {
|
|
|
7148
7704
|
content_type: contentType,
|
|
7149
7705
|
limit,
|
|
7150
7706
|
max_content_chars: maxContentChars,
|
|
7151
|
-
download: true
|
|
7707
|
+
download: true,
|
|
7708
|
+
resolve_users: Boolean(input.options.resolveUsers || input.options.refreshUsers),
|
|
7709
|
+
refresh_users: Boolean(input.options.refreshUsers)
|
|
7710
|
+
}
|
|
7711
|
+
});
|
|
7712
|
+
}
|
|
7713
|
+
});
|
|
7714
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
7715
|
+
}
|
|
7716
|
+
function registerSearchCommand(input) {
|
|
7717
|
+
const searchCmd = input.program.command("search").description("Search Slack messages and files (token-efficient JSON)");
|
|
7718
|
+
const create = (spec) => addSearchOptions(searchCmd.command(spec.name).description(spec.desc)).argument("<query>", "Search query").action(async (...args) => {
|
|
7719
|
+
const [query, options] = args;
|
|
7720
|
+
try {
|
|
7721
|
+
await runSearch({ ctx: input.ctx, kind: spec.kind, query, options });
|
|
7722
|
+
} catch (err) {
|
|
7723
|
+
console.error(input.ctx.errorMessage(err));
|
|
7724
|
+
process.exitCode = 1;
|
|
7725
|
+
}
|
|
7726
|
+
});
|
|
7727
|
+
create({ kind: "all", name: "all", desc: "Search messages and files" });
|
|
7728
|
+
create({ kind: "messages", name: "messages", desc: "Search messages" });
|
|
7729
|
+
create({ kind: "files", name: "files", desc: "Search files" });
|
|
7730
|
+
}
|
|
7731
|
+
|
|
7732
|
+
// src/slack/later.ts
|
|
7733
|
+
async function fetchLaterItems(client, options) {
|
|
7734
|
+
const stateFilter = options?.state ?? "in_progress";
|
|
7735
|
+
const limit = options?.limit ?? 20;
|
|
7736
|
+
const maxBodyChars = options?.maxBodyChars ?? 4000;
|
|
7737
|
+
const countsOnly = options?.countsOnly ?? false;
|
|
7738
|
+
let currentCursor = options?.cursor;
|
|
7739
|
+
let allRawItems = [];
|
|
7740
|
+
let counts = {};
|
|
7741
|
+
let nextCursor;
|
|
7742
|
+
while (true) {
|
|
7743
|
+
const resp = await client.api("saved.list", {
|
|
7744
|
+
limit: 50,
|
|
7745
|
+
cursor: currentCursor
|
|
7746
|
+
});
|
|
7747
|
+
if (!currentCursor) {
|
|
7748
|
+
counts = isRecord5(resp.counts) ? resp.counts : {};
|
|
7749
|
+
}
|
|
7750
|
+
const pageRawItems = asArray2(resp.saved_items).filter(isRecord5);
|
|
7751
|
+
allRawItems.push(...pageRawItems);
|
|
7752
|
+
const meta = isRecord5(resp.response_metadata) ? resp.response_metadata : null;
|
|
7753
|
+
nextCursor = meta ? getString5(meta.next_cursor) : undefined;
|
|
7754
|
+
if (countsOnly) {
|
|
7755
|
+
break;
|
|
7756
|
+
}
|
|
7757
|
+
let filtered2 = allRawItems.filter((item) => getString5(item.item_type) === "message");
|
|
7758
|
+
if (stateFilter !== "all") {
|
|
7759
|
+
filtered2 = filtered2.filter((item) => getString5(item.state) === stateFilter);
|
|
7760
|
+
}
|
|
7761
|
+
if (filtered2.length >= limit || !nextCursor) {
|
|
7762
|
+
break;
|
|
7763
|
+
}
|
|
7764
|
+
currentCursor = nextCursor;
|
|
7765
|
+
}
|
|
7766
|
+
const result = {
|
|
7767
|
+
counts: {
|
|
7768
|
+
in_progress: getNumber3(counts.uncompleted_count) ?? 0,
|
|
7769
|
+
archived: getNumber3(counts.archived_count) ?? 0,
|
|
7770
|
+
completed: getNumber3(counts.completed_count) ?? 0,
|
|
7771
|
+
total: getNumber3(counts.total_count) ?? 0
|
|
7772
|
+
},
|
|
7773
|
+
items: [],
|
|
7774
|
+
next_cursor: nextCursor
|
|
7775
|
+
};
|
|
7776
|
+
if (countsOnly) {
|
|
7777
|
+
return result;
|
|
7778
|
+
}
|
|
7779
|
+
let filtered = allRawItems.filter((item) => getString5(item.item_type) === "message");
|
|
7780
|
+
if (stateFilter !== "all") {
|
|
7781
|
+
filtered = filtered.filter((item) => getString5(item.state) === stateFilter);
|
|
7782
|
+
}
|
|
7783
|
+
filtered = filtered.slice(0, limit);
|
|
7784
|
+
result.items = await Promise.all(filtered.map(async (item) => {
|
|
7785
|
+
const channelId = getString5(item.item_id) ?? "";
|
|
7786
|
+
const ts = getString5(item.ts) ?? "";
|
|
7787
|
+
const state = getString5(item.state) ?? "in_progress";
|
|
7788
|
+
const dateSaved = getNumber3(item.date_created) ?? 0;
|
|
7789
|
+
const dateCompleted = getNumber3(item.date_completed);
|
|
7790
|
+
let channelName;
|
|
7791
|
+
let message;
|
|
7792
|
+
try {
|
|
7793
|
+
const info = await client.api("conversations.info", {
|
|
7794
|
+
channel: channelId
|
|
7795
|
+
});
|
|
7796
|
+
const ch = isRecord5(info.channel) ? info.channel : null;
|
|
7797
|
+
if (ch) {
|
|
7798
|
+
channelName = getString5(ch.name) ?? getString5(ch.name_normalized) ?? undefined;
|
|
7799
|
+
if (ch.is_im && !channelName) {
|
|
7800
|
+
const userId = getString5(ch.user);
|
|
7801
|
+
if (userId) {
|
|
7802
|
+
try {
|
|
7803
|
+
const userInfo = await client.api("users.info", { user: userId });
|
|
7804
|
+
const u = isRecord5(userInfo.user) ? userInfo.user : null;
|
|
7805
|
+
const profile = u && isRecord5(u.profile) ? u.profile : null;
|
|
7806
|
+
channelName = getString5(profile?.display_name) || getString5(u?.real_name) || getString5(u?.name) || undefined;
|
|
7807
|
+
} catch {}
|
|
7808
|
+
}
|
|
7809
|
+
}
|
|
7810
|
+
}
|
|
7811
|
+
} catch {}
|
|
7812
|
+
if (ts) {
|
|
7813
|
+
try {
|
|
7814
|
+
const history = await client.api("conversations.history", {
|
|
7815
|
+
channel: channelId,
|
|
7816
|
+
latest: ts,
|
|
7817
|
+
inclusive: true,
|
|
7818
|
+
limit: 1
|
|
7819
|
+
});
|
|
7820
|
+
const msgs = asArray2(history.messages).filter(isRecord5);
|
|
7821
|
+
const msg = msgs.find((m) => getString5(m.ts) === ts);
|
|
7822
|
+
if (msg) {
|
|
7823
|
+
const rendered = renderSlackMessageContent(msg);
|
|
7824
|
+
const content = maxBodyChars >= 0 && rendered.length > maxBodyChars ? `${rendered.slice(0, maxBodyChars)}
|
|
7825
|
+
…` : rendered;
|
|
7826
|
+
message = {
|
|
7827
|
+
author: getString5(msg.user) || getString5(msg.bot_id) ? {
|
|
7828
|
+
user_id: getString5(msg.user) ?? undefined,
|
|
7829
|
+
bot_id: getString5(msg.bot_id) ?? undefined
|
|
7830
|
+
} : undefined,
|
|
7831
|
+
content: content || undefined,
|
|
7832
|
+
thread_ts: getString5(msg.thread_ts) ?? undefined,
|
|
7833
|
+
reply_count: getNumber3(msg.reply_count) ?? undefined
|
|
7834
|
+
};
|
|
7835
|
+
}
|
|
7836
|
+
} catch {}
|
|
7837
|
+
}
|
|
7838
|
+
return {
|
|
7839
|
+
channel_id: channelId,
|
|
7840
|
+
channel_name: channelName,
|
|
7841
|
+
ts,
|
|
7842
|
+
state,
|
|
7843
|
+
date_saved: dateSaved,
|
|
7844
|
+
date_completed: dateCompleted && dateCompleted > 0 ? dateCompleted : undefined,
|
|
7845
|
+
message
|
|
7846
|
+
};
|
|
7847
|
+
}));
|
|
7848
|
+
return result;
|
|
7849
|
+
}
|
|
7850
|
+
async function updateLaterMark(client, input) {
|
|
7851
|
+
await client.apiMultipart("saved.update", {
|
|
7852
|
+
item_id: input.channelId,
|
|
7853
|
+
item_type: "message",
|
|
7854
|
+
ts: input.ts,
|
|
7855
|
+
mark: input.mark
|
|
7856
|
+
});
|
|
7857
|
+
}
|
|
7858
|
+
async function saveLater(client, input) {
|
|
7859
|
+
await client.api("saved.add", {
|
|
7860
|
+
item_id: input.channelId,
|
|
7861
|
+
item_type: "message",
|
|
7862
|
+
ts: input.ts
|
|
7863
|
+
});
|
|
7864
|
+
}
|
|
7865
|
+
async function removeLater(client, input) {
|
|
7866
|
+
await client.api("saved.delete", {
|
|
7867
|
+
item_id: input.channelId,
|
|
7868
|
+
item_type: "message",
|
|
7869
|
+
ts: input.ts
|
|
7870
|
+
});
|
|
7871
|
+
}
|
|
7872
|
+
async function setLaterReminder(client, input) {
|
|
7873
|
+
await client.apiMultipart("saved.update", {
|
|
7874
|
+
item_id: input.channelId,
|
|
7875
|
+
item_type: "message",
|
|
7876
|
+
ts: input.ts,
|
|
7877
|
+
date_due: String(input.dateDue)
|
|
7878
|
+
});
|
|
7879
|
+
}
|
|
7880
|
+
function parseReminderDuration(input) {
|
|
7881
|
+
const now = Math.floor(Date.now() / 1000);
|
|
7882
|
+
const trimmed = input.trim().toLowerCase();
|
|
7883
|
+
const relMatch = trimmed.match(/^(\d+(?:\.\d+)?)\s*(m|min|mins|minutes?|h|hr|hrs|hours?|d|days?)$/);
|
|
7884
|
+
if (relMatch) {
|
|
7885
|
+
const amount = Number.parseFloat(relMatch[1]);
|
|
7886
|
+
const unit = relMatch[2].charAt(0);
|
|
7887
|
+
if (unit === "m") {
|
|
7888
|
+
return now + amount * 60;
|
|
7889
|
+
}
|
|
7890
|
+
if (unit === "h") {
|
|
7891
|
+
return now + amount * 3600;
|
|
7892
|
+
}
|
|
7893
|
+
if (unit === "d") {
|
|
7894
|
+
return now + amount * 86400;
|
|
7895
|
+
}
|
|
7896
|
+
}
|
|
7897
|
+
const tomorrow9am = getNext9am(1);
|
|
7898
|
+
if (trimmed === "tomorrow") {
|
|
7899
|
+
return tomorrow9am;
|
|
7900
|
+
}
|
|
7901
|
+
const dayNames = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
7902
|
+
const dayIndex = dayNames.indexOf(trimmed);
|
|
7903
|
+
if (dayIndex >= 0) {
|
|
7904
|
+
const today = new Date;
|
|
7905
|
+
const currentDay = today.getDay();
|
|
7906
|
+
let daysUntil = dayIndex - currentDay;
|
|
7907
|
+
if (daysUntil <= 0) {
|
|
7908
|
+
daysUntil += 7;
|
|
7909
|
+
}
|
|
7910
|
+
return getNext9am(daysUntil);
|
|
7911
|
+
}
|
|
7912
|
+
const asNum = Number(trimmed);
|
|
7913
|
+
if (!Number.isNaN(asNum) && asNum > 1e9) {
|
|
7914
|
+
return asNum;
|
|
7915
|
+
}
|
|
7916
|
+
throw new Error(`Invalid duration: "${input}". Use: 30m, 1h, 3h, 2d, tomorrow, monday, or a unix timestamp.`);
|
|
7917
|
+
}
|
|
7918
|
+
function getNext9am(daysFromNow) {
|
|
7919
|
+
const date = new Date;
|
|
7920
|
+
date.setDate(date.getDate() + daysFromNow);
|
|
7921
|
+
date.setHours(9, 0, 0, 0);
|
|
7922
|
+
return Math.floor(date.getTime() / 1000);
|
|
7923
|
+
}
|
|
7924
|
+
function isRecord5(value) {
|
|
7925
|
+
return typeof value === "object" && value !== null;
|
|
7926
|
+
}
|
|
7927
|
+
function asArray2(value) {
|
|
7928
|
+
return Array.isArray(value) ? value : [];
|
|
7929
|
+
}
|
|
7930
|
+
function getString5(value) {
|
|
7931
|
+
return typeof value === "string" ? value : undefined;
|
|
7932
|
+
}
|
|
7933
|
+
function getNumber3(value) {
|
|
7934
|
+
return typeof value === "number" ? value : undefined;
|
|
7935
|
+
}
|
|
7936
|
+
|
|
7937
|
+
// src/cli/later-command.ts
|
|
7938
|
+
function resolveTargetRef(input) {
|
|
7939
|
+
const { targetInput, options, ctx } = input;
|
|
7940
|
+
const target = parseMsgTarget(targetInput);
|
|
7941
|
+
if (target.kind === "url") {
|
|
7942
|
+
return {
|
|
7943
|
+
workspaceUrl: target.ref.workspace_url,
|
|
7944
|
+
getChannelAndTs: async () => ({
|
|
7945
|
+
channelId: target.ref.channel_id,
|
|
7946
|
+
ts: target.ref.message_ts
|
|
7947
|
+
})
|
|
7948
|
+
};
|
|
7949
|
+
}
|
|
7950
|
+
const workspaceUrl = ctx.effectiveWorkspaceUrl(options.workspace);
|
|
7951
|
+
const ts = options.ts?.trim();
|
|
7952
|
+
if (!ts) {
|
|
7953
|
+
throw new Error('When targeting a channel, you must pass --ts "<seconds>.<micros>"');
|
|
7954
|
+
}
|
|
7955
|
+
return {
|
|
7956
|
+
workspaceUrl,
|
|
7957
|
+
getChannelAndTs: async (client) => {
|
|
7958
|
+
const channelId = await resolveChannelId(client, target.kind === "channel" ? target.channel : targetInput);
|
|
7959
|
+
return { channelId, ts };
|
|
7960
|
+
}
|
|
7961
|
+
};
|
|
7962
|
+
}
|
|
7963
|
+
function registerLaterCommand(input) {
|
|
7964
|
+
const laterCmd = input.program.command("later").description("Manage saved-for-later messages (Slack's Later tab)");
|
|
7965
|
+
laterCmd.command("list", { isDefault: true }).description("List saved-for-later messages").option("--workspace <url>", "Workspace URL (defaults to your configured workspace)").option("--state <state>", "Filter by state: in_progress (default), archived, completed, all", "in_progress").option("--limit <n>", "Max items to show (default 20)", "20").option("--max-body-chars <n>", "Max content characters per message (default 4000, -1 for unlimited)", "4000").option("--counts-only", "Only show counts per state, skip message content").action(async (options) => {
|
|
7966
|
+
try {
|
|
7967
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
7968
|
+
const state = parseState(options.state ?? "in_progress");
|
|
7969
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
7970
|
+
workspaceUrl,
|
|
7971
|
+
work: async () => {
|
|
7972
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
7973
|
+
return fetchLaterItems(client, {
|
|
7974
|
+
state,
|
|
7975
|
+
limit: Number.parseInt(options.limit ?? "20", 10),
|
|
7976
|
+
maxBodyChars: Number.parseInt(options.maxBodyChars ?? "4000", 10),
|
|
7977
|
+
countsOnly: options.countsOnly
|
|
7978
|
+
});
|
|
7979
|
+
}
|
|
7980
|
+
});
|
|
7981
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
7982
|
+
} catch (err) {
|
|
7983
|
+
console.error(input.ctx.errorMessage(err));
|
|
7984
|
+
process.exitCode = 1;
|
|
7985
|
+
}
|
|
7986
|
+
});
|
|
7987
|
+
laterCmd.command("complete").description("Mark a saved message as completed").argument("<target>", "Slack message URL or channel ID").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
7988
|
+
const [targetInput, options] = args;
|
|
7989
|
+
try {
|
|
7990
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
7991
|
+
await input.ctx.withAutoRefresh({
|
|
7992
|
+
workspaceUrl: ref.workspaceUrl,
|
|
7993
|
+
work: async () => {
|
|
7994
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
7995
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
7996
|
+
await updateLaterMark(client, { channelId, ts, mark: "completed" });
|
|
7997
|
+
}
|
|
7998
|
+
});
|
|
7999
|
+
console.log(JSON.stringify({ ok: true }));
|
|
8000
|
+
} catch (err) {
|
|
8001
|
+
console.error(input.ctx.errorMessage(err));
|
|
8002
|
+
process.exitCode = 1;
|
|
8003
|
+
}
|
|
8004
|
+
});
|
|
8005
|
+
laterCmd.command("archive").description("Archive a saved message").argument("<target>", "Slack message URL or channel ID").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
8006
|
+
const [targetInput, options] = args;
|
|
8007
|
+
try {
|
|
8008
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
8009
|
+
await input.ctx.withAutoRefresh({
|
|
8010
|
+
workspaceUrl: ref.workspaceUrl,
|
|
8011
|
+
work: async () => {
|
|
8012
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
8013
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
8014
|
+
await updateLaterMark(client, { channelId, ts, mark: "archived" });
|
|
8015
|
+
}
|
|
8016
|
+
});
|
|
8017
|
+
console.log(JSON.stringify({ ok: true }));
|
|
8018
|
+
} catch (err) {
|
|
8019
|
+
console.error(input.ctx.errorMessage(err));
|
|
8020
|
+
process.exitCode = 1;
|
|
8021
|
+
}
|
|
8022
|
+
});
|
|
8023
|
+
laterCmd.command("reopen").description("Move a saved message back to in-progress").argument("<target>", "Slack message URL or channel ID").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
8024
|
+
const [targetInput, options] = args;
|
|
8025
|
+
try {
|
|
8026
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
8027
|
+
await input.ctx.withAutoRefresh({
|
|
8028
|
+
workspaceUrl: ref.workspaceUrl,
|
|
8029
|
+
work: async () => {
|
|
8030
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
8031
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
8032
|
+
await Promise.allSettled([
|
|
8033
|
+
updateLaterMark(client, { channelId, ts, mark: "uncompleted" }),
|
|
8034
|
+
updateLaterMark(client, { channelId, ts, mark: "unarchived" })
|
|
8035
|
+
]);
|
|
8036
|
+
}
|
|
8037
|
+
});
|
|
8038
|
+
console.log(JSON.stringify({ ok: true }));
|
|
8039
|
+
} catch (err) {
|
|
8040
|
+
console.error(input.ctx.errorMessage(err));
|
|
8041
|
+
process.exitCode = 1;
|
|
8042
|
+
}
|
|
8043
|
+
});
|
|
8044
|
+
laterCmd.command("save").description("Save a message for later").argument("<target>", "Slack message URL or channel ID").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
8045
|
+
const [targetInput, options] = args;
|
|
8046
|
+
try {
|
|
8047
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
8048
|
+
await input.ctx.withAutoRefresh({
|
|
8049
|
+
workspaceUrl: ref.workspaceUrl,
|
|
8050
|
+
work: async () => {
|
|
8051
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
8052
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
8053
|
+
await saveLater(client, { channelId, ts });
|
|
8054
|
+
}
|
|
8055
|
+
});
|
|
8056
|
+
console.log(JSON.stringify({ ok: true }));
|
|
8057
|
+
} catch (err) {
|
|
8058
|
+
console.error(input.ctx.errorMessage(err));
|
|
8059
|
+
process.exitCode = 1;
|
|
8060
|
+
}
|
|
8061
|
+
});
|
|
8062
|
+
laterCmd.command("remove").description("Remove a message from Later entirely").argument("<target>", "Slack message URL or channel ID").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
8063
|
+
const [targetInput, options] = args;
|
|
8064
|
+
try {
|
|
8065
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
8066
|
+
await input.ctx.withAutoRefresh({
|
|
8067
|
+
workspaceUrl: ref.workspaceUrl,
|
|
8068
|
+
work: async () => {
|
|
8069
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
8070
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
8071
|
+
await removeLater(client, { channelId, ts });
|
|
8072
|
+
}
|
|
8073
|
+
});
|
|
8074
|
+
console.log(JSON.stringify({ ok: true }));
|
|
8075
|
+
} catch (err) {
|
|
8076
|
+
console.error(input.ctx.errorMessage(err));
|
|
8077
|
+
process.exitCode = 1;
|
|
8078
|
+
}
|
|
8079
|
+
});
|
|
8080
|
+
laterCmd.command("remind").description("Set a reminder for a saved message").argument("<target>", "Slack message URL or channel ID").requiredOption("--in <duration>", "When to remind: 30m, 1h, 3h, 2d, tomorrow, monday, etc.").option("--workspace <url>", "Workspace URL").option("--ts <ts>", "Message ts (required when using channel ID)").action(async (...args) => {
|
|
8081
|
+
const [targetInput, options] = args;
|
|
8082
|
+
try {
|
|
8083
|
+
const ref = resolveTargetRef({ targetInput, options, ctx: input.ctx });
|
|
8084
|
+
const reminderTime = parseReminderDuration(options.in);
|
|
8085
|
+
await input.ctx.withAutoRefresh({
|
|
8086
|
+
workspaceUrl: ref.workspaceUrl,
|
|
8087
|
+
work: async () => {
|
|
8088
|
+
const { client } = await input.ctx.getClientForWorkspace(ref.workspaceUrl);
|
|
8089
|
+
const { channelId, ts } = await ref.getChannelAndTs(client);
|
|
8090
|
+
await setLaterReminder(client, { channelId, ts, dateDue: reminderTime });
|
|
8091
|
+
}
|
|
8092
|
+
});
|
|
8093
|
+
console.log(JSON.stringify({
|
|
8094
|
+
ok: true,
|
|
8095
|
+
remind_at: reminderTime
|
|
8096
|
+
}));
|
|
8097
|
+
} catch (err) {
|
|
8098
|
+
console.error(input.ctx.errorMessage(err));
|
|
8099
|
+
process.exitCode = 1;
|
|
8100
|
+
}
|
|
8101
|
+
});
|
|
8102
|
+
}
|
|
8103
|
+
function parseState(value) {
|
|
8104
|
+
const v = value.toLowerCase().trim();
|
|
8105
|
+
if (v === "in_progress" || v === "in-progress" || v === "active" || v === "open") {
|
|
8106
|
+
return "in_progress";
|
|
8107
|
+
}
|
|
8108
|
+
if (v === "archived" || v === "archive") {
|
|
8109
|
+
return "archived";
|
|
8110
|
+
}
|
|
8111
|
+
if (v === "completed" || v === "complete" || v === "done") {
|
|
8112
|
+
return "completed";
|
|
8113
|
+
}
|
|
8114
|
+
if (v === "all") {
|
|
8115
|
+
return "all";
|
|
8116
|
+
}
|
|
8117
|
+
return "in_progress";
|
|
8118
|
+
}
|
|
8119
|
+
|
|
8120
|
+
// src/slack/unreads.ts
|
|
8121
|
+
async function fetchUnreads(client, options) {
|
|
8122
|
+
const includeMessages = options?.includeMessages ?? true;
|
|
8123
|
+
const maxMessages = options?.maxMessagesPerChannel ?? 10;
|
|
8124
|
+
const maxBodyChars = options?.maxBodyChars ?? 4000;
|
|
8125
|
+
const skipSystem = options?.skipSystemMessages ?? true;
|
|
8126
|
+
const resp = await client.api("client.counts", {
|
|
8127
|
+
thread_count_by_channel: true
|
|
8128
|
+
});
|
|
8129
|
+
const channels = asArray3(resp.channels).filter(isRecord6);
|
|
8130
|
+
const mpims = asArray3(resp.mpims).filter(isRecord6);
|
|
8131
|
+
const ims = asArray3(resp.ims).filter(isRecord6);
|
|
8132
|
+
const allEntries = [
|
|
8133
|
+
...channels.map((c) => ({ ...c, type: "channel" })),
|
|
8134
|
+
...mpims.map((c) => ({ ...c, type: "mpim" })),
|
|
8135
|
+
...ims.map((c) => ({ ...c, type: "dm" }))
|
|
8136
|
+
];
|
|
8137
|
+
const withUnreads = allEntries.filter((c) => c.has_unreads);
|
|
8138
|
+
const channelInfos = await Promise.all(withUnreads.map(async (entry) => {
|
|
8139
|
+
const channelInfoPromise = (async () => {
|
|
8140
|
+
let name;
|
|
8141
|
+
let { type } = entry;
|
|
8142
|
+
try {
|
|
8143
|
+
const info = await client.api("conversations.info", {
|
|
8144
|
+
channel: entry.id
|
|
8145
|
+
});
|
|
8146
|
+
const ch = isRecord6(info.channel) ? info.channel : null;
|
|
8147
|
+
if (ch) {
|
|
8148
|
+
name = getString6(ch.name) ?? getString6(ch.name_normalized) ?? undefined;
|
|
8149
|
+
if (ch.is_im) {
|
|
8150
|
+
type = "dm";
|
|
8151
|
+
const userId = getString6(ch.user);
|
|
8152
|
+
if (userId && !name) {
|
|
8153
|
+
try {
|
|
8154
|
+
const userInfo = await client.api("users.info", { user: userId });
|
|
8155
|
+
const u = isRecord6(userInfo.user) ? userInfo.user : null;
|
|
8156
|
+
const profile = u && isRecord6(u.profile) ? u.profile : null;
|
|
8157
|
+
name = getString6(profile?.display_name) || getString6(u?.real_name) || getString6(u?.name) || undefined;
|
|
8158
|
+
} catch {}
|
|
8159
|
+
}
|
|
8160
|
+
} else if (ch.is_mpim) {
|
|
8161
|
+
type = "mpim";
|
|
8162
|
+
} else if (ch.is_group || ch.is_private) {
|
|
8163
|
+
type = "channel";
|
|
8164
|
+
} else {
|
|
8165
|
+
type = "channel";
|
|
8166
|
+
}
|
|
7152
8167
|
}
|
|
7153
|
-
}
|
|
8168
|
+
} catch {}
|
|
8169
|
+
return { name, type };
|
|
8170
|
+
})();
|
|
8171
|
+
const historyPromise = (async () => {
|
|
8172
|
+
let messages;
|
|
8173
|
+
let unreadCount = entry.unread_count_display ?? entry.unread_count ?? (entry.has_unreads ? 1 : 0);
|
|
8174
|
+
if (includeMessages && entry.last_read) {
|
|
8175
|
+
try {
|
|
8176
|
+
const history = await client.api("conversations.history", {
|
|
8177
|
+
channel: entry.id,
|
|
8178
|
+
oldest: entry.last_read,
|
|
8179
|
+
limit: maxMessages,
|
|
8180
|
+
inclusive: false
|
|
8181
|
+
});
|
|
8182
|
+
let msgs = asArray3(history.messages).filter(isRecord6);
|
|
8183
|
+
if (skipSystem) {
|
|
8184
|
+
msgs = msgs.filter((m) => {
|
|
8185
|
+
const subtype = getString6(m.subtype);
|
|
8186
|
+
if (!subtype) {
|
|
8187
|
+
return true;
|
|
8188
|
+
}
|
|
8189
|
+
const systemSubtypes = [
|
|
8190
|
+
"channel_join",
|
|
8191
|
+
"channel_leave",
|
|
8192
|
+
"channel_topic",
|
|
8193
|
+
"channel_purpose",
|
|
8194
|
+
"channel_name",
|
|
8195
|
+
"channel_archive",
|
|
8196
|
+
"channel_unarchive",
|
|
8197
|
+
"group_join",
|
|
8198
|
+
"group_leave",
|
|
8199
|
+
"group_topic",
|
|
8200
|
+
"group_purpose",
|
|
8201
|
+
"group_name",
|
|
8202
|
+
"group_archive",
|
|
8203
|
+
"group_unarchive"
|
|
8204
|
+
];
|
|
8205
|
+
return !systemSubtypes.includes(subtype);
|
|
8206
|
+
});
|
|
8207
|
+
}
|
|
8208
|
+
if (entry.unread_count_display === undefined && entry.unread_count === undefined) {
|
|
8209
|
+
unreadCount = msgs.length;
|
|
8210
|
+
if (history.has_more) {
|
|
8211
|
+
unreadCount = Math.max(unreadCount, 2);
|
|
8212
|
+
}
|
|
8213
|
+
}
|
|
8214
|
+
messages = msgs.map((m) => {
|
|
8215
|
+
const rendered = renderSlackMessageContent(m);
|
|
8216
|
+
const content = maxBodyChars >= 0 && rendered.length > maxBodyChars ? `${rendered.slice(0, maxBodyChars)}
|
|
8217
|
+
...` : rendered;
|
|
8218
|
+
return {
|
|
8219
|
+
ts: getString6(m.ts) ?? "",
|
|
8220
|
+
author: getString6(m.user) || getString6(m.bot_id) ? {
|
|
8221
|
+
user_id: getString6(m.user) ?? undefined,
|
|
8222
|
+
bot_id: getString6(m.bot_id) ?? undefined
|
|
8223
|
+
} : undefined,
|
|
8224
|
+
content: content || undefined,
|
|
8225
|
+
thread_ts: getString6(m.thread_ts) ?? undefined,
|
|
8226
|
+
reply_count: getNumber4(m.reply_count) ?? undefined
|
|
8227
|
+
};
|
|
8228
|
+
});
|
|
8229
|
+
messages.sort((a, b) => Number.parseFloat(a.ts) - Number.parseFloat(b.ts));
|
|
8230
|
+
} catch {}
|
|
8231
|
+
}
|
|
8232
|
+
return { messages, unreadCount };
|
|
8233
|
+
})();
|
|
8234
|
+
const [channelData, historyData] = await Promise.all([channelInfoPromise, historyPromise]);
|
|
8235
|
+
return {
|
|
8236
|
+
channel_id: entry.id,
|
|
8237
|
+
channel_name: channelData.name,
|
|
8238
|
+
channel_type: channelData.type,
|
|
8239
|
+
unread_count: historyData.unreadCount,
|
|
8240
|
+
mention_count: entry.mention_count ?? 0,
|
|
8241
|
+
messages: historyData.messages
|
|
8242
|
+
};
|
|
8243
|
+
}));
|
|
8244
|
+
channelInfos.sort((a, b) => {
|
|
8245
|
+
if (a.mention_count !== b.mention_count) {
|
|
8246
|
+
return b.mention_count - a.mention_count;
|
|
7154
8247
|
}
|
|
8248
|
+
return b.unread_count - a.unread_count;
|
|
7155
8249
|
});
|
|
7156
|
-
|
|
8250
|
+
const threads = isRecord6(resp.threads) ? resp.threads : null;
|
|
8251
|
+
const threadInfo = threads?.has_unreads ? {
|
|
8252
|
+
has_unreads: true,
|
|
8253
|
+
mention_count: threads.mention_count ?? 0
|
|
8254
|
+
} : null;
|
|
8255
|
+
return { channels: channelInfos, threads: threadInfo };
|
|
8256
|
+
}
|
|
8257
|
+
function isRecord6(value) {
|
|
8258
|
+
return typeof value === "object" && value !== null;
|
|
7157
8259
|
}
|
|
7158
|
-
function
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
8260
|
+
function asArray3(value) {
|
|
8261
|
+
return Array.isArray(value) ? value : [];
|
|
8262
|
+
}
|
|
8263
|
+
function getString6(value) {
|
|
8264
|
+
return typeof value === "string" ? value : undefined;
|
|
8265
|
+
}
|
|
8266
|
+
function getNumber4(value) {
|
|
8267
|
+
return typeof value === "number" ? value : undefined;
|
|
8268
|
+
}
|
|
8269
|
+
|
|
8270
|
+
// src/cli/unreads-command.ts
|
|
8271
|
+
function registerUnreadsCommand(input) {
|
|
8272
|
+
input.program.command("unreads").description("Show all unread messages across channels, DMs, and threads").option("--workspace <url>", "Workspace URL (defaults to your configured workspace)").option("--counts-only", "Only show unread counts, do not fetch message content").option("--max-messages <n>", "Max unread messages to fetch per channel (default 10)", "10").option("--max-body-chars <n>", "Max content characters per message (default 4000, -1 for unlimited)", "4000").option("--include-system", "Include system messages (joins, leaves, topic changes, etc.)").action(async (options) => {
|
|
7162
8273
|
try {
|
|
7163
|
-
|
|
8274
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
8275
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
8276
|
+
workspaceUrl,
|
|
8277
|
+
work: async () => {
|
|
8278
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
8279
|
+
return fetchUnreads(client, {
|
|
8280
|
+
includeMessages: !options.countsOnly,
|
|
8281
|
+
maxMessagesPerChannel: Number.parseInt(options.maxMessages ?? "10", 10),
|
|
8282
|
+
maxBodyChars: Number.parseInt(options.maxBodyChars ?? "4000", 10),
|
|
8283
|
+
skipSystemMessages: !options.includeSystem
|
|
8284
|
+
});
|
|
8285
|
+
}
|
|
8286
|
+
});
|
|
8287
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
7164
8288
|
} catch (err) {
|
|
7165
8289
|
console.error(input.ctx.errorMessage(err));
|
|
7166
8290
|
process.exitCode = 1;
|
|
7167
8291
|
}
|
|
7168
8292
|
});
|
|
7169
|
-
create({ kind: "all", name: "all", desc: "Search messages and files" });
|
|
7170
|
-
create({ kind: "messages", name: "messages", desc: "Search messages" });
|
|
7171
|
-
create({ kind: "files", name: "files", desc: "Search files" });
|
|
7172
8293
|
}
|
|
7173
8294
|
|
|
7174
8295
|
// src/lib/update.ts
|
|
7175
8296
|
import { execSync as execSync2 } from "node:child_process";
|
|
7176
|
-
import { createHash } from "node:crypto";
|
|
8297
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
7177
8298
|
import { chmod, copyFile as copyFile2, mkdir as mkdir5, readFile as readFile7, rename, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
|
|
7178
8299
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
7179
|
-
import { basename as basename3, join as
|
|
8300
|
+
import { basename as basename3, join as join14 } from "node:path";
|
|
7180
8301
|
var REPO = "stablyai/agent-slack";
|
|
7181
8302
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
7182
8303
|
function getCachePath() {
|
|
7183
|
-
return
|
|
8304
|
+
return join14(getAppDir(), "update-check.json");
|
|
7184
8305
|
}
|
|
7185
8306
|
function compareSemver(a, b) {
|
|
7186
8307
|
const pa = a.replace(/^v/, "").split(".").map(Number);
|
|
@@ -7276,16 +8397,16 @@ function detectPlatformAsset() {
|
|
|
7276
8397
|
}
|
|
7277
8398
|
async function sha256(filePath) {
|
|
7278
8399
|
const data = await readFile7(filePath);
|
|
7279
|
-
return
|
|
8400
|
+
return createHash2("sha256").update(data).digest("hex");
|
|
7280
8401
|
}
|
|
7281
8402
|
async function performUpdate(latest) {
|
|
7282
8403
|
const asset = detectPlatformAsset();
|
|
7283
8404
|
const tag = `v${latest}`;
|
|
7284
8405
|
const baseUrl = `https://github.com/${REPO}/releases/download/${tag}`;
|
|
7285
|
-
const tmp =
|
|
8406
|
+
const tmp = join14(tmpdir5(), `agent-slack-update-${Date.now()}`);
|
|
7286
8407
|
await mkdir5(tmp, { recursive: true });
|
|
7287
|
-
const binTmp =
|
|
7288
|
-
const sumsTmp =
|
|
8408
|
+
const binTmp = join14(tmp, asset);
|
|
8409
|
+
const sumsTmp = join14(tmp, "checksums-sha256.txt");
|
|
7289
8410
|
try {
|
|
7290
8411
|
const [binResp, sumsResp] = await Promise.all([
|
|
7291
8412
|
fetch(`${baseUrl}/${asset}`, { signal: AbortSignal.timeout(120000) }),
|
|
@@ -7300,288 +8421,100 @@ async function performUpdate(latest) {
|
|
|
7300
8421
|
await writeFile4(binTmp, Buffer.from(await binResp.arrayBuffer()));
|
|
7301
8422
|
const sumsText = await sumsResp.text();
|
|
7302
8423
|
await writeFile4(sumsTmp, sumsText);
|
|
7303
|
-
const expected = sumsText.split(`
|
|
7304
|
-
`).map((line) => line.trim().split(/\s+/)).find((parts) => parts[1] === asset)?.[0];
|
|
7305
|
-
if (!expected) {
|
|
7306
|
-
return { success: false, message: `Checksum not found for ${asset} in release checksums` };
|
|
7307
|
-
}
|
|
7308
|
-
const actual = await sha256(binTmp);
|
|
7309
|
-
if (actual !== expected) {
|
|
7310
|
-
return { success: false, message: `Checksum mismatch: expected ${expected}, got ${actual}` };
|
|
7311
|
-
}
|
|
7312
|
-
const currentBin = process.execPath;
|
|
7313
|
-
const backupPath = `${currentBin}.bak`;
|
|
7314
|
-
await rename(currentBin, backupPath);
|
|
7315
|
-
try {
|
|
7316
|
-
await copyFile2(binTmp, currentBin);
|
|
7317
|
-
await chmod(currentBin, 493);
|
|
7318
|
-
await rm3(backupPath, { force: true });
|
|
7319
|
-
} catch (err) {
|
|
7320
|
-
try {
|
|
7321
|
-
await rename(backupPath, currentBin);
|
|
7322
|
-
} catch {}
|
|
7323
|
-
throw err;
|
|
7324
|
-
}
|
|
7325
|
-
return { success: true, message: `Updated agent-slack to ${latest}` };
|
|
7326
|
-
} finally {
|
|
7327
|
-
await rm3(tmp, { recursive: true, force: true }).catch(() => {});
|
|
7328
|
-
}
|
|
7329
|
-
}
|
|
7330
|
-
async function backgroundUpdateCheck() {
|
|
7331
|
-
if (process.env.AGENT_SLACK_NO_UPDATE_CHECK === "1") {
|
|
7332
|
-
return;
|
|
7333
|
-
}
|
|
7334
|
-
try {
|
|
7335
|
-
const result = await checkForUpdate();
|
|
7336
|
-
if (result?.update_available) {
|
|
7337
|
-
const cmd = getUpdateCommand();
|
|
7338
|
-
process.stderr.write(`
|
|
7339
|
-
Update available: ${result.current} → ${result.latest}. Run "${cmd}" to upgrade.
|
|
7340
|
-
`);
|
|
7341
|
-
}
|
|
7342
|
-
} catch {}
|
|
7343
|
-
}
|
|
7344
|
-
|
|
7345
|
-
// src/cli/update-command.ts
|
|
7346
|
-
function registerUpdateCommand(input) {
|
|
7347
|
-
input.program.command("update").description("Update agent-slack to the latest version").option("--check", "Only check for updates (don't install)").action(async (...args) => {
|
|
7348
|
-
const [options] = args;
|
|
7349
|
-
const method = detectInstallMethod();
|
|
7350
|
-
try {
|
|
7351
|
-
const result = await checkForUpdate(true);
|
|
7352
|
-
if (!result) {
|
|
7353
|
-
console.error("Could not check for updates. Check your network connection.");
|
|
7354
|
-
process.exitCode = 1;
|
|
7355
|
-
return;
|
|
7356
|
-
}
|
|
7357
|
-
if (!result.update_available) {
|
|
7358
|
-
console.log(JSON.stringify(pruneEmpty({ ...result, install_method: method, status: "up_to_date" }), null, 2));
|
|
7359
|
-
return;
|
|
7360
|
-
}
|
|
7361
|
-
if (options.check) {
|
|
7362
|
-
console.log(JSON.stringify(pruneEmpty({
|
|
7363
|
-
...result,
|
|
7364
|
-
install_method: method,
|
|
7365
|
-
update_command: getUpdateCommand(method),
|
|
7366
|
-
status: "update_available"
|
|
7367
|
-
}), null, 2));
|
|
7368
|
-
return;
|
|
7369
|
-
}
|
|
7370
|
-
process.stderr.write(`Updating agent-slack ${result.current} → ${result.latest}...
|
|
7371
|
-
`);
|
|
7372
|
-
let outcome;
|
|
7373
|
-
if (method === "npm" || method === "bun") {
|
|
7374
|
-
process.stderr.write(`Detected install method: ${method}
|
|
7375
|
-
`);
|
|
7376
|
-
outcome = performPackageManagerUpdate(method);
|
|
7377
|
-
} else {
|
|
7378
|
-
outcome = await performUpdate(result.latest);
|
|
7379
|
-
}
|
|
7380
|
-
if (!outcome.success) {
|
|
7381
|
-
console.error(outcome.message);
|
|
7382
|
-
process.exitCode = 1;
|
|
7383
|
-
return;
|
|
7384
|
-
}
|
|
7385
|
-
console.log(JSON.stringify(pruneEmpty({
|
|
7386
|
-
status: "updated",
|
|
7387
|
-
install_method: method,
|
|
7388
|
-
previous_version: result.current,
|
|
7389
|
-
new_version: result.latest,
|
|
7390
|
-
message: outcome.message
|
|
7391
|
-
}), null, 2));
|
|
7392
|
-
} catch (err) {
|
|
7393
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
7394
|
-
process.exitCode = 1;
|
|
7395
|
-
}
|
|
7396
|
-
});
|
|
7397
|
-
}
|
|
7398
|
-
|
|
7399
|
-
// src/slack/users.ts
|
|
7400
|
-
async function listUsers(client, options) {
|
|
7401
|
-
const limit = Math.min(Math.max(options?.limit ?? 200, 1), 1000);
|
|
7402
|
-
const includeBots = options?.includeBots ?? false;
|
|
7403
|
-
let next_cursor;
|
|
7404
|
-
const [out, dmMap] = await Promise.all([
|
|
7405
|
-
(async () => {
|
|
7406
|
-
const users = [];
|
|
7407
|
-
let cursor = options?.cursor;
|
|
7408
|
-
while (users.length < limit) {
|
|
7409
|
-
const pageSize = Math.min(200, limit - users.length);
|
|
7410
|
-
const resp = await client.api("users.list", { limit: pageSize, cursor });
|
|
7411
|
-
const members = asArray(resp.members).filter(isRecord);
|
|
7412
|
-
for (const m of members) {
|
|
7413
|
-
const id = getString(m.id);
|
|
7414
|
-
if (!id) {
|
|
7415
|
-
continue;
|
|
7416
|
-
}
|
|
7417
|
-
if (!includeBots && m.is_bot) {
|
|
7418
|
-
continue;
|
|
7419
|
-
}
|
|
7420
|
-
users.push(toCompactUser(m));
|
|
7421
|
-
if (users.length >= limit) {
|
|
7422
|
-
break;
|
|
7423
|
-
}
|
|
7424
|
-
}
|
|
7425
|
-
const meta = isRecord(resp.response_metadata) ? resp.response_metadata : null;
|
|
7426
|
-
const next = meta ? getString(meta.next_cursor) : undefined;
|
|
7427
|
-
if (!next) {
|
|
7428
|
-
break;
|
|
7429
|
-
}
|
|
7430
|
-
cursor = next;
|
|
7431
|
-
next_cursor = next;
|
|
7432
|
-
}
|
|
7433
|
-
return users;
|
|
7434
|
-
})(),
|
|
7435
|
-
fetchDmMap(client)
|
|
7436
|
-
]);
|
|
7437
|
-
for (const u of out) {
|
|
7438
|
-
const dmId = dmMap.get(u.id);
|
|
7439
|
-
if (dmId) {
|
|
7440
|
-
u.dm_id = dmId;
|
|
7441
|
-
}
|
|
7442
|
-
}
|
|
7443
|
-
return { users: out, next_cursor };
|
|
7444
|
-
}
|
|
7445
|
-
async function getUser(client, input) {
|
|
7446
|
-
const trimmed = input.trim();
|
|
7447
|
-
if (!trimmed) {
|
|
7448
|
-
throw new Error("User is empty");
|
|
7449
|
-
}
|
|
7450
|
-
const userId = await resolveUserId2(client, trimmed);
|
|
7451
|
-
if (!userId) {
|
|
7452
|
-
throw new Error(`Could not resolve user: ${input}`);
|
|
7453
|
-
}
|
|
7454
|
-
const resp = await client.api("users.info", { user: userId });
|
|
7455
|
-
const u = isRecord(resp.user) ? resp.user : null;
|
|
7456
|
-
if (!u || !getString(u.id)) {
|
|
7457
|
-
throw new Error("users.info returned no user");
|
|
7458
|
-
}
|
|
7459
|
-
return toCompactUser(u);
|
|
7460
|
-
}
|
|
7461
|
-
async function resolveUserId2(client, input) {
|
|
7462
|
-
const trimmed = input.trim();
|
|
7463
|
-
if (/^U[A-Z0-9]{8,}$/.test(trimmed)) {
|
|
7464
|
-
return trimmed;
|
|
7465
|
-
}
|
|
7466
|
-
const looksLikeEmail = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(trimmed) && !trimmed.startsWith("@");
|
|
7467
|
-
if (looksLikeEmail) {
|
|
7468
|
-
try {
|
|
7469
|
-
const byEmail = await client.api("users.lookupByEmail", { email: trimmed });
|
|
7470
|
-
const user = isRecord(byEmail.user) ? byEmail.user : null;
|
|
7471
|
-
const userId = user ? getString(user.id) : undefined;
|
|
7472
|
-
if (userId) {
|
|
7473
|
-
return userId;
|
|
7474
|
-
}
|
|
7475
|
-
} catch {}
|
|
7476
|
-
}
|
|
7477
|
-
const handle = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
7478
|
-
if (!handle) {
|
|
7479
|
-
return null;
|
|
7480
|
-
}
|
|
7481
|
-
let cursor;
|
|
7482
|
-
for (;; ) {
|
|
7483
|
-
const resp = await client.api("users.list", { limit: 200, cursor });
|
|
7484
|
-
const members = asArray(resp.members).filter(isRecord);
|
|
7485
|
-
const found = members.find((m) => {
|
|
7486
|
-
if (getString(m.name) === handle) {
|
|
7487
|
-
return true;
|
|
7488
|
-
}
|
|
7489
|
-
if (looksLikeEmail) {
|
|
7490
|
-
const profile = isRecord(m.profile) ? m.profile : null;
|
|
7491
|
-
const email = profile ? getString(profile.email) : undefined;
|
|
7492
|
-
return Boolean(email) && email?.toLowerCase() === trimmed.toLowerCase();
|
|
7493
|
-
}
|
|
7494
|
-
return false;
|
|
7495
|
-
});
|
|
7496
|
-
if (found) {
|
|
7497
|
-
const id = getString(found.id);
|
|
7498
|
-
if (id) {
|
|
7499
|
-
return id;
|
|
7500
|
-
}
|
|
7501
|
-
}
|
|
7502
|
-
const meta = isRecord(resp.response_metadata) ? resp.response_metadata : null;
|
|
7503
|
-
const next = meta ? getString(meta.next_cursor) : undefined;
|
|
7504
|
-
if (!next) {
|
|
7505
|
-
break;
|
|
8424
|
+
const expected = sumsText.split(`
|
|
8425
|
+
`).map((line) => line.trim().split(/\s+/)).find((parts) => parts[1] === asset)?.[0];
|
|
8426
|
+
if (!expected) {
|
|
8427
|
+
return { success: false, message: `Checksum not found for ${asset} in release checksums` };
|
|
7506
8428
|
}
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
}
|
|
7511
|
-
async function fetchDmMap(client) {
|
|
7512
|
-
const map = new Map;
|
|
7513
|
-
let cursor;
|
|
7514
|
-
for (;; ) {
|
|
7515
|
-
const resp = await client.api("conversations.list", {
|
|
7516
|
-
types: "im",
|
|
7517
|
-
limit: 200,
|
|
7518
|
-
cursor
|
|
7519
|
-
});
|
|
7520
|
-
const channels = asArray(resp.channels).filter(isRecord);
|
|
7521
|
-
for (const ch of channels) {
|
|
7522
|
-
const id = getString(ch.id);
|
|
7523
|
-
const user = getString(ch.user);
|
|
7524
|
-
if (id && user) {
|
|
7525
|
-
map.set(user, id);
|
|
7526
|
-
}
|
|
8429
|
+
const actual = await sha256(binTmp);
|
|
8430
|
+
if (actual !== expected) {
|
|
8431
|
+
return { success: false, message: `Checksum mismatch: expected ${expected}, got ${actual}` };
|
|
7527
8432
|
}
|
|
7528
|
-
const
|
|
7529
|
-
const
|
|
7530
|
-
|
|
7531
|
-
|
|
8433
|
+
const currentBin = process.execPath;
|
|
8434
|
+
const backupPath = `${currentBin}.bak`;
|
|
8435
|
+
await rename(currentBin, backupPath);
|
|
8436
|
+
try {
|
|
8437
|
+
await copyFile2(binTmp, currentBin);
|
|
8438
|
+
await chmod(currentBin, 493);
|
|
8439
|
+
await rm3(backupPath, { force: true });
|
|
8440
|
+
} catch (err) {
|
|
8441
|
+
try {
|
|
8442
|
+
await rename(backupPath, currentBin);
|
|
8443
|
+
} catch {}
|
|
8444
|
+
throw err;
|
|
7532
8445
|
}
|
|
7533
|
-
|
|
8446
|
+
return { success: true, message: `Updated agent-slack to ${latest}` };
|
|
8447
|
+
} finally {
|
|
8448
|
+
await rm3(tmp, { recursive: true, force: true }).catch(() => {});
|
|
7534
8449
|
}
|
|
7535
|
-
return map;
|
|
7536
8450
|
}
|
|
7537
|
-
async function
|
|
7538
|
-
if (
|
|
7539
|
-
|
|
7540
|
-
}
|
|
7541
|
-
if (inputs.length > 8) {
|
|
7542
|
-
throw new Error("Slack supports a maximum of 8 users in a group DM");
|
|
8451
|
+
async function backgroundUpdateCheck() {
|
|
8452
|
+
if (process.env.AGENT_SLACK_NO_UPDATE_CHECK === "1") {
|
|
8453
|
+
return;
|
|
7543
8454
|
}
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
if (!userId) {
|
|
7552
|
-
throw new Error(`Could not resolve user: ${input}`);
|
|
8455
|
+
try {
|
|
8456
|
+
const result = await checkForUpdate();
|
|
8457
|
+
if (result?.update_available) {
|
|
8458
|
+
const cmd = getUpdateCommand();
|
|
8459
|
+
process.stderr.write(`
|
|
8460
|
+
Update available: ${result.current} → ${result.latest}. Run "${cmd}" to upgrade.
|
|
8461
|
+
`);
|
|
7553
8462
|
}
|
|
7554
|
-
|
|
7555
|
-
}
|
|
7556
|
-
if (userIds.length === 0) {
|
|
7557
|
-
throw new Error("No valid users provided");
|
|
7558
|
-
}
|
|
7559
|
-
const resp = await client.api("conversations.open", { users: userIds.join(",") });
|
|
7560
|
-
const channel = isRecord(resp.channel) ? resp.channel : null;
|
|
7561
|
-
const channelId = channel ? getString(channel.id) : null;
|
|
7562
|
-
if (!channelId) {
|
|
7563
|
-
throw new Error("conversations.open returned no channel");
|
|
7564
|
-
}
|
|
7565
|
-
const channelType = channelId.startsWith("D") ? "dm" : "group_dm";
|
|
7566
|
-
return {
|
|
7567
|
-
user_ids: userIds,
|
|
7568
|
-
dm_channel_id: channelId,
|
|
7569
|
-
channel_type: channelType
|
|
7570
|
-
};
|
|
8463
|
+
} catch {}
|
|
7571
8464
|
}
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
8465
|
+
|
|
8466
|
+
// src/cli/update-command.ts
|
|
8467
|
+
function registerUpdateCommand(input) {
|
|
8468
|
+
input.program.command("update").description("Update agent-slack to the latest version").option("--check", "Only check for updates (don't install)").action(async (...args) => {
|
|
8469
|
+
const [options] = args;
|
|
8470
|
+
const method = detectInstallMethod();
|
|
8471
|
+
try {
|
|
8472
|
+
const result = await checkForUpdate(true);
|
|
8473
|
+
if (!result) {
|
|
8474
|
+
console.error("Could not check for updates. Check your network connection.");
|
|
8475
|
+
process.exitCode = 1;
|
|
8476
|
+
return;
|
|
8477
|
+
}
|
|
8478
|
+
if (!result.update_available) {
|
|
8479
|
+
console.log(JSON.stringify(pruneEmpty({ ...result, install_method: method, status: "up_to_date" }), null, 2));
|
|
8480
|
+
return;
|
|
8481
|
+
}
|
|
8482
|
+
if (options.check) {
|
|
8483
|
+
console.log(JSON.stringify(pruneEmpty({
|
|
8484
|
+
...result,
|
|
8485
|
+
install_method: method,
|
|
8486
|
+
update_command: getUpdateCommand(method),
|
|
8487
|
+
status: "update_available"
|
|
8488
|
+
}), null, 2));
|
|
8489
|
+
return;
|
|
8490
|
+
}
|
|
8491
|
+
process.stderr.write(`Updating agent-slack ${result.current} → ${result.latest}...
|
|
8492
|
+
`);
|
|
8493
|
+
let outcome;
|
|
8494
|
+
if (method === "npm" || method === "bun") {
|
|
8495
|
+
process.stderr.write(`Detected install method: ${method}
|
|
8496
|
+
`);
|
|
8497
|
+
outcome = performPackageManagerUpdate(method);
|
|
8498
|
+
} else {
|
|
8499
|
+
outcome = await performUpdate(result.latest);
|
|
8500
|
+
}
|
|
8501
|
+
if (!outcome.success) {
|
|
8502
|
+
console.error(outcome.message);
|
|
8503
|
+
process.exitCode = 1;
|
|
8504
|
+
return;
|
|
8505
|
+
}
|
|
8506
|
+
console.log(JSON.stringify(pruneEmpty({
|
|
8507
|
+
status: "updated",
|
|
8508
|
+
install_method: method,
|
|
8509
|
+
previous_version: result.current,
|
|
8510
|
+
new_version: result.latest,
|
|
8511
|
+
message: outcome.message
|
|
8512
|
+
}), null, 2));
|
|
8513
|
+
} catch (err) {
|
|
8514
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
8515
|
+
process.exitCode = 1;
|
|
8516
|
+
}
|
|
8517
|
+
});
|
|
7585
8518
|
}
|
|
7586
8519
|
|
|
7587
8520
|
// src/cli/user-command.ts
|
|
@@ -7768,7 +8701,7 @@ function registerChannelCommand(input) {
|
|
|
7768
8701
|
}
|
|
7769
8702
|
let userId;
|
|
7770
8703
|
if (options.user) {
|
|
7771
|
-
const resolvedUserId = await
|
|
8704
|
+
const resolvedUserId = await resolveUserId(client, options.user);
|
|
7772
8705
|
if (!resolvedUserId) {
|
|
7773
8706
|
throw new Error(`Could not resolve user: ${options.user}`);
|
|
7774
8707
|
}
|
|
@@ -7851,7 +8784,7 @@ function registerChannelCommand(input) {
|
|
|
7851
8784
|
const resolvedUserIds = [];
|
|
7852
8785
|
const unresolvedUsers = [];
|
|
7853
8786
|
for (const userInput of userInputs) {
|
|
7854
|
-
const userId = await
|
|
8787
|
+
const userId = await resolveUserId(client, userInput);
|
|
7855
8788
|
if (!userId) {
|
|
7856
8789
|
unresolvedUsers.push(userInput);
|
|
7857
8790
|
continue;
|
|
@@ -7921,6 +8854,274 @@ function registerChannelCommand(input) {
|
|
|
7921
8854
|
});
|
|
7922
8855
|
}
|
|
7923
8856
|
|
|
8857
|
+
// src/slack/workflows.ts
|
|
8858
|
+
async function listChannelWorkflows(client, channelId) {
|
|
8859
|
+
const [bookmarked, featured] = await Promise.all([
|
|
8860
|
+
listBookmarkedWorkflows(client, channelId),
|
|
8861
|
+
listFeaturedWorkflows(client, channelId)
|
|
8862
|
+
]);
|
|
8863
|
+
const featuredIds = new Set(featured.map((f) => f.trigger_id));
|
|
8864
|
+
const seen = new Set;
|
|
8865
|
+
const workflows = [];
|
|
8866
|
+
for (const bk of bookmarked) {
|
|
8867
|
+
if (bk.trigger_id) {
|
|
8868
|
+
seen.add(bk.trigger_id);
|
|
8869
|
+
}
|
|
8870
|
+
workflows.push({
|
|
8871
|
+
title: bk.title,
|
|
8872
|
+
trigger_id: bk.trigger_id ?? "",
|
|
8873
|
+
link: bk.link,
|
|
8874
|
+
app_id: bk.app_id,
|
|
8875
|
+
featured: bk.trigger_id ? featuredIds.has(bk.trigger_id) : false
|
|
8876
|
+
});
|
|
8877
|
+
}
|
|
8878
|
+
for (const ft of featured) {
|
|
8879
|
+
if (!seen.has(ft.trigger_id)) {
|
|
8880
|
+
workflows.push({
|
|
8881
|
+
title: ft.title,
|
|
8882
|
+
trigger_id: ft.trigger_id,
|
|
8883
|
+
featured: true
|
|
8884
|
+
});
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
return { channel_id: channelId, workflows };
|
|
8888
|
+
}
|
|
8889
|
+
async function listBookmarkedWorkflows(client, channelId) {
|
|
8890
|
+
const resp = await client.api("bookmarks.list", {
|
|
8891
|
+
channel_id: channelId
|
|
8892
|
+
});
|
|
8893
|
+
return asArray(resp.bookmarks).filter(isRecord).filter((b) => {
|
|
8894
|
+
const link = getString(b.link) ?? "";
|
|
8895
|
+
const shortcutId = getString(b.shortcut_id);
|
|
8896
|
+
return shortcutId || link.includes("slack.com/shortcuts/");
|
|
8897
|
+
}).map((b) => {
|
|
8898
|
+
const link = getString(b.link);
|
|
8899
|
+
const shortcutId = getString(b.shortcut_id);
|
|
8900
|
+
const triggerId = shortcutId || extractTriggerId(link);
|
|
8901
|
+
return {
|
|
8902
|
+
title: getString(b.title) ?? "",
|
|
8903
|
+
trigger_id: triggerId,
|
|
8904
|
+
link,
|
|
8905
|
+
app_id: getString(b.app_id)
|
|
8906
|
+
};
|
|
8907
|
+
});
|
|
8908
|
+
}
|
|
8909
|
+
async function listFeaturedWorkflows(client, channelId) {
|
|
8910
|
+
try {
|
|
8911
|
+
const resp = await client.api("workflows.featured.list", {
|
|
8912
|
+
channel_ids: JSON.stringify([channelId])
|
|
8913
|
+
});
|
|
8914
|
+
const entries = asArray(resp.featured_workflows).filter(isRecord);
|
|
8915
|
+
const entry = entries.find((e) => getString(e.channel_id) === channelId);
|
|
8916
|
+
if (!entry) {
|
|
8917
|
+
return [];
|
|
8918
|
+
}
|
|
8919
|
+
return asArray(entry.triggers).filter(isRecord).map((t) => ({
|
|
8920
|
+
trigger_id: getString(t.id) ?? "",
|
|
8921
|
+
title: getString(t.title) ?? ""
|
|
8922
|
+
})).filter((t) => t.trigger_id);
|
|
8923
|
+
} catch {
|
|
8924
|
+
return [];
|
|
8925
|
+
}
|
|
8926
|
+
}
|
|
8927
|
+
async function previewWorkflow(client, triggerId) {
|
|
8928
|
+
const resp = await client.api("workflows.triggers.preview", {
|
|
8929
|
+
trigger_ids: triggerId
|
|
8930
|
+
});
|
|
8931
|
+
const triggers = asArray(resp.triggers).filter(isRecord);
|
|
8932
|
+
if (triggers.length === 0) {
|
|
8933
|
+
const rejected = asArray(resp.rejected_triggers);
|
|
8934
|
+
if (rejected.length > 0) {
|
|
8935
|
+
throw new Error(`Trigger ${triggerId} was rejected — you may not have access`);
|
|
8936
|
+
}
|
|
8937
|
+
throw new Error(`No preview data returned for trigger ${triggerId}`);
|
|
8938
|
+
}
|
|
8939
|
+
const t = triggers[0];
|
|
8940
|
+
const wf = isRecord(t.workflow) ? t.workflow : {};
|
|
8941
|
+
const wfApp = isRecord(wf.app) ? wf.app : {};
|
|
8942
|
+
const details = isRecord(t.workflow_details) ? t.workflow_details : {};
|
|
8943
|
+
return {
|
|
8944
|
+
trigger_id: getString(t.id) ?? triggerId,
|
|
8945
|
+
type: getString(t.type) ?? "",
|
|
8946
|
+
name: getString(t.name) ?? "",
|
|
8947
|
+
description: getString(t.description) ?? "",
|
|
8948
|
+
shortcut_url: getString(t.shortcut_url),
|
|
8949
|
+
workflow: {
|
|
8950
|
+
id: getString(wf.workflow_id) ?? "",
|
|
8951
|
+
title: getString(wf.title) ?? "",
|
|
8952
|
+
description: getString(wf.description) ?? "",
|
|
8953
|
+
app_id: getString(wf.app_id) ?? getString(wfApp.id) ?? "",
|
|
8954
|
+
app_name: getString(wfApp.name) ?? ""
|
|
8955
|
+
},
|
|
8956
|
+
collaborators: asArray(details.collaborators).map((c) => typeof c === "string" ? c : "").filter(Boolean)
|
|
8957
|
+
};
|
|
8958
|
+
}
|
|
8959
|
+
async function runWorkflow(client, input) {
|
|
8960
|
+
const clientToken = `cli-${Date.now()}`;
|
|
8961
|
+
const resp = await client.api("workflows.triggers.trip", {
|
|
8962
|
+
url: input.shortcutUrl,
|
|
8963
|
+
client_token: clientToken,
|
|
8964
|
+
context: JSON.stringify({
|
|
8965
|
+
location: "bookmark",
|
|
8966
|
+
channel_id: input.channelId,
|
|
8967
|
+
trigger_id: input.triggerId
|
|
8968
|
+
}),
|
|
8969
|
+
run_precheck: true
|
|
8970
|
+
});
|
|
8971
|
+
return {
|
|
8972
|
+
function_execution_id: getString(resp.function_execution_id) ?? "",
|
|
8973
|
+
trigger_execution_id: getString(resp.trigger_execution_id) ?? "",
|
|
8974
|
+
is_slow_workflow: resp.is_slow_workflow === true
|
|
8975
|
+
};
|
|
8976
|
+
}
|
|
8977
|
+
async function resolveShortcutUrl(client, input) {
|
|
8978
|
+
const { channelId, triggerId } = input;
|
|
8979
|
+
const resp = await client.api("bookmarks.list", {
|
|
8980
|
+
channel_id: channelId
|
|
8981
|
+
});
|
|
8982
|
+
const bookmarks = asArray(resp.bookmarks).filter(isRecord);
|
|
8983
|
+
for (const b of bookmarks) {
|
|
8984
|
+
const shortcutId = getString(b.shortcut_id);
|
|
8985
|
+
if (shortcutId === triggerId) {
|
|
8986
|
+
const link = getString(b.link);
|
|
8987
|
+
if (link) {
|
|
8988
|
+
return link;
|
|
8989
|
+
}
|
|
8990
|
+
}
|
|
8991
|
+
}
|
|
8992
|
+
throw new Error(`Could not find shortcut URL for trigger ${triggerId} in channel bookmarks`);
|
|
8993
|
+
}
|
|
8994
|
+
async function getWorkflowSchema(client, workflowId) {
|
|
8995
|
+
const resp = await client.api("workflows.get", { workflow_id: workflowId });
|
|
8996
|
+
const wf = isRecord(resp.workflow) ? resp.workflow : null;
|
|
8997
|
+
if (!wf) {
|
|
8998
|
+
throw new Error(`No workflow found for ID ${workflowId}`);
|
|
8999
|
+
}
|
|
9000
|
+
const steps = asArray(wf.steps).filter(isRecord);
|
|
9001
|
+
const stepSummaries = [];
|
|
9002
|
+
let fields = [];
|
|
9003
|
+
let formTitle;
|
|
9004
|
+
for (const step of steps) {
|
|
9005
|
+
const fn = isRecord(step.function) ? step.function : {};
|
|
9006
|
+
const callbackId = getString(fn.callback_id) ?? "";
|
|
9007
|
+
const title = getString(fn.title) ?? callbackId;
|
|
9008
|
+
stepSummaries.push(title);
|
|
9009
|
+
if (callbackId === "open_form") {
|
|
9010
|
+
const inputs = isRecord(step.inputs) ? step.inputs : {};
|
|
9011
|
+
const titleInput = isRecord(inputs.title) ? inputs.title : {};
|
|
9012
|
+
formTitle = getString(titleInput.value);
|
|
9013
|
+
const fieldsInput = isRecord(inputs.fields) ? inputs.fields : {};
|
|
9014
|
+
const fieldsValue = isRecord(fieldsInput.value) ? fieldsInput.value : {};
|
|
9015
|
+
const elements = asArray(fieldsValue.elements).filter(isRecord);
|
|
9016
|
+
const required = new Set(asArray(fieldsValue.required).map((r) => typeof r === "string" ? r : "").filter(Boolean));
|
|
9017
|
+
fields = elements.map((el) => ({
|
|
9018
|
+
name: getString(el.name) ?? "",
|
|
9019
|
+
title: getString(el.title) ?? "",
|
|
9020
|
+
type: getString(el.type) ?? "string",
|
|
9021
|
+
description: getString(el.description) ?? "",
|
|
9022
|
+
required: required.has(getString(el.name) ?? ""),
|
|
9023
|
+
long: el.long === true ? true : undefined
|
|
9024
|
+
}));
|
|
9025
|
+
}
|
|
9026
|
+
}
|
|
9027
|
+
return {
|
|
9028
|
+
workflow_id: getString(wf.id) ?? workflowId,
|
|
9029
|
+
title: getString(wf.title) ?? "",
|
|
9030
|
+
description: getString(wf.description) ?? "",
|
|
9031
|
+
form_title: formTitle,
|
|
9032
|
+
fields,
|
|
9033
|
+
steps: stepSummaries
|
|
9034
|
+
};
|
|
9035
|
+
}
|
|
9036
|
+
function extractTriggerId(link) {
|
|
9037
|
+
if (!link) {
|
|
9038
|
+
return;
|
|
9039
|
+
}
|
|
9040
|
+
const match = link.match(/slack\.com\/shortcuts\/(Ft[A-Za-z0-9]+)/);
|
|
9041
|
+
return match?.[1];
|
|
9042
|
+
}
|
|
9043
|
+
|
|
9044
|
+
// src/cli/workflow-command.ts
|
|
9045
|
+
function registerWorkflowCommand(input) {
|
|
9046
|
+
const workflowCmd = input.program.command("workflow").description("Discover and interact with Slack workflows");
|
|
9047
|
+
workflowCmd.command("list").description("List workflows bookmarked or featured in a channel").argument("<channel>", "Channel id or name (#channel, channel, C...)").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
|
|
9048
|
+
const [channel, options] = args;
|
|
9049
|
+
try {
|
|
9050
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
9051
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
9052
|
+
workspaceUrl,
|
|
9053
|
+
work: async () => {
|
|
9054
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
9055
|
+
const channelId = await resolveChannelId(client, channel);
|
|
9056
|
+
return await listChannelWorkflows(client, channelId);
|
|
9057
|
+
}
|
|
9058
|
+
});
|
|
9059
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
9060
|
+
} catch (err) {
|
|
9061
|
+
console.error(input.ctx.errorMessage(err));
|
|
9062
|
+
process.exitCode = 1;
|
|
9063
|
+
}
|
|
9064
|
+
});
|
|
9065
|
+
workflowCmd.command("preview").description("Get workflow metadata from a trigger ID (no side effects)").argument("<trigger-id>", "Trigger ID (Ft...)").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
|
|
9066
|
+
const [triggerId, options] = args;
|
|
9067
|
+
try {
|
|
9068
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
9069
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
9070
|
+
workspaceUrl,
|
|
9071
|
+
work: async () => {
|
|
9072
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
9073
|
+
return await previewWorkflow(client, triggerId);
|
|
9074
|
+
}
|
|
9075
|
+
});
|
|
9076
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
9077
|
+
} catch (err) {
|
|
9078
|
+
console.error(input.ctx.errorMessage(err));
|
|
9079
|
+
process.exitCode = 1;
|
|
9080
|
+
}
|
|
9081
|
+
});
|
|
9082
|
+
workflowCmd.command("get").description("Get workflow definition including form fields and steps (accepts Ft... or Wf...)").argument("<id>", "Trigger ID (Ft...) or Workflow ID (Wf...)").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
|
|
9083
|
+
const [id, options] = args;
|
|
9084
|
+
try {
|
|
9085
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
9086
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
9087
|
+
workspaceUrl,
|
|
9088
|
+
work: async () => {
|
|
9089
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
9090
|
+
let workflowId = id;
|
|
9091
|
+
if (id.startsWith("Ft")) {
|
|
9092
|
+
const preview = await previewWorkflow(client, id);
|
|
9093
|
+
workflowId = preview.workflow.id;
|
|
9094
|
+
}
|
|
9095
|
+
return await getWorkflowSchema(client, workflowId);
|
|
9096
|
+
}
|
|
9097
|
+
});
|
|
9098
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
9099
|
+
} catch (err) {
|
|
9100
|
+
console.error(input.ctx.errorMessage(err));
|
|
9101
|
+
process.exitCode = 1;
|
|
9102
|
+
}
|
|
9103
|
+
});
|
|
9104
|
+
workflowCmd.command("run").description("Trip a workflow trigger").argument("<trigger-id>", "Trigger ID (Ft...)").requiredOption("--channel <id-or-name>", "Channel where the workflow is bookmarked").option("--workspace <url>", "Workspace selector (full URL or unique substring; required if you have multiple workspaces)").action(async (...args) => {
|
|
9105
|
+
const [triggerId, options] = args;
|
|
9106
|
+
try {
|
|
9107
|
+
const workspaceUrl = input.ctx.effectiveWorkspaceUrl(options.workspace);
|
|
9108
|
+
const payload = await input.ctx.withAutoRefresh({
|
|
9109
|
+
workspaceUrl,
|
|
9110
|
+
work: async () => {
|
|
9111
|
+
const { client } = await input.ctx.getClientForWorkspace(workspaceUrl);
|
|
9112
|
+
const channelId = await resolveChannelId(client, options.channel);
|
|
9113
|
+
const shortcutUrl = await resolveShortcutUrl(client, { channelId, triggerId });
|
|
9114
|
+
return await runWorkflow(client, { shortcutUrl, channelId, triggerId });
|
|
9115
|
+
}
|
|
9116
|
+
});
|
|
9117
|
+
console.log(JSON.stringify(pruneEmpty(payload), null, 2));
|
|
9118
|
+
} catch (err) {
|
|
9119
|
+
console.error(input.ctx.errorMessage(err));
|
|
9120
|
+
process.exitCode = 1;
|
|
9121
|
+
}
|
|
9122
|
+
});
|
|
9123
|
+
}
|
|
9124
|
+
|
|
7924
9125
|
// src/index.ts
|
|
7925
9126
|
var program = new Command;
|
|
7926
9127
|
program.name("agent-slack").description("Slack automation CLI for AI agents").version(getPackageVersion());
|
|
@@ -7929,9 +9130,12 @@ registerAuthCommand({ program, ctx });
|
|
|
7929
9130
|
registerMessageCommand({ program, ctx });
|
|
7930
9131
|
registerCanvasCommand({ program, ctx });
|
|
7931
9132
|
registerSearchCommand({ program, ctx });
|
|
9133
|
+
registerLaterCommand({ program, ctx });
|
|
9134
|
+
registerUnreadsCommand({ program, ctx });
|
|
7932
9135
|
registerUpdateCommand({ program });
|
|
7933
9136
|
registerUserCommand({ program, ctx });
|
|
7934
9137
|
registerChannelCommand({ program, ctx });
|
|
9138
|
+
registerWorkflowCommand({ program, ctx });
|
|
7935
9139
|
program.parse(process.argv);
|
|
7936
9140
|
if (!process.argv.slice(2).length) {
|
|
7937
9141
|
program.outputHelp();
|
|
@@ -7941,5 +9145,5 @@ if (subcommand && subcommand !== "update") {
|
|
|
7941
9145
|
backgroundUpdateCheck();
|
|
7942
9146
|
}
|
|
7943
9147
|
|
|
7944
|
-
//# debugId=
|
|
9148
|
+
//# debugId=4F295E0EDEFC23F164756E2164756E21
|
|
7945
9149
|
//# sourceMappingURL=index.js.map
|