@meetopenbot/slack 0.0.1

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 ADDED
@@ -0,0 +1,64 @@
1
+ # @meetopenbot/slack
2
+
3
+ Slack specialist agent for OpenBot: Events API ingress, simple LLM replies, and thread delivery.
4
+
5
+ ## Features (MVP)
6
+
7
+ - **Ingress** — verify Slack webhooks, dedupe events, turn messages into `agent:invoke`
8
+ - **Agent** — OpenAI + Slack MCP tools (`post_message`, `list_channels`, etc.)
9
+ - **Egress** — post inbound-thread replies via `chat.postMessage`
10
+
11
+ ## Setup
12
+
13
+ 1. Create a Slack app with bot scopes:
14
+
15
+ - `app_mentions:read`, `chat:write`
16
+ - For DMs: `im:history`, `im:read`, `im:write`
17
+
18
+ 2. Add the plugin to `~/.openbot/agents/slack/AGENT.md`:
19
+
20
+ ```yaml
21
+ plugins:
22
+ - id: "@meetopenbot/slack"
23
+ config:
24
+ signingSecret: your-signing-secret
25
+ botToken: xoxb-your-bot-token
26
+ teamId: T01234567
27
+ openaiApiKey: sk-...
28
+ model: gpt-4o-mini
29
+ channelMappings: "C01234567:engineering,C89ABCDEF:support"
30
+ ```
31
+
32
+ ### Channel routing (webhook ingress)
33
+
34
+ When a Slack message arrives via the Events API webhook, the plugin delegates to the OpenBot **system** agent in a target channel. Set `channelMappings` as comma-separated `slackChannelId:openbotChannelId` pairs:
35
+
36
+ ```
37
+ C01234567:engineering,C89ABCDEF:support,D0789ABC:slack-dms
38
+ ```
39
+
40
+ - **Left side** — Slack channel id (`C…` for channels, `D…` for DMs). In Slack: right-click the channel → **View channel details** → copy the id at the bottom.
41
+ - **Right side** — OpenBot channel id to run in (must already exist).
42
+
43
+ Slack channels without a mapping route to **`uncategorized`**.
44
+
45
+ This applies only to **webhook ingress delegation**, not Slack MCP tools.
46
+
47
+ 3. Point Slack **Event Subscriptions** request URL to:
48
+
49
+ ```
50
+ https://<your-host>/api/webhooks/slack
51
+ ```
52
+
53
+ 4. Subscribe to **`app_mention`** for channel mentions. Add **`message.im`** only if you want DMs.
54
+
55
+ Do **not** subscribe to `message.channels` or `message.groups` — that causes duplicate replies alongside `app_mention`.
56
+
57
+ ## Build
58
+
59
+ ```bash
60
+ pnpm install
61
+ pnpm build
62
+ ```
63
+
64
+ Restart the OpenBot runtime to load the plugin.
package/dist/config.js ADDED
@@ -0,0 +1,77 @@
1
+ export const DEFAULT_OPENBOT_CHANNEL_ID = "uncategorized";
2
+ /** Parse `C0123:engineering,C0456:support` into a lookup index. */
3
+ export function buildSlackChannelMappingIndex(config) {
4
+ const index = new Map();
5
+ const raw = typeof config.channelMappings === "string"
6
+ ? config.channelMappings.trim()
7
+ : "";
8
+ if (!raw)
9
+ return index;
10
+ for (const segment of raw.split(",")) {
11
+ const trimmed = segment.trim();
12
+ if (!trimmed)
13
+ continue;
14
+ const colon = trimmed.indexOf(":");
15
+ if (colon <= 0 || colon === trimmed.length - 1)
16
+ continue;
17
+ const slackChannelId = trimmed.slice(0, colon).trim();
18
+ const channelId = trimmed.slice(colon + 1).trim();
19
+ if (!slackChannelId || !channelId)
20
+ continue;
21
+ index.set(slackChannelId, channelId);
22
+ }
23
+ return index;
24
+ }
25
+ /** Map a Slack channel id from webhook ingress to an OpenBot channel id. */
26
+ export function resolveOpenBotChannelId(slackChannelId, config) {
27
+ const normalized = slackChannelId.trim();
28
+ if (!normalized)
29
+ return DEFAULT_OPENBOT_CHANNEL_ID;
30
+ const mapped = buildSlackChannelMappingIndex(config).get(normalized);
31
+ return mapped ?? DEFAULT_OPENBOT_CHANNEL_ID;
32
+ }
33
+ function varValue(variables, key) {
34
+ const variable = variables[key];
35
+ return typeof variable === "string" ? variable : variable?.value;
36
+ }
37
+ export async function resolveSlackCredentials(config, storage, options) {
38
+ const requireOpenAi = options?.requireOpenAi ?? true;
39
+ const variables = await storage.getVariables();
40
+ const resolve = (configKey, envKey) => {
41
+ const fromConfig = config[configKey];
42
+ if (typeof fromConfig === "string" && fromConfig.trim()) {
43
+ return fromConfig.trim();
44
+ }
45
+ if (process.env[envKey]?.trim())
46
+ return process.env[envKey].trim();
47
+ return varValue(variables, envKey)?.trim();
48
+ };
49
+ const signingSecret = (typeof config.signingSecret === "string" && config.signingSecret.trim()) ||
50
+ process.env.SLACK_SIGNING_SECRET?.trim() ||
51
+ varValue(variables, "SLACK_SIGNING_SECRET") ||
52
+ "";
53
+ const botToken = resolve("botToken", "SLACK_BOT_TOKEN");
54
+ const teamId = resolve("teamId", "SLACK_TEAM_ID");
55
+ const openaiApiKey = resolve("openaiApiKey", "OPENAI_API_KEY");
56
+ const model = resolve("model", "OPENAI_MODEL") ?? "gpt-4o-mini";
57
+ const missing = [];
58
+ if (!botToken)
59
+ missing.push("botToken");
60
+ if (!teamId)
61
+ missing.push("teamId");
62
+ if (requireOpenAi && !openaiApiKey)
63
+ missing.push("openaiApiKey");
64
+ if (missing.length > 0) {
65
+ return { ok: false, missing };
66
+ }
67
+ return {
68
+ ok: true,
69
+ credentials: {
70
+ signingSecret,
71
+ botToken: botToken,
72
+ teamId: teamId,
73
+ openaiApiKey: openaiApiKey ?? "",
74
+ model,
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,419 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { agentOutput, definePlugin, decodeWebhookRawBody, getWebhookHeader, shouldHandleInvoke, webhookHttpResponse, } from "@meetopenbot/plugin-sdk";
3
+ import { DEFAULT_OPENBOT_CHANNEL_ID, resolveOpenBotChannelId, resolveSlackCredentials, } from "./config.js";
4
+ import { postSlackMessage } from "./slack-post.js";
5
+ import { runSlackAgent } from "./slack-agent.js";
6
+ import { verifySlackSignature } from "./slack-verify.js";
7
+ const ROUTING_AGENT_ID = "system";
8
+ function isSlackWebhookIngress(meta) {
9
+ return meta?.source === "slack";
10
+ }
11
+ const seenSlackEvents = new Map();
12
+ const SLACK_EVENT_DEDUP_MS = 5 * 60 * 1000;
13
+ function purgeStaleSlackEvents(now) {
14
+ for (const [id, at] of seenSlackEvents) {
15
+ if (now - at > SLACK_EVENT_DEDUP_MS)
16
+ seenSlackEvents.delete(id);
17
+ }
18
+ }
19
+ function markSlackEventSeen(dedupKey) {
20
+ const now = Date.now();
21
+ purgeStaleSlackEvents(now);
22
+ if (seenSlackEvents.has(dedupKey))
23
+ return true;
24
+ seenSlackEvents.set(dedupKey, now);
25
+ return false;
26
+ }
27
+ function slackEventDedupKey(payload, slackEvent) {
28
+ if (payload.event_id)
29
+ return payload.event_id;
30
+ return `${slackEvent.channel}:${slackEvent.ts}`;
31
+ }
32
+ /** MVP: channel mentions via app_mention; DMs via message.im only. */
33
+ function shouldProcessSlackEvent(slackEvent) {
34
+ if (slackEvent.bot_id)
35
+ return false;
36
+ if (slackEvent.subtype)
37
+ return false;
38
+ if (slackEvent.type === "app_mention")
39
+ return true;
40
+ if (slackEvent.type === "message" && slackEvent.channel_type === "im") {
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ function normalizeSlackText(text) {
46
+ return text.replace(/<@[^>]+>/g, "").replace(/\s+/g, " ").trim();
47
+ }
48
+ function getSlackThreadMeta(meta) {
49
+ const slack = meta?.slack;
50
+ if (!slack || typeof slack !== "object")
51
+ return undefined;
52
+ return slack;
53
+ }
54
+ function formatMissingCredentials(missing) {
55
+ const lines = [
56
+ "Slack agent setup is incomplete. Configure the following in plugin config or environment variables:",
57
+ ];
58
+ if (missing.includes("botToken")) {
59
+ lines.push("- `botToken` / `SLACK_BOT_TOKEN` — Slack bot OAuth token (`xoxb-…`)");
60
+ }
61
+ if (missing.includes("teamId")) {
62
+ lines.push("- `teamId` / `SLACK_TEAM_ID` — workspace team id (`T…`)");
63
+ }
64
+ if (missing.includes("openaiApiKey")) {
65
+ lines.push("- `openaiApiKey` / `OPENAI_API_KEY` — OpenAI API key for the agent loop");
66
+ }
67
+ return lines.join("\n");
68
+ }
69
+ async function* bridgeSystemAgentRun(args) {
70
+ const runId = `slack_${randomUUID()}`;
71
+ const slackMeta = getSlackThreadMeta(args.meta);
72
+ const eventQueue = [];
73
+ let resolveNext = null;
74
+ let isFinished = false;
75
+ const runPromise = args.host
76
+ .runAgent({
77
+ runId,
78
+ agentId: ROUTING_AGENT_ID,
79
+ publicBaseUrl: args.publicBaseUrl,
80
+ // Runtime's webhook ingress handles persistence.
81
+ persistEvents: false,
82
+ event: {
83
+ type: "agent:invoke",
84
+ data: {
85
+ agentId: ROUTING_AGENT_ID,
86
+ role: "user",
87
+ content: args.userMessage,
88
+ },
89
+ meta: {
90
+ channelId: args.channelId,
91
+ threadId: args.threadId,
92
+ source: "slack",
93
+ slack: slackMeta,
94
+ },
95
+ },
96
+ onEvent: async (outEvent) => {
97
+ if (outEvent.type === "agent:output" &&
98
+ !outEvent.meta?.parentToolCallId) {
99
+ const output = outEvent;
100
+ eventQueue.push({
101
+ ...output,
102
+ meta: {
103
+ ...output.meta,
104
+ agentId: ROUTING_AGENT_ID,
105
+ threadId: args.threadId,
106
+ source: "slack",
107
+ slack: slackMeta,
108
+ },
109
+ });
110
+ }
111
+ if (resolveNext) {
112
+ resolveNext();
113
+ resolveNext = null;
114
+ }
115
+ },
116
+ })
117
+ .catch((error) => {
118
+ const message = error instanceof Error ? error.message : String(error);
119
+ eventQueue.push({
120
+ type: "agent:output",
121
+ data: { content: `Slack agent error: ${message}` },
122
+ meta: {
123
+ agentId: ROUTING_AGENT_ID,
124
+ threadId: args.threadId,
125
+ source: "slack",
126
+ slack: slackMeta,
127
+ },
128
+ });
129
+ })
130
+ .finally(() => {
131
+ isFinished = true;
132
+ if (resolveNext) {
133
+ resolveNext();
134
+ resolveNext = null;
135
+ }
136
+ });
137
+ while (!isFinished || eventQueue.length > 0) {
138
+ if (eventQueue.length === 0) {
139
+ await new Promise((resolve) => {
140
+ resolveNext = resolve;
141
+ });
142
+ }
143
+ while (eventQueue.length > 0) {
144
+ yield eventQueue.shift();
145
+ }
146
+ }
147
+ await runPromise;
148
+ }
149
+ const slackPluginConfigSchema = {
150
+ type: "object",
151
+ properties: {
152
+ signingSecret: {
153
+ type: "string",
154
+ description: "Slack app signing secret (Basic Information → App Credentials)",
155
+ format: "password",
156
+ },
157
+ botToken: {
158
+ type: "string",
159
+ description: "Slack bot token (OAuth & Permissions → Bot User OAuth Token)",
160
+ format: "password",
161
+ },
162
+ teamId: {
163
+ type: "string",
164
+ description: "Slack workspace team id (starts with T)",
165
+ },
166
+ channelMappings: {
167
+ type: "string",
168
+ description: "Webhook ingress routing. Comma-separated slackChannelId:openbotChannelId pairs (e.g. C01234567:engineering,C89ABCDEF:support). Unmapped Slack channels use uncategorized.",
169
+ },
170
+ openaiApiKey: {
171
+ type: "string",
172
+ description: "OpenAI API key for direct Slack agent invocations",
173
+ format: "password",
174
+ },
175
+ model: {
176
+ type: "string",
177
+ description: "OpenAI model id for direct Slack agent invocations (default: gpt-4o-mini)",
178
+ default: "gpt-4o-mini",
179
+ },
180
+ },
181
+ required: ["signingSecret"],
182
+ };
183
+ export default definePlugin({
184
+ name: "Slack",
185
+ description: "Slack Events API ingress, local agent runtime, and system delegation for webhooks",
186
+ configSchema: slackPluginConfigSchema,
187
+ factory: (pluginContext) => (builder) => {
188
+ const { agentId, config, storage } = pluginContext;
189
+ const slackConfig = config;
190
+ const host = pluginContext.host;
191
+ const publicBaseUrl = pluginContext.publicBaseUrl ?? "";
192
+ builder.on("action:webhook", async function* (event, ctx) {
193
+ const webhook = event;
194
+ if (webhook.data.provider !== "slack")
195
+ return;
196
+ const auth = await resolveSlackCredentials(slackConfig, storage);
197
+ const signingSecret = auth.ok
198
+ ? auth.credentials.signingSecret
199
+ : String(config.signingSecret ?? "").trim();
200
+ const rawBody = decodeWebhookRawBody(webhook);
201
+ let payload;
202
+ try {
203
+ payload = JSON.parse(rawBody.toString("utf8"));
204
+ }
205
+ catch {
206
+ yield webhookHttpResponse({
207
+ status: 400,
208
+ body: { error: "invalid json" },
209
+ });
210
+ return;
211
+ }
212
+ if (payload.type === "url_verification") {
213
+ yield webhookHttpResponse({
214
+ status: 200,
215
+ body: { challenge: payload.challenge },
216
+ });
217
+ return;
218
+ }
219
+ const timestamp = getWebhookHeader(webhook, "x-slack-request-timestamp");
220
+ const signature = getWebhookHeader(webhook, "x-slack-signature");
221
+ if (!verifySlackSignature(signingSecret, rawBody, timestamp, signature)) {
222
+ yield webhookHttpResponse({
223
+ status: 401,
224
+ body: { error: "invalid signature" },
225
+ });
226
+ return;
227
+ }
228
+ const slackEvent = payload.type === "event_callback" ? payload.event : undefined;
229
+ if (!slackEvent) {
230
+ yield webhookHttpResponse({ status: 200, body: {} });
231
+ return;
232
+ }
233
+ if (!shouldProcessSlackEvent(slackEvent)) {
234
+ yield webhookHttpResponse({ status: 200, body: {} });
235
+ return;
236
+ }
237
+ const dedupKey = slackEventDedupKey(payload, slackEvent);
238
+ if (markSlackEventSeen(dedupKey)) {
239
+ yield webhookHttpResponse({ status: 200, body: {} });
240
+ return;
241
+ }
242
+ const text = normalizeSlackText(slackEvent.text ?? "");
243
+ if (!text) {
244
+ yield webhookHttpResponse({ status: 200, body: {} });
245
+ return;
246
+ }
247
+ const threadId = slackEvent.thread_ts ?? slackEvent.ts;
248
+ if (!threadId) {
249
+ yield webhookHttpResponse({ status: 200, body: {} });
250
+ return;
251
+ }
252
+ ctx.state.threadId = threadId;
253
+ const slackMeta = {
254
+ teamId: payload.team_id,
255
+ channel: slackEvent.channel,
256
+ user: slackEvent.user,
257
+ ts: slackEvent.ts,
258
+ threadTs: slackEvent.thread_ts,
259
+ };
260
+ const openBotChannelId = slackEvent.channel
261
+ ? resolveOpenBotChannelId(slackEvent.channel, slackConfig)
262
+ : DEFAULT_OPENBOT_CHANNEL_ID;
263
+ ctx.state.channelId = openBotChannelId;
264
+ yield webhookHttpResponse({ status: 200, body: {} });
265
+ yield {
266
+ type: "agent:invoke",
267
+ data: {
268
+ agentId: ROUTING_AGENT_ID,
269
+ role: "user",
270
+ content: text,
271
+ },
272
+ meta: {
273
+ channelId: openBotChannelId,
274
+ threadId,
275
+ source: "slack",
276
+ slack: slackMeta,
277
+ },
278
+ };
279
+ });
280
+ builder.on("agent:invoke", async function* (event, ctx) {
281
+ const fromWebhook = isSlackWebhookIngress(event.meta);
282
+ if (fromWebhook) {
283
+ if (!shouldHandleInvoke(event, ROUTING_AGENT_ID))
284
+ return;
285
+ }
286
+ else {
287
+ if (!shouldHandleInvoke(event, agentId))
288
+ return;
289
+ }
290
+ if (event.meta?.threadId) {
291
+ ctx.state.threadId = event.meta.threadId;
292
+ }
293
+ const threadId = event.meta?.threadId ?? ctx.state.threadId;
294
+ const userMessage = (event.data.content ?? "").trim();
295
+ if (!userMessage || !threadId)
296
+ return;
297
+ if (fromWebhook) {
298
+ const auth = await resolveSlackCredentials(slackConfig, storage, {
299
+ requireOpenAi: false,
300
+ });
301
+ if (!auth.ok) {
302
+ yield {
303
+ type: "agent:output",
304
+ data: { content: formatMissingCredentials(auth.missing) },
305
+ meta: {
306
+ agentId: ROUTING_AGENT_ID,
307
+ threadId,
308
+ source: "slack",
309
+ slack: getSlackThreadMeta(event.meta),
310
+ },
311
+ };
312
+ return;
313
+ }
314
+ if (!host?.runAgent) {
315
+ yield {
316
+ type: "agent:output",
317
+ data: {
318
+ content: "Slack plugin requires host.runAgent to delegate to the system agent.",
319
+ },
320
+ meta: {
321
+ agentId: ROUTING_AGENT_ID,
322
+ threadId,
323
+ source: "slack",
324
+ slack: getSlackThreadMeta(event.meta),
325
+ },
326
+ };
327
+ return;
328
+ }
329
+ const slackMeta = getSlackThreadMeta(event.meta);
330
+ const channelId = slackMeta?.channel
331
+ ? resolveOpenBotChannelId(slackMeta.channel, slackConfig)
332
+ : ctx.state.channelId;
333
+ if (!channelId)
334
+ return;
335
+ try {
336
+ yield* bridgeSystemAgentRun({
337
+ host,
338
+ channelId,
339
+ threadId,
340
+ userMessage,
341
+ meta: event.meta,
342
+ publicBaseUrl,
343
+ });
344
+ }
345
+ catch (error) {
346
+ const message = error instanceof Error ? error.message : String(error);
347
+ yield {
348
+ type: "agent:output",
349
+ data: { content: `Slack agent error: ${message}` },
350
+ meta: {
351
+ agentId: ROUTING_AGENT_ID,
352
+ threadId,
353
+ source: "slack",
354
+ slack: getSlackThreadMeta(event.meta),
355
+ },
356
+ };
357
+ }
358
+ return;
359
+ }
360
+ const auth = await resolveSlackCredentials(slackConfig, storage);
361
+ if (!auth.ok) {
362
+ yield agentOutput({
363
+ agentId,
364
+ content: formatMissingCredentials(auth.missing),
365
+ threadId,
366
+ meta: event.meta,
367
+ });
368
+ return;
369
+ }
370
+ try {
371
+ const reply = await runSlackAgent({
372
+ prompt: userMessage,
373
+ openaiApiKey: auth.credentials.openaiApiKey,
374
+ botToken: auth.credentials.botToken,
375
+ teamId: auth.credentials.teamId,
376
+ model: auth.credentials.model,
377
+ });
378
+ yield agentOutput({
379
+ agentId,
380
+ content: reply,
381
+ threadId,
382
+ meta: event.meta,
383
+ });
384
+ }
385
+ catch (error) {
386
+ const message = error instanceof Error ? error.message : String(error);
387
+ yield agentOutput({
388
+ agentId,
389
+ content: `Slack agent error: ${message}`,
390
+ threadId,
391
+ meta: event.meta,
392
+ });
393
+ }
394
+ });
395
+ builder.on("agent:output", async function* (event) {
396
+ if (event.meta?.source !== "slack")
397
+ return;
398
+ if (event.meta?.agentId !== ROUTING_AGENT_ID)
399
+ return;
400
+ const slack = getSlackThreadMeta(event.meta);
401
+ if (!slack?.channel)
402
+ return;
403
+ const auth = await resolveSlackCredentials(slackConfig, storage);
404
+ if (!auth.ok)
405
+ return;
406
+ try {
407
+ await postSlackMessage({
408
+ token: auth.credentials.botToken,
409
+ channel: slack.channel,
410
+ text: event.data.content,
411
+ threadTs: slack.threadTs ?? slack.ts,
412
+ });
413
+ }
414
+ catch {
415
+ // Avoid failing the bus run if Slack delivery fails.
416
+ }
417
+ });
418
+ },
419
+ });
package/dist/logger.js ADDED
@@ -0,0 +1,17 @@
1
+ const PREFIX = "[slack]";
2
+ function formatMessage(message, details) {
3
+ if (!details || Object.keys(details).length === 0)
4
+ return message;
5
+ return `${message} ${JSON.stringify(details)}`;
6
+ }
7
+ export const log = {
8
+ info(message, details) {
9
+ console.log(`${PREFIX} ${formatMessage(message, details)}`);
10
+ },
11
+ warn(message, details) {
12
+ console.warn(`${PREFIX} ${formatMessage(message, details)}`);
13
+ },
14
+ debug(message, details) {
15
+ console.debug(`${PREFIX} ${formatMessage(message, details)}`);
16
+ },
17
+ };
@@ -0,0 +1,27 @@
1
+ import { createOpenAI } from "@ai-sdk/openai";
2
+ import { generateText, stepCountIs } from "ai";
3
+ import { createSlackMcpClient } from "./slack-mcp.js";
4
+ const SYSTEM_PROMPT = `You are the OpenBot Slack specialist agent.
5
+ Use Slack MCP tools to post messages, list channels, read history, and perform other workspace actions when needed.
6
+ Summarize results clearly in plain text for the user. Be concise and friendly.`;
7
+ export async function runSlackAgent(args) {
8
+ const openai = createOpenAI({ apiKey: args.openaiApiKey });
9
+ const mcpClient = await createSlackMcpClient({
10
+ botToken: args.botToken,
11
+ teamId: args.teamId,
12
+ });
13
+ try {
14
+ const tools = await mcpClient.tools();
15
+ const result = await generateText({
16
+ model: openai(args.model ?? "gpt-4o-mini"),
17
+ system: SYSTEM_PROMPT,
18
+ prompt: args.prompt,
19
+ tools,
20
+ stopWhen: stepCountIs(10),
21
+ });
22
+ return result.text.trim() || "Done.";
23
+ }
24
+ finally {
25
+ await mcpClient.close();
26
+ }
27
+ }
@@ -0,0 +1,15 @@
1
+ import { createMCPClient } from "@ai-sdk/mcp";
2
+ import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
3
+ export async function createSlackMcpClient(args) {
4
+ return createMCPClient({
5
+ transport: new Experimental_StdioMCPTransport({
6
+ command: "npx",
7
+ args: ["-y", "@modelcontextprotocol/server-slack"],
8
+ env: {
9
+ ...process.env,
10
+ SLACK_BOT_TOKEN: args.botToken,
11
+ SLACK_TEAM_ID: args.teamId,
12
+ },
13
+ }),
14
+ });
15
+ }
@@ -0,0 +1,18 @@
1
+ export async function postSlackMessage(args) {
2
+ const res = await fetch("https://slack.com/api/chat.postMessage", {
3
+ method: "POST",
4
+ headers: {
5
+ Authorization: `Bearer ${args.token}`,
6
+ "Content-Type": "application/json",
7
+ },
8
+ body: JSON.stringify({
9
+ channel: args.channel,
10
+ thread_ts: args.threadTs,
11
+ text: args.text,
12
+ }),
13
+ });
14
+ const body = (await res.json());
15
+ if (!body.ok) {
16
+ throw new Error(body.error ?? `Slack API error (${res.status})`);
17
+ }
18
+ }
@@ -0,0 +1,20 @@
1
+ function slackThreadKey(channel, rootTs) {
2
+ return `${channel}:${rootTs}`;
3
+ }
4
+ const activeSlackThreads = new Set();
5
+ const threadHistories = new Map();
6
+ export function markSlackThreadActive(channel, rootTs) {
7
+ activeSlackThreads.add(slackThreadKey(channel, rootTs));
8
+ }
9
+ export function isSlackThreadActive(channel, rootTs) {
10
+ return activeSlackThreads.has(slackThreadKey(channel, rootTs));
11
+ }
12
+ export function getSlackThreadHistory(channel, rootTs) {
13
+ return threadHistories.get(slackThreadKey(channel, rootTs)) ?? [];
14
+ }
15
+ export function appendSlackThreadHistory(channel, rootTs, ...messages) {
16
+ const key = slackThreadKey(channel, rootTs);
17
+ const history = threadHistories.get(key) ?? [];
18
+ history.push(...messages);
19
+ threadHistories.set(key, history);
20
+ }
@@ -0,0 +1 @@
1
+ export declare function verifySlackSignature(signingSecret: string, rawBody: Buffer, timestamp: string | undefined, signature: string | undefined): boolean;
@@ -0,0 +1,20 @@
1
+ import crypto from "node:crypto";
2
+ export function verifySlackSignature(signingSecret, rawBody, timestamp, signature) {
3
+ if (!signingSecret || !timestamp || !signature)
4
+ return false;
5
+ const age = Math.abs(Date.now() / 1000 - Number(timestamp));
6
+ if (!Number.isFinite(age) || age > 60 * 5)
7
+ return false;
8
+ const base = `v0:${timestamp}:${rawBody.toString("utf8")}`;
9
+ const hmac = crypto
10
+ .createHmac("sha256", signingSecret)
11
+ .update(base)
12
+ .digest("hex");
13
+ const expected = `v0=${hmac}`;
14
+ try {
15
+ return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@meetopenbot/slack",
3
+ "version": "0.0.1",
4
+ "description": "Slack Events API ingress, simple agent replies, and thread delivery for OpenBot",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc"
19
+ },
20
+ "dependencies": {
21
+ "@ai-sdk/mcp": "^2.0.14",
22
+ "@ai-sdk/openai": "^4.0.15",
23
+ "@meetopenbot/plugin-sdk": "^0.1.8",
24
+ "ai": "^7.0.29"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.10.1",
28
+ "typescript": "^5.9.3"
29
+ }
30
+ }