Connect to Slack
Collect your Slack messages from the last 90 days for profile analysis
Source Code
import fs from "fs";
import path from "path";
const [outputPath = "session/writing-samples.json"] = process.argv.slice(2);
const ninetyDaysAgo = Math.floor(
(Date.now() - 90 * 24 * 60 * 60 * 1000) / 1000
);
const formatDate = (d) =>
d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
console.log("Collecting your Slack messages (last 90 days)...");
try {
// 1. Get auth, channels, DMs, and users in parallel
console.log("Fetching workspace data...");
const [authData, channelsData, dmsData, usersData] = await Promise.all([
(async () => {
const res = await fetch("https://slack.com/api/auth.test", {
headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" },
});
const data = await res.json();
if (!data.ok) throw new Error(`Auth failed: ${data.error}`);
return data;
})(),
(async () => {
const channels = [];
let cursor = null;
do {
const params = new URLSearchParams({
types: "public_channel,private_channel",
limit: "200",
exclude_archived: "true",
});
if (cursor) params.set("cursor", cursor);
const res = await fetch(
"https://slack.com/api/conversations.list?" + params,
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const data = await res.json();
if (!data.ok)
throw new Error(`conversations.list failed: ${data.error}`);
channels.push(...(data.channels || []));
cursor = data.response_metadata?.next_cursor;
} while (cursor);
return channels;
})(),
(async () => {
const res = await fetch(
"https://slack.com/api/conversations.list?" +
new URLSearchParams({
types: "im,mpim",
limit: "100",
}),
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const data = await res.json();
return data.ok ? data.channels || [] : [];
})(),
(async () => {
const users = [];
let cursor = null;
do {
const params = new URLSearchParams({ limit: "200" });
if (cursor) params.set("cursor", cursor);
const res = await fetch("https://slack.com/api/users.list?" + params, {
headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" },
});
const data = await res.json();
if (!data.ok) throw new Error(`users.list failed: ${data.error}`);
users.push(...(data.members || []));
cursor = data.response_metadata?.next_cursor;
} while (cursor);
return users;
})(),
]);
const userId = authData.user_id;
const userName = authData.user;
const workspace = authData.team;
console.log(`✓ Connected as @${userName} in ${workspace}`);
// Build user lookup
const userMap = new Map();
for (const u of usersData) {
userMap.set(u.id, {
name: u.profile?.display_name || u.real_name || u.name,
realName: u.real_name,
});
}
// Helper to expand <@USERID> references to @username
const expandUserRefs = (text) => {
if (!text) return text;
return text.replace(/<@(U[A-Z0-9]+)>/g, (match, id) => {
const user = userMap.get(id);
return user ? `@${user.name}` : match;
});
};
// Get channels user is member of
const myChannels = channelsData.filter((ch) => ch.is_member);
console.log(
` ${myChannels.length} channels, ${dmsData.length} DM contacts, ${userMap.size} users`
);
// 2. Fetch history from channels
console.log("Fetching channel message history...");
const fetchHistory = async (channelId) => {
const params = new URLSearchParams({
channel: channelId,
limit: "200",
oldest: ninetyDaysAgo.toString(),
});
const res = await fetch(
"https://slack.com/api/conversations.history?" + params,
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const data = await res.json();
return data.ok ? data.messages || [] : [];
};
// Fetch channel histories in parallel (batch to avoid rate limits)
const histories = [];
for (let i = 0; i < myChannels.length; i += 25) {
const batch = myChannels.slice(i, i + 25);
const results = await Promise.all(
batch.map(async (conv) => ({
conv,
messages: await fetchHistory(conv.id),
}))
);
histories.push(...results);
}
// 3. Extract user's messages AND mentions of user
const userMessages = [];
const mentionsOfUser = [];
const mentionedUserIds = new Set();
const mentionPattern = /<@(U[A-Z0-9]+)>/g;
const userMentionPattern = new RegExp(`<@${userId}>`, "g");
for (const { conv, messages } of histories) {
for (const msg of messages) {
if (msg.user === userId && msg.text) {
// Messages FROM the user
let match;
while ((match = mentionPattern.exec(msg.text)) !== null) {
mentionedUserIds.add(match[1]);
}
userMessages.push({
text: expandUserRefs(msg.text),
ts: msg.ts,
timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
channel: {
id: conv.id,
name: conv.name,
type: conv.is_private ? "private_channel" : "channel",
},
thread_ts: msg.thread_ts,
is_thread_reply: msg.thread_ts && msg.ts !== msg.thread_ts,
reactions:
msg.reactions?.map((r) => ({ name: r.name, count: r.count })) ||
null,
});
} else if (msg.user !== userId && msg.text?.includes(`<@${userId}>`)) {
// Mentions OF the user (others mentioning them)
mentionsOfUser.push({
text: expandUserRefs(msg.text),
from: userMap.get(msg.user)?.name || msg.user,
timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
channel: {
id: conv.id,
name: conv.name,
},
});
}
}
}
console.log(
` Found ${userMessages.length} messages from you, ${mentionsOfUser.length} mentions of you`
);
if (userMessages.length === 0) {
console.error("\n✗ No messages found from you in the last 90 days.");
console.log(
JSON.stringify({
success: false,
error: "no_messages_found",
user: userName,
workspace,
channelsChecked: myChannels.length,
})
);
process.exit(1);
}
// 4. Fetch thread context for replies (batch in parallel)
const threadReplies = userMessages.filter((m) => m.is_thread_reply);
console.log(
` ${threadReplies.length} are thread replies, fetching context...`
);
// Group by channel+thread
const threadGroups = new Map();
for (const msg of threadReplies) {
const key = `${msg.channel.id}:${msg.thread_ts}`;
if (!threadGroups.has(key)) {
threadGroups.set(key, {
channelId: msg.channel.id,
threadTs: msg.thread_ts,
});
}
}
const threadContextMap = new Map();
const threadParentAuthors = new Map(); // Track who started threads user replied to
const threadEntries = [...threadGroups.entries()];
for (let i = 0; i < threadEntries.length; i += 25) {
const batch = threadEntries.slice(i, i + 25);
const results = await Promise.all(
batch.map(async ([key, { channelId, threadTs }]) => {
const params = new URLSearchParams({
channel: channelId,
ts: threadTs,
limit: "10",
inclusive: "true",
});
const res = await fetch(
"https://slack.com/api/conversations.replies?" + params,
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const data = await res.json();
if (data.ok && data.messages?.length > 0) {
// Track thread parent author (person user is replying to)
const parentMsg = data.messages[0];
if (parentMsg.user && parentMsg.user !== userId) {
threadParentAuthors.set(key, parentMsg.user);
}
// Get up to 5 preceding messages for context
const contextMessages = data.messages.slice(0, 5).map((m) => ({
text: expandUserRefs(m.text),
user: userMap.get(m.user)?.name || m.user,
userId: m.user,
timestamp: new Date(parseFloat(m.ts) * 1000).toISOString(),
}));
return [key, contextMessages];
}
return [key, null];
})
);
for (const [key, context] of results) {
if (context) threadContextMap.set(key, context);
}
}
console.log(` Retrieved ${threadContextMap.size} thread parents`);
// 5. Build final output
const messages = userMessages.map((msg) => {
let threadContext = null;
if (msg.is_thread_reply) {
const key = `${msg.channel.id}:${msg.thread_ts}`;
const context = threadContextMap.get(key);
if (context) {
threadContext = context; // Array of preceding messages
}
}
return {
text: msg.text,
timestamp: msg.timestamp,
channel: msg.channel,
thread_context: threadContext,
reactions: msg.reactions || null,
};
});
// Sort newest first
messages.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
mentionsOfUser.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
// Build DM contacts list (we can list DMs but not read messages)
const dmContacts = dmsData
.filter((dm) => dm.user && userMap.has(dm.user))
.map((dm) => ({
id: dm.id,
userId: dm.user,
name: userMap.get(dm.user)?.name,
}));
// Build active channels map with message counts
const channelCounts = new Map();
for (const msg of userMessages) {
const key = msg.channel.id;
if (!channelCounts.has(key)) {
channelCounts.set(key, { ...msg.channel, messageCount: 0 });
}
channelCounts.get(key).messageCount++;
}
const activeChannels = [...channelCounts.values()].sort(
(a, b) => b.messageCount - a.messageCount
);
// Build collaborators with multi-signal scoring
const collaboratorScores = new Map();
const addScore = (targetUserId, signal, weight = 1) => {
if (!targetUserId || targetUserId === userId) return; // Skip self
if (!collaboratorScores.has(targetUserId)) {
collaboratorScores.set(targetUserId, {
repliedToMe: 0,
iRepliedTo: 0,
hasDM: false,
iMentioned: 0,
});
}
const scores = collaboratorScores.get(targetUserId);
if (signal === "repliedToMe") scores.repliedToMe += weight;
else if (signal === "iRepliedTo") scores.iRepliedTo += weight;
else if (signal === "hasDM") scores.hasDM = true;
else if (signal === "iMentioned") scores.iMentioned += weight;
};
// Signal 1: People who mentioned me (strong - they're engaging with me)
for (const mention of mentionsOfUser) {
const mentionerId = [...userMap.entries()].find(
([_, u]) => u.name === mention.from
)?.[0];
if (mentionerId) addScore(mentionerId, "repliedToMe");
}
// Signal 2: People whose threads I replied to (strong - I'm engaging with them)
for (const [_, parentUserId] of threadParentAuthors) {
addScore(parentUserId, "iRepliedTo");
}
// Signal 3: DM contacts (strong - we have a direct relationship)
const dmContactIds = new Set(dmsData.map((dm) => dm.user).filter(Boolean));
for (const dmUserId of dmContactIds) {
addScore(dmUserId, "hasDM");
}
// Signal 4: People I mentioned (weak - could be broadcasting)
for (const mentionedId of mentionedUserIds) {
addScore(mentionedId, "iMentioned");
}
// Build collaborators array with scores
const collaborators = [...collaboratorScores.entries()]
.filter(([id]) => userMap.has(id))
.map(([id, scores]) => ({
id,
name: userMap.get(id).name,
signals: {
theyMentionedMe: scores.repliedToMe,
iRepliedToThem: scores.iRepliedTo,
hasDM: scores.hasDM,
iMentionedThem: scores.iMentioned,
},
// Score: bidirectional interaction > unidirectional
interactionScore:
scores.repliedToMe * 3 + // They engaged with me (strong)
scores.iRepliedTo * 3 + // I engaged with them (strong)
(scores.hasDM ? 5 : 0) + // We have a direct channel (strong)
scores.iMentioned * 1, // I mentioned them (weak)
}))
.sort((a, b) => b.interactionScore - a.interactionScore);
const output = {
user: userName,
workspace,
period: `${formatDate(new Date(ninetyDaysAgo * 1000))} - ${formatDate(
new Date()
)}`,
messagesFrom: messages,
mentionsOf: mentionsOfUser,
activeChannels,
collaborators,
dmContacts,
};
// Write output
const dir = path.dirname(outputPath);
if (dir && dir !== ".") fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
console.log(
`\n✓ Collected ${messages.length} messages from, ${mentionsOfUser.length} mentions of`
);
console.log(` Output: ${outputPath}`);
console.log(
JSON.stringify({
success: true,
outputPath,
user: userName,
workspace,
messagesFromCount: messages.length,
mentionsOfCount: mentionsOfUser.length,
threadContextCount: threadContextMap.size,
})
);
} catch (error) {
console.error("Failed:", error.message);
throw error;
} import fs from "fs";
import path from "path";
const [outputPath = "session/writing-samples.json"] = process.argv.slice(2);
const ninetyDaysAgo = Math.floor(
(Date.now() - 90 * 24 * 60 * 60 * 1000) / 1000
);
const formatDate = (d) =>
d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
console.log("Collecting your Slack messages (last 90 days)...");
try {
// 1. Get auth, channels, DMs, and users in parallel
console.log("Fetching workspace data...");
const [authData, channelsData, dmsData, usersData] = await Promise.all([
(async () => {
const res = await fetch("https://slack.com/api/auth.test", {
headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" },
});
const data = await res.json();
if (!data.ok) throw new Error(`Auth failed: ${data.error}`);
return data;
})(),
(async () => {
const channels = [];
let cursor = null;
do {
const params = new URLSearchParams({
types: "public_channel,private_channel",
limit: "200",
exclude_archived: "true",
});
if (cursor) params.set("cursor", cursor);
const res = await fetch(
"https://slack.com/api/conversations.list?" + params,
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const data = await res.json();
if (!data.ok)
throw new Error(`conversations.list failed: ${data.error}`);
channels.push(...(data.channels || []));
cursor = data.response_metadata?.next_cursor;
} while (cursor);
return channels;
})(),
(async () => {
const res = await fetch(
"https://slack.com/api/conversations.list?" +
new URLSearchParams({
types: "im,mpim",
limit: "100",
}),
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const data = await res.json();
return data.ok ? data.channels || [] : [];
})(),
(async () => {
const users = [];
let cursor = null;
do {
const params = new URLSearchParams({ limit: "200" });
if (cursor) params.set("cursor", cursor);
const res = await fetch("https://slack.com/api/users.list?" + params, {
headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" },
});
const data = await res.json();
if (!data.ok) throw new Error(`users.list failed: ${data.error}`);
users.push(...(data.members || []));
cursor = data.response_metadata?.next_cursor;
} while (cursor);
return users;
})(),
]);
const userId = authData.user_id;
const userName = authData.user;
const workspace = authData.team;
console.log(`✓ Connected as @${userName} in ${workspace}`);
// Build user lookup
const userMap = new Map();
for (const u of usersData) {
userMap.set(u.id, {
name: u.profile?.display_name || u.real_name || u.name,
realName: u.real_name,
});
}
// Helper to expand <@USERID> references to @username
const expandUserRefs = (text) => {
if (!text) return text;
return text.replace(/<@(U[A-Z0-9]+)>/g, (match, id) => {
const user = userMap.get(id);
return user ? `@${user.name}` : match;
});
};
// Get channels user is member of
const myChannels = channelsData.filter((ch) => ch.is_member);
console.log(
` ${myChannels.length} channels, ${dmsData.length} DM contacts, ${userMap.size} users`
);
// 2. Fetch history from channels
console.log("Fetching channel message history...");
const fetchHistory = async (channelId) => {
const params = new URLSearchParams({
channel: channelId,
limit: "200",
oldest: ninetyDaysAgo.toString(),
});
const res = await fetch(
"https://slack.com/api/conversations.history?" + params,
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const data = await res.json();
return data.ok ? data.messages || [] : [];
};
// Fetch channel histories in parallel (batch to avoid rate limits)
const histories = [];
for (let i = 0; i < myChannels.length; i += 25) {
const batch = myChannels.slice(i, i + 25);
const results = await Promise.all(
batch.map(async (conv) => ({
conv,
messages: await fetchHistory(conv.id),
}))
);
histories.push(...results);
}
// 3. Extract user's messages AND mentions of user
const userMessages = [];
const mentionsOfUser = [];
const mentionedUserIds = new Set();
const mentionPattern = /<@(U[A-Z0-9]+)>/g;
const userMentionPattern = new RegExp(`<@${userId}>`, "g");
for (const { conv, messages } of histories) {
for (const msg of messages) {
if (msg.user === userId && msg.text) {
// Messages FROM the user
let match;
while ((match = mentionPattern.exec(msg.text)) !== null) {
mentionedUserIds.add(match[1]);
}
userMessages.push({
text: expandUserRefs(msg.text),
ts: msg.ts,
timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
channel: {
id: conv.id,
name: conv.name,
type: conv.is_private ? "private_channel" : "channel",
},
thread_ts: msg.thread_ts,
is_thread_reply: msg.thread_ts && msg.ts !== msg.thread_ts,
reactions:
msg.reactions?.map((r) => ({ name: r.name, count: r.count })) ||
null,
});
} else if (msg.user !== userId && msg.text?.includes(`<@${userId}>`)) {
// Mentions OF the user (others mentioning them)
mentionsOfUser.push({
text: expandUserRefs(msg.text),
from: userMap.get(msg.user)?.name || msg.user,
timestamp: new Date(parseFloat(msg.ts) * 1000).toISOString(),
channel: {
id: conv.id,
name: conv.name,
},
});
}
}
}
console.log(
` Found ${userMessages.length} messages from you, ${mentionsOfUser.length} mentions of you`
);
if (userMessages.length === 0) {
console.error("\n✗ No messages found from you in the last 90 days.");
console.log(
JSON.stringify({
success: false,
error: "no_messages_found",
user: userName,
workspace,
channelsChecked: myChannels.length,
})
);
process.exit(1);
}
// 4. Fetch thread context for replies (batch in parallel)
const threadReplies = userMessages.filter((m) => m.is_thread_reply);
console.log(
` ${threadReplies.length} are thread replies, fetching context...`
);
// Group by channel+thread
const threadGroups = new Map();
for (const msg of threadReplies) {
const key = `${msg.channel.id}:${msg.thread_ts}`;
if (!threadGroups.has(key)) {
threadGroups.set(key, {
channelId: msg.channel.id,
threadTs: msg.thread_ts,
});
}
}
const threadContextMap = new Map();
const threadParentAuthors = new Map(); // Track who started threads user replied to
const threadEntries = [...threadGroups.entries()];
for (let i = 0; i < threadEntries.length; i += 25) {
const batch = threadEntries.slice(i, i + 25);
const results = await Promise.all(
batch.map(async ([key, { channelId, threadTs }]) => {
const params = new URLSearchParams({
channel: channelId,
ts: threadTs,
limit: "10",
inclusive: "true",
});
const res = await fetch(
"https://slack.com/api/conversations.replies?" + params,
{ headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
);
const data = await res.json();
if (data.ok && data.messages?.length > 0) {
// Track thread parent author (person user is replying to)
const parentMsg = data.messages[0];
if (parentMsg.user && parentMsg.user !== userId) {
threadParentAuthors.set(key, parentMsg.user);
}
// Get up to 5 preceding messages for context
const contextMessages = data.messages.slice(0, 5).map((m) => ({
text: expandUserRefs(m.text),
user: userMap.get(m.user)?.name || m.user,
userId: m.user,
timestamp: new Date(parseFloat(m.ts) * 1000).toISOString(),
}));
return [key, contextMessages];
}
return [key, null];
})
);
for (const [key, context] of results) {
if (context) threadContextMap.set(key, context);
}
}
console.log(` Retrieved ${threadContextMap.size} thread parents`);
// 5. Build final output
const messages = userMessages.map((msg) => {
let threadContext = null;
if (msg.is_thread_reply) {
const key = `${msg.channel.id}:${msg.thread_ts}`;
const context = threadContextMap.get(key);
if (context) {
threadContext = context; // Array of preceding messages
}
}
return {
text: msg.text,
timestamp: msg.timestamp,
channel: msg.channel,
thread_context: threadContext,
reactions: msg.reactions || null,
};
});
// Sort newest first
messages.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
mentionsOfUser.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
// Build DM contacts list (we can list DMs but not read messages)
const dmContacts = dmsData
.filter((dm) => dm.user && userMap.has(dm.user))
.map((dm) => ({
id: dm.id,
userId: dm.user,
name: userMap.get(dm.user)?.name,
}));
// Build active channels map with message counts
const channelCounts = new Map();
for (const msg of userMessages) {
const key = msg.channel.id;
if (!channelCounts.has(key)) {
channelCounts.set(key, { ...msg.channel, messageCount: 0 });
}
channelCounts.get(key).messageCount++;
}
const activeChannels = [...channelCounts.values()].sort(
(a, b) => b.messageCount - a.messageCount
);
// Build collaborators with multi-signal scoring
const collaboratorScores = new Map();
const addScore = (targetUserId, signal, weight = 1) => {
if (!targetUserId || targetUserId === userId) return; // Skip self
if (!collaboratorScores.has(targetUserId)) {
collaboratorScores.set(targetUserId, {
repliedToMe: 0,
iRepliedTo: 0,
hasDM: false,
iMentioned: 0,
});
}
const scores = collaboratorScores.get(targetUserId);
if (signal === "repliedToMe") scores.repliedToMe += weight;
else if (signal === "iRepliedTo") scores.iRepliedTo += weight;
else if (signal === "hasDM") scores.hasDM = true;
else if (signal === "iMentioned") scores.iMentioned += weight;
};
// Signal 1: People who mentioned me (strong - they're engaging with me)
for (const mention of mentionsOfUser) {
const mentionerId = [...userMap.entries()].find(
([_, u]) => u.name === mention.from
)?.[0];
if (mentionerId) addScore(mentionerId, "repliedToMe");
}
// Signal 2: People whose threads I replied to (strong - I'm engaging with them)
for (const [_, parentUserId] of threadParentAuthors) {
addScore(parentUserId, "iRepliedTo");
}
// Signal 3: DM contacts (strong - we have a direct relationship)
const dmContactIds = new Set(dmsData.map((dm) => dm.user).filter(Boolean));
for (const dmUserId of dmContactIds) {
addScore(dmUserId, "hasDM");
}
// Signal 4: People I mentioned (weak - could be broadcasting)
for (const mentionedId of mentionedUserIds) {
addScore(mentionedId, "iMentioned");
}
// Build collaborators array with scores
const collaborators = [...collaboratorScores.entries()]
.filter(([id]) => userMap.has(id))
.map(([id, scores]) => ({
id,
name: userMap.get(id).name,
signals: {
theyMentionedMe: scores.repliedToMe,
iRepliedToThem: scores.iRepliedTo,
hasDM: scores.hasDM,
iMentionedThem: scores.iMentioned,
},
// Score: bidirectional interaction > unidirectional
interactionScore:
scores.repliedToMe * 3 + // They engaged with me (strong)
scores.iRepliedTo * 3 + // I engaged with them (strong)
(scores.hasDM ? 5 : 0) + // We have a direct channel (strong)
scores.iMentioned * 1, // I mentioned them (weak)
}))
.sort((a, b) => b.interactionScore - a.interactionScore);
const output = {
user: userName,
workspace,
period: `${formatDate(new Date(ninetyDaysAgo * 1000))} - ${formatDate(
new Date()
)}`,
messagesFrom: messages,
mentionsOf: mentionsOfUser,
activeChannels,
collaborators,
dmContacts,
};
// Write output
const dir = path.dirname(outputPath);
if (dir && dir !== ".") fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
console.log(
`\n✓ Collected ${messages.length} messages from, ${mentionsOfUser.length} mentions of`
);
console.log(` Output: ${outputPath}`);
console.log(
JSON.stringify({
success: true,
outputPath,
user: userName,
workspace,
messagesFromCount: messages.length,
mentionsOfCount: mentionsOfUser.length,
threadContextCount: threadContextMap.size,
})
);
} catch (error) {
console.error("Failed:", error.message);
throw error;
}