@cuylabs/channel-slack 0.10.0 → 0.11.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.
@@ -0,0 +1,127 @@
1
+ import {
2
+ openSlackModal
3
+ } from "./chunk-IRFKUPJN.js";
4
+
5
+ // src/feedback/block.ts
6
+ var SLACK_FEEDBACK_ACTION_ID = "agent_feedback";
7
+ function createSlackFeedbackBlock(options = {}) {
8
+ const actionId = (options.actionId ?? SLACK_FEEDBACK_ACTION_ID).trim();
9
+ if (!actionId) {
10
+ throw new Error(
11
+ "createSlackFeedbackBlock: actionId must be a non-empty string"
12
+ );
13
+ }
14
+ const positiveValue = options.positiveValue ?? "good-feedback";
15
+ const negativeValue = options.negativeValue ?? "bad-feedback";
16
+ const positiveLabel = options.positiveButton?.text ?? "Good Response";
17
+ const negativeLabel = options.negativeButton?.text ?? "Bad Response";
18
+ const positiveA11y = options.positiveButton?.accessibilityLabel ?? "Submit positive feedback on this response";
19
+ const negativeA11y = options.negativeButton?.accessibilityLabel ?? "Submit negative feedback on this response";
20
+ return {
21
+ type: "context_actions",
22
+ elements: [
23
+ {
24
+ type: "feedback_buttons",
25
+ action_id: actionId,
26
+ positive_button: {
27
+ text: { type: "plain_text", text: positiveLabel },
28
+ accessibility_label: positiveA11y,
29
+ value: positiveValue
30
+ },
31
+ negative_button: {
32
+ text: { type: "plain_text", text: negativeLabel },
33
+ accessibility_label: negativeA11y,
34
+ value: negativeValue
35
+ }
36
+ }
37
+ ]
38
+ };
39
+ }
40
+
41
+ // src/feedback/action.ts
42
+ function registerSlackFeedbackAction(app, options) {
43
+ const actionId = (options.actionId ?? SLACK_FEEDBACK_ACTION_ID).trim();
44
+ const positiveValue = options.positiveValue ?? "good-feedback";
45
+ const negativeValue = options.negativeValue ?? "bad-feedback";
46
+ const positiveAck = options.positiveAck === void 0 ? "Thanks for the feedback!" : options.positiveAck;
47
+ const negativeAck = options.negativeAck === void 0 ? "Thanks \u2014 we'll use this to improve the assistant." : options.negativeAck;
48
+ app.action(
49
+ actionId,
50
+ // Bolt's action args don't have a single shared exported type; the
51
+ // structural shape we read here (ack/body/client/logger) is stable.
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ async ({ ack, body, client, logger }) => {
54
+ const ackFn = ack;
55
+ await ackFn();
56
+ try {
57
+ if (body?.type !== "block_actions" || !Array.isArray(body.actions) || body.actions.length === 0) {
58
+ return;
59
+ }
60
+ const action = body.actions[0];
61
+ if (action?.type !== "feedback_buttons") {
62
+ return;
63
+ }
64
+ const value = String(action.value ?? "");
65
+ const verdict = value === positiveValue ? "positive" : value === negativeValue ? "negative" : void 0;
66
+ if (!verdict) {
67
+ return;
68
+ }
69
+ const channelId = body.channel?.id ?? body.container?.channel_id;
70
+ const userId = body.user?.id;
71
+ const messageTs = body.message?.ts ?? body.container?.message_ts;
72
+ const threadTs = body.message?.thread_ts ?? body.container?.thread_ts;
73
+ const triggerId = typeof body.trigger_id === "string" ? body.trigger_id : void 0;
74
+ if (!channelId || !userId || !messageTs) {
75
+ return;
76
+ }
77
+ const openModal = async (view) => {
78
+ if (!triggerId) {
79
+ throw new Error(
80
+ "Slack feedback payload did not include trigger_id."
81
+ );
82
+ }
83
+ await openSlackModal({ client, triggerId, view });
84
+ };
85
+ const acknowledgeEphemeral = async (text) => {
86
+ await client.chat.postEphemeral({
87
+ channel: channelId,
88
+ user: userId,
89
+ ...threadTs ? { thread_ts: threadTs } : { thread_ts: messageTs },
90
+ text
91
+ });
92
+ };
93
+ const feedbackContext = {
94
+ verdict,
95
+ value,
96
+ channelId,
97
+ userId,
98
+ messageTs,
99
+ ...threadTs ? { threadTs } : {},
100
+ ...triggerId ? { triggerId } : {}
101
+ };
102
+ const sessionId = options.resolveSessionId?.(feedbackContext);
103
+ await options.onFeedback({
104
+ ...feedbackContext,
105
+ ...sessionId ? { sessionId } : {},
106
+ openModal,
107
+ acknowledgeEphemeral
108
+ });
109
+ const ackText = verdict === "positive" ? positiveAck : negativeAck;
110
+ if (ackText) {
111
+ await acknowledgeEphemeral(ackText);
112
+ }
113
+ } catch (error) {
114
+ logger?.error?.(
115
+ `[channel-slack] feedback handler failed: ${error instanceof Error ? error.message : String(error)}`
116
+ );
117
+ }
118
+ }
119
+ );
120
+ return { actionId, positiveValue, negativeValue };
121
+ }
122
+
123
+ export {
124
+ SLACK_FEEDBACK_ACTION_ID,
125
+ createSlackFeedbackBlock,
126
+ registerSlackFeedbackAction
127
+ };
@@ -0,0 +1,446 @@
1
+ import {
2
+ UnsupportedSlackInteractiveRequestError,
3
+ bridgeSlackTurnEventsToSlack,
4
+ resolveSlackEventBridgeOptions
5
+ } from "./chunk-IAQXQESO.js";
6
+ import {
7
+ extractSlackAuthContext,
8
+ extractSlackUserIdentity,
9
+ parseSlackMentionActivity,
10
+ parseSlackMessageActivity,
11
+ resolveThreadAwareSlackSessionId,
12
+ runWithSlackTurnContext
13
+ } from "./chunk-37RN2YUI.js";
14
+ import {
15
+ isProcessableMessage
16
+ } from "./chunk-FPCE5V5Y.js";
17
+ import {
18
+ resolveSlackMessageFormatter
19
+ } from "./chunk-6WHFQUYQ.js";
20
+
21
+ // src/adapter/session-map.ts
22
+ function createSlackSessionMap(options) {
23
+ const strategy = options.sessionStrategy ?? "thread-aware";
24
+ if (strategy === "custom") {
25
+ if (!options.resolveSessionId) {
26
+ throw new Error(
27
+ "SlackChannelOptions.resolveSessionId is required when sessionStrategy is 'custom'"
28
+ );
29
+ }
30
+ const customResolve = options.resolveSessionId;
31
+ return {
32
+ resolve(info) {
33
+ return customResolve(info);
34
+ }
35
+ };
36
+ }
37
+ if (strategy === "channel-id") {
38
+ return {
39
+ resolve(info) {
40
+ return info.channelId;
41
+ }
42
+ };
43
+ }
44
+ if (strategy === "user-per-channel") {
45
+ return {
46
+ resolve(info) {
47
+ return `${info.channelId}:${info.userId}`;
48
+ }
49
+ };
50
+ }
51
+ if (strategy === "user-per-thread") {
52
+ return {
53
+ resolve(info) {
54
+ if (info.channelType === "dm") {
55
+ return `${info.channelId}:${info.userId}`;
56
+ }
57
+ return `${info.channelId}:${info.threadTs ?? info.messageTs ?? info.channelId}:${info.userId}`;
58
+ }
59
+ };
60
+ }
61
+ return {
62
+ resolve: resolveThreadAwareSlackSessionId
63
+ };
64
+ }
65
+
66
+ // src/adapter/sink.ts
67
+ function buildThreadPayload(info, respondInThread) {
68
+ if (!respondInThread || info.channelType === "dm") return {};
69
+ const threadTs = info.threadTs ?? info.messageTs;
70
+ return threadTs ? { thread_ts: threadTs } : {};
71
+ }
72
+ function buildResponseSink(say, client, info, respondInThread, chatStreamStartArgs, sayStream) {
73
+ const threadPayload = buildThreadPayload(info, respondInThread);
74
+ return {
75
+ artifactClient: client,
76
+ artifactTarget: {
77
+ channelId: info.channelId,
78
+ ...threadPayload.thread_ts ? { threadTs: threadPayload.thread_ts } : {}
79
+ },
80
+ async postMessage(text) {
81
+ const result = await say({ text, ...threadPayload });
82
+ const response = result;
83
+ const { channel, ts } = response;
84
+ if (!channel || !ts) {
85
+ throw new Error("Slack say() response did not include channel and ts.");
86
+ }
87
+ return { channel, ts };
88
+ },
89
+ async updateMessage(channel, ts, text) {
90
+ await client.chat.update({ channel, ts, text });
91
+ },
92
+ createChatStream({ bufferSize }) {
93
+ const threadTs = threadPayload.thread_ts ?? info.messageTs;
94
+ if (!threadTs) {
95
+ throw new Error(
96
+ "Slack chat-stream mode requires a source message timestamp."
97
+ );
98
+ }
99
+ const streamArgs = {
100
+ ...chatStreamStartArgs ?? {},
101
+ buffer_size: bufferSize
102
+ };
103
+ if (typeof sayStream === "function") {
104
+ return sayStream(streamArgs);
105
+ }
106
+ const streamer = client.chatStream({
107
+ channel: info.channelId,
108
+ thread_ts: threadTs,
109
+ ...info.teamId ? { recipient_team_id: info.teamId } : {},
110
+ recipient_user_id: info.userId,
111
+ ...streamArgs
112
+ });
113
+ return streamer;
114
+ }
115
+ };
116
+ }
117
+ function buildInteractiveResponder(say, client, info, respondInThread) {
118
+ const threadPayload = buildThreadPayload(info, respondInThread);
119
+ return {
120
+ async postMessage(message) {
121
+ const result = await say({
122
+ text: message.text,
123
+ ...message.blocks ? { blocks: message.blocks } : {},
124
+ ...threadPayload
125
+ });
126
+ const response = result;
127
+ const channel = response.channel ?? info.channelId;
128
+ const ts = response.ts;
129
+ if (!channel || !ts) {
130
+ throw new Error("Slack say() response did not include channel and ts.");
131
+ }
132
+ return { channel, ts };
133
+ },
134
+ async updateMessage(ref, message) {
135
+ await client.chat.update({
136
+ channel: ref.channel,
137
+ ts: ref.ts,
138
+ text: message.text,
139
+ ...message.blocks ? { blocks: message.blocks } : {}
140
+ });
141
+ }
142
+ };
143
+ }
144
+
145
+ // src/adapter/adapter.ts
146
+ var DEFAULT_CLASSIC_INITIAL_STATUS = {
147
+ status: "Thinking..."
148
+ };
149
+ function createSlackChannelAdapter(options) {
150
+ const { source } = options;
151
+ if (!source || typeof source.chat !== "function") {
152
+ throw new Error(
153
+ "createSlackChannelAdapter: options.source must implement SlackTurnSource."
154
+ );
155
+ }
156
+ const sessionMap = createSlackSessionMap(options);
157
+ const bridgeOptions = resolveSlackEventBridgeOptions(
158
+ {
159
+ showReasoning: options.showReasoning,
160
+ showToolUsage: options.showToolUsage,
161
+ showSubagentToolUsage: options.showSubagentToolUsage,
162
+ showSubagentResultInTask: options.showSubagentResultInTask,
163
+ formatToolTitle: options.formatToolTitle,
164
+ formatToolUpdate: options.formatToolUpdate,
165
+ formatToolDetails: options.formatToolDetails,
166
+ formatToolResultOutput: options.formatToolResultOutput,
167
+ formatToolError: options.formatToolError,
168
+ formatReasoningUpdate: options.formatReasoningUpdate,
169
+ interactiveMode: options.interactiveMode,
170
+ formatApprovalRequired: options.formatApprovalRequired,
171
+ formatHumanInputRequired: options.formatHumanInputRequired,
172
+ formatMessageText: resolveSlackMessageFormatter(options),
173
+ streamingMode: options.streamingMode,
174
+ progressiveUpdateThreshold: options.progressiveUpdateThreshold,
175
+ progressiveUpdateIntervalMs: options.progressiveUpdateIntervalMs,
176
+ chatStreamBufferSize: options.chatStreamBufferSize,
177
+ maxTaskUpdates: options.maxTaskUpdates,
178
+ maxTaskUpdateTextChars: options.maxTaskUpdateTextChars,
179
+ maxTaskUpdateFieldChars: options.maxTaskUpdateFieldChars,
180
+ chatStreamFinalArgs: options.chatStreamFinalArgs,
181
+ publishFinalResponseArtifact: options.publishFinalResponseArtifact,
182
+ finalResponseArtifactMode: options.finalResponseArtifactMode,
183
+ finalResponseArtifactStreamThreshold: options.finalResponseArtifactStreamThreshold,
184
+ formatFinalResponseArtifactContinuationNotice: options.formatFinalResponseArtifactContinuationNotice,
185
+ formatFinalResponseArtifactMessage: options.formatFinalResponseArtifactMessage,
186
+ onFinalResponseArtifactError: options.onFinalResponseArtifactError
187
+ }
188
+ );
189
+ const timeout = options.timeout ?? 12e4;
190
+ const respondInThread = options.respondInThread ?? true;
191
+ const respondToMentions = options.respondToMentions ?? true;
192
+ const respondToMessages = options.respondToMessages ?? true;
193
+ const respondToChannelMessages = options.respondToChannelMessages ?? false;
194
+ async function processTurn(slackActivity, say, client, context, utilities = {}) {
195
+ const rawText = slackActivity.text.trim();
196
+ const userText = options.resolveMessage ? await options.resolveMessage(slackActivity) : rawText;
197
+ if (!userText) return;
198
+ const initialSessionId = await sessionMap.resolve(slackActivity);
199
+ const userIdentity = extractSlackUserIdentity(slackActivity);
200
+ const auth = extractSlackAuthContext(context, slackActivity.userId);
201
+ const hasSayStream = typeof utilities.sayStream === "function";
202
+ const hasSetStatus = typeof utilities.setStatus === "function";
203
+ options.logger?.debug?.("slack classic turn utilities", {
204
+ channelId: slackActivity.channelId,
205
+ channelType: slackActivity.channelType,
206
+ isMention: slackActivity.isMention,
207
+ threadTs: slackActivity.threadTs,
208
+ messageTs: slackActivity.messageTs,
209
+ hasSayStream,
210
+ hasSetStatus
211
+ });
212
+ const baseTurnRequest = {
213
+ slackActivity,
214
+ user: userIdentity,
215
+ sessionId: initialSessionId,
216
+ message: userText,
217
+ auth,
218
+ ...utilities.setStatus ? { setStatus: utilities.setStatus } : {}
219
+ };
220
+ if (utilities.setStatus) {
221
+ const initialStatus = await resolveClassicInitialStatus(
222
+ options,
223
+ baseTurnRequest
224
+ );
225
+ if (initialStatus) {
226
+ try {
227
+ await utilities.setStatus(initialStatus);
228
+ } catch (error) {
229
+ options.logger?.warn?.("slack classic setStatus failed", {
230
+ error: formatErrorForLog(error)
231
+ });
232
+ }
233
+ }
234
+ }
235
+ const resolvedSessionId = await options.resolveSession?.(baseTurnRequest);
236
+ const sessionId = resolvedSessionId && resolvedSessionId.length > 0 ? resolvedSessionId : initialSessionId;
237
+ const turnRequest = {
238
+ ...baseTurnRequest,
239
+ sessionId
240
+ };
241
+ const preparedTurn = await resolveTurnPreparation(
242
+ options,
243
+ turnRequest,
244
+ userIdentity
245
+ );
246
+ const finalSessionId = preparedTurn.sessionId && preparedTurn.sessionId.length > 0 ? preparedTurn.sessionId : sessionId;
247
+ const finalUserText = preparedTurn.message ?? userText;
248
+ const sink = buildResponseSink(
249
+ say,
250
+ client,
251
+ slackActivity,
252
+ respondInThread,
253
+ options.chatStreamStartArgs,
254
+ utilities.sayStream
255
+ );
256
+ const abortController = new AbortController();
257
+ const chatOptions = {
258
+ abort: abortController.signal
259
+ };
260
+ if (preparedTurn.system) {
261
+ chatOptions.system = preparedTurn.system;
262
+ }
263
+ const timeoutId = timeout > 0 ? setTimeout(() => abortController.abort(), timeout) : void 0;
264
+ try {
265
+ await runWithSlackTurnContext(
266
+ {
267
+ ...turnRequest,
268
+ sessionId: finalSessionId,
269
+ message: finalUserText,
270
+ ...utilities.setStatus ? { setStatus: utilities.setStatus } : {},
271
+ ...preparedTurn.context ? { context: preparedTurn.context } : {}
272
+ },
273
+ async () => {
274
+ const events = source.chat(
275
+ finalSessionId,
276
+ finalUserText,
277
+ chatOptions
278
+ );
279
+ const baseTurnBridgeOptions = options.handleInteractiveRequest ? {
280
+ ...bridgeOptions,
281
+ handleInteractiveRequest: (interactive) => options.handleInteractiveRequest({
282
+ ...interactive,
283
+ slackActivity,
284
+ user: userIdentity,
285
+ sessionId: finalSessionId,
286
+ message: finalUserText,
287
+ responder: buildInteractiveResponder(
288
+ say,
289
+ client,
290
+ slackActivity,
291
+ respondInThread
292
+ )
293
+ })
294
+ } : bridgeOptions;
295
+ const turnBridgeOptions = withClassicStatusUpdates(
296
+ baseTurnBridgeOptions,
297
+ utilities.setStatus
298
+ );
299
+ await bridgeSlackTurnEventsToSlack(events, sink, turnBridgeOptions);
300
+ }
301
+ );
302
+ } catch (error) {
303
+ const errorInstance = error instanceof Error ? error : new Error(String(error));
304
+ if (!(errorInstance instanceof UnsupportedSlackInteractiveRequestError)) {
305
+ await say({
306
+ text: `I encountered an error while processing your request: ${errorInstance.message}`,
307
+ ...buildThreadPayload(slackActivity, respondInThread)
308
+ }).catch(() => void 0);
309
+ }
310
+ if (options.onError) {
311
+ await options.onError(errorInstance, slackActivity);
312
+ }
313
+ } finally {
314
+ if (timeoutId) {
315
+ clearTimeout(timeoutId);
316
+ }
317
+ if (utilities.setStatus) {
318
+ await utilities.setStatus({ status: "" }).catch(
319
+ (error) => options.logger?.warn?.("slack classic clear status failed", {
320
+ error: formatErrorForLog(error)
321
+ })
322
+ );
323
+ }
324
+ }
325
+ }
326
+ function mount(app) {
327
+ if (respondToMessages) {
328
+ app.message(
329
+ async ({
330
+ message,
331
+ say,
332
+ client,
333
+ context,
334
+ sayStream,
335
+ setStatus
336
+ }) => {
337
+ const raw = message;
338
+ if (!isProcessableMessage(
339
+ raw
340
+ )) {
341
+ return;
342
+ }
343
+ const info = parseSlackMessageActivity(
344
+ raw
345
+ );
346
+ if (info.channelType !== "dm" && !respondToChannelMessages) {
347
+ return;
348
+ }
349
+ if (!info.text) return;
350
+ await processTurn(info, say, client, context, {
351
+ ...typeof sayStream === "function" ? { sayStream } : {},
352
+ ...typeof setStatus === "function" ? { setStatus } : {}
353
+ });
354
+ }
355
+ );
356
+ }
357
+ if (respondToMentions) {
358
+ app.event(
359
+ "app_mention",
360
+ async ({
361
+ event,
362
+ say,
363
+ client,
364
+ context,
365
+ sayStream,
366
+ setStatus
367
+ }) => {
368
+ const info = parseSlackMentionActivity(
369
+ event
370
+ );
371
+ if (!info.text) return;
372
+ await processTurn(info, say, client, context, {
373
+ ...typeof sayStream === "function" ? { sayStream } : {},
374
+ ...typeof setStatus === "function" ? { setStatus } : {}
375
+ });
376
+ }
377
+ );
378
+ }
379
+ if (options.welcomeMessage != null) {
380
+ app.event("member_joined_channel", async ({ event, client }) => {
381
+ const channelId = event.channel;
382
+ const userId = event.user;
383
+ const botInfo = await client.auth.test().catch(() => void 0);
384
+ if (botInfo?.user_id === userId) return;
385
+ await client.chat.postMessage({
386
+ channel: userId,
387
+ // DM the new member
388
+ text: options.welcomeMessage
389
+ }).catch(() => void 0);
390
+ await client.chat.postMessage({
391
+ channel: channelId,
392
+ text: options.welcomeMessage
393
+ }).catch(() => void 0);
394
+ });
395
+ }
396
+ }
397
+ return {
398
+ mount,
399
+ getSessionId: (info) => sessionMap.resolve(info)
400
+ };
401
+ }
402
+ function formatClassicSlackStatusUpdate(status) {
403
+ return { status };
404
+ }
405
+ async function resolveClassicInitialStatus(options, request) {
406
+ if (options.initialStatus === void 0) {
407
+ return DEFAULT_CLASSIC_INITIAL_STATUS;
408
+ }
409
+ if (typeof options.initialStatus === "function") {
410
+ return await options.initialStatus(request) ?? void 0;
411
+ }
412
+ return options.initialStatus;
413
+ }
414
+ function withClassicStatusUpdates(options, setStatus) {
415
+ if (!setStatus) {
416
+ return options;
417
+ }
418
+ return {
419
+ ...options,
420
+ onStatusChange: async (label, event) => {
421
+ await options.onStatusChange?.(label, event);
422
+ await setStatus(formatClassicSlackStatusUpdate(label));
423
+ }
424
+ };
425
+ }
426
+ async function resolveTurnPreparation(options, request, user) {
427
+ if (options.prepareTurn) {
428
+ return await options.prepareTurn(request) ?? {};
429
+ }
430
+ if (options.resolveUserContext) {
431
+ const ctx = await options.resolveUserContext(user);
432
+ return ctx ?? {};
433
+ }
434
+ return {};
435
+ }
436
+ function formatErrorForLog(error) {
437
+ if (error instanceof Error) {
438
+ return error.stack ?? error.message;
439
+ }
440
+ return String(error);
441
+ }
442
+
443
+ export {
444
+ createSlackSessionMap,
445
+ createSlackChannelAdapter
446
+ };