@meetopenbot/slack 0.0.3 → 0.0.5

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 CHANGED
@@ -4,7 +4,7 @@ Slack specialist agent for OpenBot: Events API ingress, simple LLM replies, and
4
4
 
5
5
  ## Features (MVP)
6
6
 
7
- - **Ingress** — verify Slack webhooks, dedupe events, turn messages into `agent:invoke`
7
+ - **Ingress** — verify Slack webhooks, dedupe events, prefetch thread/channel context, turn messages into `agent:invoke`
8
8
  - **Agent** — OpenAI + Slack MCP tools (`post_message`, `list_channels`, etc.)
9
9
  - **Egress** — post inbound-thread replies via `chat.postMessage`
10
10
 
@@ -12,7 +12,8 @@ Slack specialist agent for OpenBot: Events API ingress, simple LLM replies, and
12
12
 
13
13
  1. Create a Slack app with bot scopes:
14
14
 
15
- - `app_mentions:read`, `chat:write`
15
+ - `app_mentions:read`, `chat:write`, `channels:history`, `users:read`
16
+ - For private channels: `groups:history` (bot must be invited)
16
17
  - For DMs: `im:history`, `im:read`, `im:write`
17
18
 
18
19
  2. Add the plugin to `~/.openbot/agents/slack/AGENT.md`:
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { isCloudMode } from "./cloud-mode.js";
4
4
  import { CREDITS_NOT_CONFIGURED_MESSAGE, creditsErrorMessage, resolveCreditsAuthConfig, } from "./credits-auth.js";
5
5
  import { DEFAULT_OPENBOT_CHANNEL_ID, formatMissingCredentials, readSlackConfig, resolveOpenBotChannelId, resolveSlackCredentials, } from "./config.js";
6
6
  import { resolveModelConfigField } from "./model-registry.js";
7
+ import { buildPromptWithSlackContext } from "./slack-context.js";
7
8
  import { postSlackMessage } from "./slack-post.js";
8
9
  import { runSlackAgent } from "./slack-agent.js";
9
10
  import { verifySlackSignature } from "./slack-verify.js";
