@copilotkit/channels-slack 0.1.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.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/dist/__tests__/assistant.test.d.ts +2 -0
  4. package/dist/__tests__/assistant.test.d.ts.map +1 -0
  5. package/dist/__tests__/assistant.test.js +153 -0
  6. package/dist/__tests__/auto-close-streaming.test.d.ts +2 -0
  7. package/dist/__tests__/auto-close-streaming.test.d.ts.map +1 -0
  8. package/dist/__tests__/auto-close-streaming.test.js +139 -0
  9. package/dist/__tests__/built-ins.test.d.ts +2 -0
  10. package/dist/__tests__/built-ins.test.d.ts.map +1 -0
  11. package/dist/__tests__/built-ins.test.js +67 -0
  12. package/dist/__tests__/chunked-message-stream.test.d.ts +2 -0
  13. package/dist/__tests__/chunked-message-stream.test.d.ts.map +1 -0
  14. package/dist/__tests__/chunked-message-stream.test.js +226 -0
  15. package/dist/__tests__/conversation-store.test.d.ts +2 -0
  16. package/dist/__tests__/conversation-store.test.d.ts.map +1 -0
  17. package/dist/__tests__/conversation-store.test.js +209 -0
  18. package/dist/__tests__/download-files.test.d.ts +2 -0
  19. package/dist/__tests__/download-files.test.d.ts.map +1 -0
  20. package/dist/__tests__/download-files.test.js +142 -0
  21. package/dist/__tests__/ephemeral.test.d.ts +2 -0
  22. package/dist/__tests__/ephemeral.test.d.ts.map +1 -0
  23. package/dist/__tests__/ephemeral.test.js +85 -0
  24. package/dist/__tests__/event-renderer.test.d.ts +2 -0
  25. package/dist/__tests__/event-renderer.test.d.ts.map +1 -0
  26. package/dist/__tests__/event-renderer.test.js +521 -0
  27. package/dist/__tests__/markdown-to-mrkdwn.test.d.ts +2 -0
  28. package/dist/__tests__/markdown-to-mrkdwn.test.d.ts.map +1 -0
  29. package/dist/__tests__/markdown-to-mrkdwn.test.js +80 -0
  30. package/dist/__tests__/message-stream.test.d.ts +2 -0
  31. package/dist/__tests__/message-stream.test.d.ts.map +1 -0
  32. package/dist/__tests__/message-stream.test.js +116 -0
  33. package/dist/__tests__/native-renderer.test.d.ts +2 -0
  34. package/dist/__tests__/native-renderer.test.d.ts.map +1 -0
  35. package/dist/__tests__/native-renderer.test.js +236 -0
  36. package/dist/__tests__/native-stream.test.d.ts +2 -0
  37. package/dist/__tests__/native-stream.test.d.ts.map +1 -0
  38. package/dist/__tests__/native-stream.test.js +235 -0
  39. package/dist/__tests__/reactions.test.d.ts +2 -0
  40. package/dist/__tests__/reactions.test.d.ts.map +1 -0
  41. package/dist/__tests__/reactions.test.js +193 -0
  42. package/dist/__tests__/slack-listener.test.d.ts +2 -0
  43. package/dist/__tests__/slack-listener.test.d.ts.map +1 -0
  44. package/dist/__tests__/slack-listener.test.js +559 -0
  45. package/dist/__tests__/views.test.d.ts +2 -0
  46. package/dist/__tests__/views.test.d.ts.map +1 -0
  47. package/dist/__tests__/views.test.js +143 -0
  48. package/dist/adapter.d.ts +188 -0
  49. package/dist/adapter.d.ts.map +1 -0
  50. package/dist/adapter.js +859 -0
  51. package/dist/adapter.test.d.ts +2 -0
  52. package/dist/adapter.test.d.ts.map +1 -0
  53. package/dist/adapter.test.js +368 -0
  54. package/dist/assistant.d.ts +19 -0
  55. package/dist/assistant.d.ts.map +1 -0
  56. package/dist/assistant.js +124 -0
  57. package/dist/auto-close-streaming.d.ts +62 -0
  58. package/dist/auto-close-streaming.d.ts.map +1 -0
  59. package/dist/auto-close-streaming.js +273 -0
  60. package/dist/built-in-context.d.ts +22 -0
  61. package/dist/built-in-context.d.ts.map +1 -0
  62. package/dist/built-in-context.js +65 -0
  63. package/dist/built-in-tools.d.ts +23 -0
  64. package/dist/built-in-tools.d.ts.map +1 -0
  65. package/dist/built-in-tools.js +45 -0
  66. package/dist/chunked-message-stream.d.ts +67 -0
  67. package/dist/chunked-message-stream.d.ts.map +1 -0
  68. package/dist/chunked-message-stream.js +134 -0
  69. package/dist/codec.d.ts +18 -0
  70. package/dist/codec.d.ts.map +1 -0
  71. package/dist/codec.js +18 -0
  72. package/dist/codec.test.d.ts +2 -0
  73. package/dist/codec.test.d.ts.map +1 -0
  74. package/dist/codec.test.js +19 -0
  75. package/dist/conversation-store.d.ts +92 -0
  76. package/dist/conversation-store.d.ts.map +1 -0
  77. package/dist/conversation-store.js +196 -0
  78. package/dist/download-files.d.ts +59 -0
  79. package/dist/download-files.d.ts.map +1 -0
  80. package/dist/download-files.js +116 -0
  81. package/dist/event-renderer.d.ts +98 -0
  82. package/dist/event-renderer.d.ts.map +1 -0
  83. package/dist/event-renderer.js +493 -0
  84. package/dist/index.d.ts +34 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +23 -0
  87. package/dist/ingress-normalize.d.ts +75 -0
  88. package/dist/ingress-normalize.d.ts.map +1 -0
  89. package/dist/ingress-normalize.js +153 -0
  90. package/dist/ingress-normalize.test.d.ts +2 -0
  91. package/dist/ingress-normalize.test.d.ts.map +1 -0
  92. package/dist/ingress-normalize.test.js +187 -0
  93. package/dist/interaction.d.ts +30 -0
  94. package/dist/interaction.d.ts.map +1 -0
  95. package/dist/interaction.js +219 -0
  96. package/dist/interaction.test.d.ts +2 -0
  97. package/dist/interaction.test.d.ts.map +1 -0
  98. package/dist/interaction.test.js +137 -0
  99. package/dist/markdown-to-mrkdwn.d.ts +21 -0
  100. package/dist/markdown-to-mrkdwn.d.ts.map +1 -0
  101. package/dist/markdown-to-mrkdwn.js +99 -0
  102. package/dist/message-stream.d.ts +44 -0
  103. package/dist/message-stream.d.ts.map +1 -0
  104. package/dist/message-stream.js +70 -0
  105. package/dist/native-stream.d.ts +131 -0
  106. package/dist/native-stream.d.ts.map +1 -0
  107. package/dist/native-stream.js +208 -0
  108. package/dist/render/block-kit.d.ts +35 -0
  109. package/dist/render/block-kit.d.ts.map +1 -0
  110. package/dist/render/block-kit.js +399 -0
  111. package/dist/render/block-kit.test.d.ts +2 -0
  112. package/dist/render/block-kit.test.d.ts.map +1 -0
  113. package/dist/render/block-kit.test.js +289 -0
  114. package/dist/render/budget.d.ts +24 -0
  115. package/dist/render/budget.d.ts.map +1 -0
  116. package/dist/render/budget.js +30 -0
  117. package/dist/render/budget.test.d.ts +2 -0
  118. package/dist/render/budget.test.d.ts.map +1 -0
  119. package/dist/render/budget.test.js +49 -0
  120. package/dist/render/modal.d.ts +13 -0
  121. package/dist/render/modal.d.ts.map +1 -0
  122. package/dist/render/modal.js +108 -0
  123. package/dist/render/transport.d.ts +42 -0
  124. package/dist/render/transport.d.ts.map +1 -0
  125. package/dist/render/transport.js +1 -0
  126. package/dist/render.d.ts +23 -0
  127. package/dist/render.d.ts.map +1 -0
  128. package/dist/render.js +20 -0
  129. package/dist/sanitizing-http-agent.d.ts +28 -0
  130. package/dist/sanitizing-http-agent.d.ts.map +1 -0
  131. package/dist/sanitizing-http-agent.js +51 -0
  132. package/dist/slack-listener.d.ts +73 -0
  133. package/dist/slack-listener.d.ts.map +1 -0
  134. package/dist/slack-listener.js +126 -0
  135. package/dist/slack-listener.test.d.ts +2 -0
  136. package/dist/slack-listener.test.d.ts.map +1 -0
  137. package/dist/slack-listener.test.js +115 -0
  138. package/dist/types.d.ts +153 -0
  139. package/dist/types.d.ts.map +1 -0
  140. package/dist/types.js +20 -0
  141. package/package.json +73 -0
