@meetopenbot/slack 0.0.2 → 0.0.4

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,10 +4,12 @@ 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";
10
11
  const ROUTING_AGENT_ID = "system";
12
+ const SLACK_USER_DISPLAY_NAME = "Slack user";
11
13
  function isSlackWebhookIngress(meta) {
12
14
  return meta?.source === "slack";
13
15
  }
@@ -79,6 +81,10 @@ async function* bridgeSystemAgentRun(args) {
79
81
  threadId: args.threadId,
80
82
  source: "slack",
81
83
  slack: slackMeta,
84
+ userName: SLACK_USER_DISPLAY_NAME,
85
+ ...(typeof args.meta?.userId === "string"
86
+ ? { userId: args.meta.userId }
87
+ : {}),
82
88
  },
83
89
  },
84
90
  onEvent: async (outEvent) => {
@@ -267,6 +273,8 @@ export default definePlugin({
267
273
  threadId,
268
274
  source: "slack",
269
275
  slack: slackMeta,
276
+ userName: SLACK_USER_DISPLAY_NAME,
277
+ ...(slackEvent.user ? { userId: slackEvent.user } : {}),
270
278
  },
271
279
  };
272
280
  });
@@ -327,12 +335,27 @@ export default definePlugin({
327
335
  : ctx.state.channelId;
328
336
  if (!channelId)
329
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
+ }
330
353
  try {
331
354
  yield* bridgeSystemAgentRun({
332
355
  host,
333
356
  channelId,
334
357
  threadId,
335
- userMessage,
358
+ userMessage: prompt,
336
359
  meta: event.meta,
337
360
  publicBaseUrl,
338
361
  });
@@ -0,0 +1,90 @@
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 formatContextMessages(messages, userNames) {
50
+ return messages.map((message) => {
51
+ const author = message.user
52
+ ? (userNames.get(message.user) ?? message.user)
53
+ : "unknown";
54
+ const when = formatSlackTimestamp(message.ts);
55
+ const prefix = when ? `[${when}] ${author}` : author;
56
+ return `${prefix}: ${message.text?.trim() ?? ""}`;
57
+ });
58
+ }
59
+ async function fetchThreadMessages(token, channel, threadTs, limit = DEFAULT_THREAD_REPLY_LIMIT) {
60
+ const body = await slackApiGet(token, "conversations.replies", { channel, ts: threadTs, limit });
61
+ return (body.messages ?? []).filter(isContextMessage);
62
+ }
63
+ async function fetchChannelMessagesBefore(token, channel, latestTs, limit = DEFAULT_CHANNEL_HISTORY_LIMIT) {
64
+ const body = await slackApiGet(token, "conversations.history", { channel, latest: latestTs, inclusive: false, limit });
65
+ return (body.messages ?? [])
66
+ .filter(isContextMessage)
67
+ .reverse();
68
+ }
69
+ export async function buildPromptWithSlackContext(args) {
70
+ const messages = args.threadTs
71
+ ? await fetchThreadMessages(args.token, args.channel, args.threadTs)
72
+ : await fetchChannelMessagesBefore(args.token, args.channel, args.messageTs, args.channelHistoryLimit ?? DEFAULT_CHANNEL_HISTORY_LIMIT);
73
+ if (messages.length === 0) {
74
+ return args.userMessage;
75
+ }
76
+ const userNames = await resolveUserDisplayNames(args.token, messages.flatMap((message) => (message.user ? [message.user] : [])));
77
+ const formattedMessages = formatContextMessages(messages, userNames);
78
+ const contextLabel = args.threadTs
79
+ ? "Slack thread context"
80
+ : "Recent Slack channel messages";
81
+ return [
82
+ `## ${contextLabel}`,
83
+ `Channel: ${args.channel}`,
84
+ "",
85
+ ...formattedMessages,
86
+ "",
87
+ "## User request",
88
+ args.userMessage,
89
+ ].join("\n");
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meetopenbot/slack",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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",