@@ -334,12 +335,27 @@ export default definePlugin({
334
335
  : ctx.state.channelId;
335
336
  if (!channelId)
336
337
  return;
338
+ let prompt = userMessage;
339
+ if (slackMeta?.channel && slackMeta.ts) {
340
+ try {
341
+ prompt = await buildPromptWithSlackContext({
342
+ token: auth.credentials.botToken,
343
+ channel: slackMeta.channel,
344
+ messageTs: slackMeta.ts,
345
+ threadTs: slackMeta.threadTs,
346
+ userMessage,
347
+ });
348
+ }
349
+ catch {
350
+ // Fall back to the raw mention if Slack history is unavailable.
351
+ }
352
+ }
337
353
  try {
338
354
  yield* bridgeSystemAgentRun({
339
355
  host,
340
356
  channelId,
341
357
  threadId,
342
- userMessage,
358
+ userMessage: prompt,
343
359
  meta: event.meta,
344
360
  publicBaseUrl,
345
361
  });
@@ -0,0 +1,118 @@
1
+ const DEFAULT_CHANNEL_HISTORY_LIMIT = 20;
2
+ const DEFAULT_THREAD_REPLY_LIMIT = 100;
3
+ async function slackApiGet(token, method, params) {
4
+ const searchParams = new URLSearchParams();
5
+ for (const [key, value] of Object.entries(params)) {
6
+ if (value !== undefined)
7
+ searchParams.set(key, String(value));
8
+ }
9
+ const res = await fetch(`https://slack.com/api/${method}?${searchParams}`, {
10
+ headers: { Authorization: `Bearer ${token}` },
11
+ });
12
+ const body = (await res.json());
13
+ if (!body.ok) {
14
+ throw new Error(body.error ?? `Slack API ${method} failed (${res.status})`);
15
+ }
16
+ return body;
17
+ }
18
+ function isContextMessage(message) {
19
+ if (message.bot_id)
20
+ return false;
21
+ if (message.subtype && message.subtype !== "thread_broadcast")
22
+ return false;
23
+ return Boolean(message.text?.trim());
24
+ }
25
+ async function resolveUserDisplayNames(token, userIds) {
26
+ const names = new Map();
27
+ const uniqueIds = [...new Set(userIds)];
28
+ await Promise.all(uniqueIds.map(async (userId) => {
29
+ try {
30
+ const body = await slackApiGet(token, "users.info", { user: userId });
31
+ names.set(userId, body.user?.real_name?.trim() ||
32
+ body.user?.name?.trim() ||
33
+ userId);
34
+ }
35
+ catch {
36
+ names.set(userId, userId);
37
+ }
38
+ }));
39
+ return names;
40
+ }
41
+ function formatSlackTimestamp(ts) {
42
+ if (!ts)
43
+ return "";
44
+ const seconds = Number(ts.split(".")[0]);
45
+ if (!Number.isFinite(seconds))
46
+ return "";
47
+ return new Date(seconds * 1000).toISOString();
48
+ }
49
+ function escapeXml(text) {
50
+ return text
51
+ .replace(/&/g, "&")
52
+ .replace(/</g, "&lt;")
53
+ .replace(/>/g, "&gt;")
54
+ .replace(/"/g, "&quot;");
55
+ }
56
+ function formatContextMessages(messages, userNames) {
57
+ return messages.map((message) => {
58
+ const author = message.user
59
+ ? (userNames.get(message.user) ?? message.user)
60
+ : "unknown";
61
+ const when = formatSlackTimestamp(message.ts);
62
+ const attrs = [
63
+ `author="${escapeXml(author)}"`,
64
+ when ? `timestamp="${escapeXml(when)}"` : null,
65
+ ]
66
+ .filter(Boolean)
67
+ .join(" ");
68
+ return ` <message ${attrs}>${escapeXml(message.text?.trim() ?? "")}</message>`;
69
+ });
70
+ }
71
+ function formatPromptWithSlackContext(args) {
72
+ const sourceNote = args.source === "thread"
73
+ ? "Messages from the Slack thread the user mentioned the bot in."
74
+ : "Recent messages from the Slack channel before the user's mention.";
75
+ return [
76
+ `<slack_context`,
77
+ ` channel="${escapeXml(args.channel)}"`,
78
+ ` source="${args.source}"`,
79
+ ` message_count="${args.messageCount}"`,
80
+ ` note="Background context fetched from Slack. Summarized conversation history — not the user's direct request.">`,
81
+ ` <description>${escapeXml(sourceNote)}</description>`,
82
+ ` <messages>`,
83
+ ...args.contextMessages,
84
+ ` </messages>`,
85
+ `</slack_context>`,
86
+ ``,
87
+ `<user_request>`,
88
+ escapeXml(args.userMessage),
89
+ `</user_request>`,
90
+ ].join("\n");
91
+ }
92
+ async function fetchThreadMessages(token, channel, threadTs, limit = DEFAULT_THREAD_REPLY_LIMIT) {
93
+ const body = await slackApiGet(token, "conversations.replies", { channel, ts: threadTs, limit });
94
+ return (body.messages ?? []).filter(isContextMessage);
95
+ }
96
+ async function fetchChannelMessagesBefore(token, channel, latestTs, limit = DEFAULT_CHANNEL_HISTORY_LIMIT) {
97
+ const body = await slackApiGet(token, "conversations.history", { channel, latest: latestTs, inclusive: false, limit });
98
+ return (body.messages ?? [])
99
+ .filter(isContextMessage)
100
+ .reverse();
101
+ }
102
+ export async function buildPromptWithSlackContext(args) {
103
+ const messages = args.threadTs
104
+ ? await fetchThreadMessages(args.token, args.channel, args.threadTs)
105
+ : await fetchChannelMessagesBefore(args.token, args.channel, args.messageTs, args.channelHistoryLimit ?? DEFAULT_CHANNEL_HISTORY_LIMIT);
106
+ if (messages.length === 0) {
107
+ return args.userMessage;
108
+ }
109
+ const userNames = await resolveUserDisplayNames(args.token, messages.flatMap((message) => (message.user ? [message.user] : [])));
110
+ const formattedMessages = formatContextMessages(messages, userNames);
111
+ return formatPromptWithSlackContext({
112
+ channel: args.channel,
113
+ source: args.threadTs ? "thread" : "channel",
114
+ messageCount: messages.length,
115
+ contextMessages: formattedMessages,
116
+ userMessage: args.userMessage,
117
+ });
118
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/slack",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Slack Events API ingress, simple agent replies, and thread delivery for OpenBot",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",