@@ -0,0 +1,51 @@
1
+ import { HttpAgent, parseSSEStream, runHttpRequest } from "@ag-ui/client";
2
+ import { map } from "rxjs";
3
+ /**
4
+ * An `HttpAgent` that tolerates the AG-UI event streams real agents emit.
5
+ *
6
+ * `@ag-ui/client`'s stock transform re-validates every streamed event
7
+ * against a strict Zod schema. Some events that `@ag-ui/langgraph`
8
+ * legitimately emits fail it — notably a `TOOL_CALL_START` whose
9
+ * `parentMessageId` is `null` ("Expected string, received null") — and a
10
+ * single rejected event aborts the entire run. That breaks LangGraph
11
+ * interrupts / human-in-the-loop in Slack, where the tool call that
12
+ * triggers the interrupt carries exactly that shape.
13
+ *
14
+ * The bridge talks to a trusted runtime, so rather than re-validate its
15
+ * output we use the same SSE parse the stock path wraps
16
+ * (`parseSSEStream`) and coerce the known nullable-string fields. Note
17
+ * this deliberately drops the stock transform's *entire* strict Zod
18
+ * re-validation step (not just the offending field) — acceptable only
19
+ * because the runtime is trusted and `runHttpRequest` still throws on
20
+ * transport/HTTP errors. The first coercion logs a one-time breadcrumb so
21
+ * the workaround is visible in production. Revert to the stock transform
22
+ * (`transformHttpEventStream`) once upstream makes these fields nullable.
23
+ */
24
+ export class SanitizingHttpAgent extends HttpAgent {
25
+ run(input) {
26
+ return parseSSEStream(runHttpRequest(() => this.fetch(this.url, this.requestInit(input))), this.debugLogger).pipe(map((event) => coerceNullStrings(event)));
27
+ }
28
+ }
29
+ /** One-time breadcrumb so the workaround's use is visible in production. */
30
+ let coercionWarned = false;
31
+ /**
32
+ * Coerce known nullable-string event fields to `""`. Targeted on purpose:
33
+ * we only touch fields where a `null` is known to come through from
34
+ * `@ag-ui/langgraph` and would otherwise trip a downstream string check.
35
+ */
36
+ function coerceNullStrings(event) {
37
+ if (!event || typeof event !== "object")
38
+ return event;
39
+ const e = event;
40
+ if (e["parentMessageId"] === null) {
41
+ e["parentMessageId"] = "";
42
+ if (!coercionWarned) {
43
+ coercionWarned = true;
44
+ console.warn('[SanitizingHttpAgent] coerced a null `parentMessageId` to "" and ' +
45
+ "bypassed @ag-ui/client strict event re-validation for this stream " +
46
+ "(known @ag-ui/langgraph quirk). Remove this agent once upstream " +
47
+ "makes the field nullable.");
48
+ }
49
+ }
50
+ return e;
51
+ }
@@ -0,0 +1,73 @@
1
+ import type { App } from "@slack/bolt";
2
+ import type { WebClient } from "@slack/web-api";
3
+ import type { SlackConversationStore } from "./conversation-store.js";
4
+ import type { IncomingTurn, ResolvedSlackRespondToOptions } from "./types.js";
5
+ /**
6
+ * Handler the listener calls when a Slack event maps to a usable turn.
7
+ * The listener doesn't know what happens next — it just hands off.
8
+ */
9
+ export type TurnHandler = (turn: IncomingTurn, client: WebClient) => Promise<void> | void;
10
+ /** A normalized slash-command invocation the listener hands off. */
11
+ export interface SlackCommand {
12
+ /** Command name as Slack sent it, including the leading slash (e.g. "/triage"). */
13
+ command: string;
14
+ /** The argument string after the command. */
15
+ text: string;
16
+ conversation: {
17
+ channelId: string;
18
+ scope: string;
19
+ };
20
+ replyTarget: {
21
+ channel: string;
22
+ };
23
+ senderUserId?: string;
24
+ /** Opaque platform trigger for opening a modal (Slack `trigger_id`). */
25
+ triggerId?: string;
26
+ /**
27
+ * Stable per-invocation id for inbound idempotency. Slash commands carry no
28
+ * Events API `event_id`, so this is derived from
29
+ * `${command}:${user_id}:${trigger_id}` — the closest stable-per-click value
30
+ * Slack provides.
31
+ */
32
+ eventId?: string;
33
+ }
34
+ export type CommandHandler = (command: SlackCommand, client: WebClient) => Promise<void> | void;
35
+ export interface ListenerConfig {
36
+ app: App;
37
+ /** Conversation store — used to check whether a thread-reply is "ours". */
38
+ store: SlackConversationStore;
39
+ /** Bot user id, used to filter out our own messages (loop guard). */
40
+ botUserId: string | undefined;
41
+ /** Resolved response-routing policy for Slack ingress. */
42
+ respondTo?: ResolvedSlackRespondToOptions;
43
+ /** Where each accepted turn is dispatched. */
44
+ onTurn: TurnHandler;
45
+ /** Where each slash command is dispatched. */
46
+ onCommand: CommandHandler;
47
+ /**
48
+ * True if `(channel, threadTs)` is an assistant-pane thread (owned by the
49
+ * Assistant middleware). When provided, threaded `message.im` events for
50
+ * those threads are skipped here so each pane message becomes exactly one
51
+ * turn. Absent (or returning false) → messages flow as shipped.
52
+ */
53
+ isAssistantThread?: (channel: string, threadTs: string) => boolean;
54
+ }
55
+ /**
56
+ * Attach Slack event handlers to a Bolt app. After this returns, the listener
57
+ * is the sole writer of IncomingTurn events — anything downstream sees a
58
+ * stream of cleanly-normalised turns regardless of which Slack event fired.
59
+ *
60
+ * Triggers:
61
+ * 1. @mention in a channel/thread → start or continue a conversation.
62
+ * 2. DM to the bot → reply flat in the DM.
63
+ * 3. Plain reply in a tracked thread when explicitly configured.
64
+ *
65
+ * Filters out:
66
+ * - `subtype` events (edits, joins, channel_renames, …)
67
+ * - bot messages (any `bot_id`, including our own posts)
68
+ * - top-level channel chatter we weren't @-mentioned in
69
+ * - the `message.channels` event that arrives alongside every `app_mention`
70
+ * (we recognise it by the presence of `<@botUserId>` in the text)
71
+ */
72
+ export declare function attachSlackListener(config: ListenerConfig): void;
73
+ //# sourceMappingURL=slack-listener.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-listener.d.ts","sourceRoot":"","sources":["../src/slack-listener.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,6BAA6B,EAAE,MAAM,YAAY,CAAC;AAQ9E;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,SAAS,KACd,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAE1B,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,WAAW,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,cAAc,GAAG,CAC3B,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,SAAS,KACd,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAE1B,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,GAAG,CAAC;IACT,2EAA2E;IAC3E,KAAK,EAAE,sBAAsB,CAAC;IAC9B,qEAAqE;IACrE,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,6BAA6B,CAAC;IAC1C,8CAA8C;IAC9C,MAAM,EAAE,WAAW,CAAC;IACpB,8CAA8C;IAC9C,SAAS,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;CACpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAsIhE"}
@@ -0,0 +1,126 @@
1
+ import { DEFAULT_SLACK_RESPOND_TO_OPTIONS, DM_SCOPE } from "./types.js";
2
+ import { stripMentions, deriveEventId, isPlainUserMessage, } from "./ingress-normalize.js";
3
+ /**
4
+ * Attach Slack event handlers to a Bolt app. After this returns, the listener
5
+ * is the sole writer of IncomingTurn events — anything downstream sees a
6
+ * stream of cleanly-normalised turns regardless of which Slack event fired.
7
+ *
8
+ * Triggers:
9
+ * 1. @mention in a channel/thread → start or continue a conversation.
10
+ * 2. DM to the bot → reply flat in the DM.
11
+ * 3. Plain reply in a tracked thread when explicitly configured.
12
+ *
13
+ * Filters out:
14
+ * - `subtype` events (edits, joins, channel_renames, …)
15
+ * - bot messages (any `bot_id`, including our own posts)
16
+ * - top-level channel chatter we weren't @-mentioned in
17
+ * - the `message.channels` event that arrives alongside every `app_mention`
18
+ * (we recognise it by the presence of `<@botUserId>` in the text)
19
+ */
20
+ export function attachSlackListener(config) {
21
+ const { app, store, onTurn, onCommand } = config;
22
+ const respondTo = config.respondTo ?? DEFAULT_SLACK_RESPOND_TO_OPTIONS;
23
+ // ── Slash commands ──────────────────────────────────────────────────
24
+ // Forward EVERY registered slash command to the engine, which routes it
25
+ // to the matching `bot.onCommand` handler (and ignores unregistered ones).
26
+ // We ack immediately (Slack's 3s deadline) and hand off; the handler does
27
+ // the slow work. Slack only delivers commands declared in the app config,
28
+ // so the `/.*/ ` matcher just catches whatever Slack sends.
29
+ // The command's args aren't posted to the channel, so we synthesise a
30
+ // stable per-(user) scope, letting a user re-run a command and continue
31
+ // the same conversation.
32
+ app.command(/.*/, async ({ command, ack, client }) => {
33
+ await ack();
34
+ await onCommand({
35
+ command: command.command,
36
+ text: (command.text ?? "").trim(),
37
+ conversation: {
38
+ channelId: command.channel_id,
39
+ scope: `slash::${command.user_id}`,
40
+ },
41
+ replyTarget: { channel: command.channel_id },
42
+ senderUserId: command.user_id,
43
+ triggerId: command.trigger_id,
44
+ // Slash commands carry no Events API event_id; trigger_id is the most
45
+ // stable per-invocation value Slack provides.
46
+ eventId: command.trigger_id
47
+ ? `${command.command}:${command.user_id}:${command.trigger_id}`
48
+ : undefined,
49
+ }, client);
50
+ });
51
+ app.event("app_mention", async ({ event, body, client }) => {
52
+ if (respondTo.appMentions === false)
53
+ return;
54
+ const threadTs = event.thread_ts ?? event.ts;
55
+ const userText = stripMentions(event.text ?? "");
56
+ const hasFiles = Array.isArray(event.files) &&
57
+ event.files.length > 0;
58
+ // Fire on a mention with an attachment even if the only text is the
59
+ // mention itself (e.g. "@bot" + a CSV). The store reads the file.
60
+ if (!userText && !hasFiles)
61
+ return;
62
+ await onTurn({
63
+ conversation: { channelId: event.channel, scope: threadTs },
64
+ replyTarget: respondTo.appMentions.reply === "thread"
65
+ ? { channel: event.channel, threadTs }
66
+ : { channel: event.channel },
67
+ userText,
68
+ senderUserId: event.user,
69
+ eventId: deriveEventId(body, event, event.channel),
70
+ }, client);
71
+ });
72
+ app.message(async ({ message, body, client }) => {
73
+ if (!isPlainUserMessage(message, config.botUserId))
74
+ return;
75
+ const text = (message.text ?? "").trim();
76
+ const hasFiles = Array.isArray(message.files) && message.files.length > 0;
77
+ // A bare file upload has empty text but is still a real turn.
78
+ if (!text && !hasFiles)
79
+ return;
80
+ const isDM = message.channel_type === "im";
81
+ // Pane messages are threaded DMs owned by the Assistant middleware — skip
82
+ // them here so each pane message becomes EXACTLY ONE turn. Gated per-THREAD
83
+ // (assistant threads tracked at runtime), never per-config: ordinary
84
+ // threaded DMs in apps without the Agents toggle keep flowing.
85
+ if (isDM &&
86
+ message.thread_ts &&
87
+ config.isAssistantThread?.(message.channel, message.thread_ts))
88
+ return;
89
+ if (!respondTo.directMessages)
90
+ return;
91
+ if (isDM) {
92
+ await onTurn({
93
+ conversation: { channelId: message.channel, scope: DM_SCOPE },
94
+ // Flat DM reply (no threadTs); carry the inbound ts so the renderer
95
+ // can anchor the native "is thinking…" status to a thread.
96
+ replyTarget: { channel: message.channel, statusTs: message.ts },
97
+ userText: text,
98
+ senderUserId: message.user,
99
+ eventId: deriveEventId(body, message, message.channel),
100
+ }, client);
101
+ return;
102
+ }
103
+ if (!message.thread_ts)
104
+ return; // top-level channel chatter — ignore
105
+ // app_mention runs separately for these; skip the duplicate.
106
+ if (config.botUserId && text.includes(`<@${config.botUserId}>`))
107
+ return;
108
+ if (respondTo.threadReplies === "mentionsOnly")
109
+ return;
110
+ // Only continue threads we already own. `has` consults Slack itself,
111
+ // so a restarted bridge naturally recognises threads it replied to
112
+ // before the restart.
113
+ if (!(await store.has({
114
+ channelId: message.channel,
115
+ scope: message.thread_ts,
116
+ })))
117
+ return;
118
+ await onTurn({
119
+ conversation: { channelId: message.channel, scope: message.thread_ts },
120
+ replyTarget: { channel: message.channel, threadTs: message.thread_ts },
121
+ userText: stripMentions(text),
122
+ senderUserId: message.user,
123
+ eventId: deriveEventId(body, message, message.channel),
124
+ }, client);
125
+ });
126
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=slack-listener.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slack-listener.test.d.ts","sourceRoot":"","sources":["../src/slack-listener.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { attachSlackListener } from "./slack-listener.js";
3
+ /**
4
+ * Capture the Bolt handlers `attachSlackListener` registers, without a real
5
+ * App. Each `app.command/event/message` records its handler so a test can
6
+ * invoke it with a representative Slack payload and assert on the emitted
7
+ * ingress object.
8
+ */
9
+ function captureListener(opts) {
10
+ let commandHandler;
11
+ let mentionHandler;
12
+ let messageHandler;
13
+ const app = {
14
+ command: vi.fn((_m, h) => {
15
+ commandHandler = h;
16
+ }),
17
+ event: vi.fn((_name, h) => {
18
+ mentionHandler = h;
19
+ }),
20
+ message: vi.fn((h) => {
21
+ messageHandler = h;
22
+ }),
23
+ };
24
+ const store = {
25
+ has: vi.fn(async () => opts?.storeHas ?? true),
26
+ };
27
+ const turns = [];
28
+ const commands = [];
29
+ attachSlackListener({
30
+ app,
31
+ store,
32
+ botUserId: "UBOT",
33
+ onTurn: (turn) => {
34
+ turns.push(turn);
35
+ },
36
+ onCommand: (cmd) => {
37
+ commands.push(cmd);
38
+ },
39
+ });
40
+ return {
41
+ turns,
42
+ commands,
43
+ command: (args) => commandHandler(args),
44
+ mention: (args) => mentionHandler(args),
45
+ message: (args) => messageHandler(args),
46
+ };
47
+ }
48
+ const client = {};
49
+ describe("attachSlackListener eventId (inbound dedup)", () => {
50
+ it("derives a turn eventId from the Events API envelope event_id for an app_mention", async () => {
51
+ const l = captureListener();
52
+ await l.mention({
53
+ event: {
54
+ channel: "C1",
55
+ user: "U1",
56
+ text: "<@UBOT> hi",
57
+ ts: "100.0",
58
+ client_msg_id: "cmid-1",
59
+ },
60
+ body: { event_id: "Ev123ABC" },
61
+ client,
62
+ });
63
+ expect(l.turns).toHaveLength(1);
64
+ // event_id wins over client_msg_id / ts.
65
+ expect(l.turns[0].eventId).toBe("Ev123ABC");
66
+ });
67
+ it("falls back to client_msg_id then ${channel}:${ts} when no envelope event_id", async () => {
68
+ const l = captureListener();
69
+ // DM message with client_msg_id but no envelope event_id.
70
+ await l.message({
71
+ message: {
72
+ channel: "D9",
73
+ channel_type: "im",
74
+ user: "U1",
75
+ text: "hello",
76
+ ts: "200.1",
77
+ client_msg_id: "cmid-2",
78
+ },
79
+ body: {},
80
+ client,
81
+ });
82
+ expect(l.turns).toHaveLength(1);
83
+ expect(l.turns[0].eventId).toBe("cmid-2");
84
+ // Without client_msg_id, falls back to channel:ts.
85
+ const l2 = captureListener();
86
+ await l2.message({
87
+ message: {
88
+ channel: "D9",
89
+ channel_type: "im",
90
+ user: "U1",
91
+ text: "hello",
92
+ ts: "200.2",
93
+ },
94
+ body: {},
95
+ client,
96
+ });
97
+ expect(l2.turns[0].eventId).toBe("D9:200.2");
98
+ });
99
+ it("derives a stable command eventId from command:user:trigger_id", async () => {
100
+ const l = captureListener();
101
+ await l.command({
102
+ command: {
103
+ command: "/triage",
104
+ text: "now",
105
+ channel_id: "C1",
106
+ user_id: "U1",
107
+ trigger_id: "trig-9",
108
+ },
109
+ ack: vi.fn(async () => { }),
110
+ client,
111
+ });
112
+ expect(l.commands).toHaveLength(1);
113
+ expect(l.commands[0].eventId).toBe("/triage:U1:trig-9");
114
+ });
115
+ });
@@ -0,0 +1,153 @@
1
+ import type { PlatformUser } from "@copilotkit/channels";
2
+ /**
3
+ * Where to post a reply in Slack. Used by the renderer; constructed by the
4
+ * listener once per turn.
5
+ */
6
+ export interface ReplyTarget {
7
+ channel: string;
8
+ /** Thread ts to post into. Omit for flat replies (DMs). */
9
+ threadTs?: string;
10
+ /**
11
+ * Turn sender's Slack user id. `chat.startStream` requires it
12
+ * (`recipient_user_id`) when streaming into a channel; carried here so the
13
+ * native streamer can read it off the target.
14
+ */
15
+ recipientUserId?: string;
16
+ /**
17
+ * Inbound message ts used as the thread anchor for
18
+ * `assistant.threads.setStatus` in flat DMs (which have no `threadTs`).
19
+ * Replies still post flat; this only gives the native "is thinking…"
20
+ * indicator a thread to attach to.
21
+ */
22
+ statusTs?: string;
23
+ }
24
+ /**
25
+ * Assistant-pane behavior ("Agents & AI Apps"). The pane is ON by default —
26
+ * it activates whenever the Slack app config has the Agents toggle, and lies
27
+ * dormant otherwise. Pass an object to {@link SlackAdapterOptions.assistant} to
28
+ * customize, or `false` to disable pane handling entirely.
29
+ */
30
+ export interface SlackAssistantOptions {
31
+ /** Posted when a user opens the pane. */
32
+ greeting?: string;
33
+ /** Up to 4 prompt chips shown on thread start. */
34
+ suggestedPrompts?: ReadonlyArray<{
35
+ title: string;
36
+ message: string;
37
+ }>;
38
+ /** "auto" (default): title the thread from the first user message. false: never title. */
39
+ title?: "auto" | false;
40
+ /** Native status shown under the composer while the agent runs. */
41
+ status?: {
42
+ /** Status while reasoning. Default "is thinking…". */
43
+ thinking?: string;
44
+ /** Up to 10 loading messages Slack rotates through. */
45
+ loadingMessages?: readonly string[];
46
+ /** Surface "is using `tool`…" per tool call. Default true. */
47
+ toolStatus?: boolean;
48
+ };
49
+ }
50
+ /**
51
+ * A 👍/👎 click on a streamed AI reply's native feedback row (Slack's
52
+ * `feedback_buttons` element, attached at `chat.stopStream`). Delivered to
53
+ * {@link SlackFeedbackOptions.onFeedback}.
54
+ */
55
+ export interface SlackFeedback {
56
+ sentiment: "positive" | "negative";
57
+ /** The user who clicked, if Slack supplied their identity. */
58
+ user?: PlatformUser;
59
+ /** Channel (or DM) the reply lives in. */
60
+ channel: string;
61
+ /** Thread the reply belongs to, if any. */
62
+ threadTs?: string;
63
+ /** ts of the streamed reply message the feedback is about. */
64
+ messageTs: string;
65
+ }
66
+ /**
67
+ * Opt-in native AI feedback buttons. When provided, streamed replies on the
68
+ * native path finalize with a `context_actions` + `feedback_buttons` row, and
69
+ * clicks are routed to {@link onFeedback} (they never reach the engine's
70
+ * interaction dispatch). Omit to show no feedback row.
71
+ */
72
+ export interface SlackFeedbackOptions {
73
+ /** Invoked when a user clicks 👍/👎 on a streamed reply. */
74
+ onFeedback: (feedback: SlackFeedback) => void | Promise<void>;
75
+ /** Positive button label. Default "Good response". */
76
+ positiveLabel?: string;
77
+ /** Negative button label. Default "Bad response". */
78
+ negativeLabel?: string;
79
+ }
80
+ export type SlackMentionReplyMode = "thread" | "channel";
81
+ export type SlackThreadReplyMode = "mentionsOnly" | "afterBotReply";
82
+ export interface SlackAppMentionOptions {
83
+ /**
84
+ * Where an app mention should reply. "thread" keeps channel noise down and is
85
+ * the default; "channel" posts a top-level channel reply.
86
+ */
87
+ reply?: SlackMentionReplyMode;
88
+ }
89
+ export interface SlackRespondToOptions {
90
+ /** Respond to normal Slack DMs (`message.im`). Default true. */
91
+ directMessages?: boolean;
92
+ /**
93
+ * Respond to Slack `app_mention` events in channels/private channels. Pass
94
+ * false to ignore app mentions entirely. Default: `{ reply: "thread" }`.
95
+ */
96
+ appMentions?: false | SlackAppMentionOptions;
97
+ /**
98
+ * How to handle plain, non-mention replies in channel/private-channel threads.
99
+ * "mentionsOnly" requires every thread turn to explicitly @mention the bot.
100
+ * "afterBotReply" preserves the legacy behavior: once the bot has replied in a
101
+ * thread, future plain replies in that thread can trigger new turns.
102
+ */
103
+ threadReplies?: SlackThreadReplyMode;
104
+ }
105
+ export interface ResolvedSlackRespondToOptions {
106
+ directMessages: boolean;
107
+ appMentions: false | {
108
+ reply: SlackMentionReplyMode;
109
+ };
110
+ threadReplies: SlackThreadReplyMode;
111
+ }
112
+ export declare const DEFAULT_SLACK_RESPOND_TO_OPTIONS: ResolvedSlackRespondToOptions;
113
+ export declare function resolveSlackRespondToOptions(respondTo?: SlackRespondToOptions): ResolvedSlackRespondToOptions;
114
+ /**
115
+ * Stable key identifying one ongoing conversation with the bot.
116
+ *
117
+ * - For a channel thread: `{ channelId, scope: <threadTs> }`
118
+ * - For a DM: `{ channelId, scope: "dm" }`
119
+ *
120
+ * The store uses the pair as a string key; conversations from different
121
+ * channels never collide.
122
+ */
123
+ export interface ConversationKey {
124
+ channelId: string;
125
+ scope: string;
126
+ }
127
+ /** Sentinel scope used for DMs (DMs are flat — no thread). */
128
+ export declare const DM_SCOPE = "dm";
129
+ /**
130
+ * What the listener emits per turn. Everything downstream of the listener
131
+ * works in terms of these — they don't know about Slack event shapes.
132
+ */
133
+ export interface IncomingTurn {
134
+ conversation: ConversationKey;
135
+ replyTarget: ReplyTarget;
136
+ userText: string;
137
+ /**
138
+ * Slack user id of the person who sent this message (the requester).
139
+ * Surfaced to the agent so it can act on behalf of the right person —
140
+ * e.g. scope "my issues" to them, or assign created work to them.
141
+ * Absent only if the originating event carried no user (rare).
142
+ */
143
+ senderUserId?: string;
144
+ /**
145
+ * Stable per-delivery id for inbound idempotency. Prefer the Events API
146
+ * envelope `event_id` (survives Slack's retries), falling back to
147
+ * `client_msg_id` or `${channel}:${ts}`. Undefined when no stable id is
148
+ * available — the engine simply skips dedup for that event (never fabricate
149
+ * a random id; that would defeat dedup).
150
+ */
151
+ eventId?: string;
152
+ }
153
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,20 @@
1
+ export const DEFAULT_SLACK_RESPOND_TO_OPTIONS = {
2
+ directMessages: true,
3
+ appMentions: { reply: "thread" },
4
+ threadReplies: "mentionsOnly",
5
+ };
6
+ export function resolveSlackRespondToOptions(respondTo) {
7
+ return {
8
+ directMessages: respondTo?.directMessages ??
9
+ DEFAULT_SLACK_RESPOND_TO_OPTIONS.directMessages,
10
+ appMentions: respondTo?.appMentions === false
11
+ ? false
12
+ : {
13
+ reply: respondTo?.appMentions?.reply ?? "thread",
14
+ },
15
+ threadReplies: respondTo?.threadReplies ??
16
+ DEFAULT_SLACK_RESPOND_TO_OPTIONS.threadReplies,
17
+ };
18
+ }
19
+ /** Sentinel scope used for DMs (DMs are flat — no thread). */
20
+ export const DM_SCOPE = "dm";
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@copilotkit/channels-slack",
3
+ "version": "0.1.0",
4
+ "description": "Slack platform adapter for CopilotKit JSX channels (@copilotkit/channels).",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/CopilotKit/CopilotKit.git",
9
+ "directory": "packages/channels-slack"
10
+ },
11
+ "homepage": "https://github.com/CopilotKit/CopilotKit",
12
+ "keywords": [
13
+ "ai",
14
+ "agent",
15
+ "bot",
16
+ "slack",
17
+ "block-kit",
18
+ "socket-mode",
19
+ "copilotkit",
20
+ "ag-ui"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "type": "module",
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ },
36
+ "./codec": {
37
+ "types": "./dist/codec.d.ts",
38
+ "import": "./dist/codec.js"
39
+ },
40
+ "./render": {
41
+ "types": "./dist/render.d.ts",
42
+ "import": "./dist/render.js"
43
+ }
44
+ },
45
+ "dependencies": {
46
+ "@ag-ui/client": "0.0.57",
47
+ "@ag-ui/core": "0.0.57",
48
+ "@slack/bolt": "^4.2.0",
49
+ "@slack/types": "^2.21.1",
50
+ "@slack/web-api": "^7.16.0",
51
+ "rxjs": "^7.8.1",
52
+ "zod": "^3.25.76",
53
+ "zod-to-json-schema": "^3.25.1",
54
+ "@copilotkit/channels-ui": "~0.1.0",
55
+ "@copilotkit/core": "^1.62.3",
56
+ "@copilotkit/shared": "^1.62.3",
57
+ "@copilotkit/channels": "~0.1.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^22.10.0",
61
+ "typescript": "^5.6.3",
62
+ "vitest": "^4.1.3",
63
+ "@copilotkit/typescript-config": "^1.55.0-next.8"
64
+ },
65
+ "scripts": {
66
+ "build": "tsc -p tsconfig.json",
67
+ "check-types": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.check.json",
68
+ "test": "vitest run",
69
+ "test:watch": "vitest",
70
+ "publint": "publint .",
71
+ "attw": "attw --pack . --profile esm-only"
72
+ }
73
+ }