@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,273 @@
1
+ /**
2
+ * Streaming-polish layer: while an agent is mid-stream, the buffer is
3
+ * usually an *unfinished* markdown document — an open code fence, an
4
+ * unclosed `**`, etc. If we send the unbalanced text straight to Slack,
5
+ * the rest of the message renders broken (a stray ``` will turn the rest
6
+ * of the Slack message into a code block until the closer arrives).
7
+ *
8
+ * `autoCloseOpenMarkdown` returns a balanced copy of the buffer by
9
+ * appending the minimum set of closers needed so that the markdown is
10
+ * well-formed for display. When the agent later emits the real closer,
11
+ * the buffer becomes balanced on its own and this function adds nothing
12
+ * — i.e. no double-close in the committed Slack message.
13
+ *
14
+ * Operates in **markdown space** (i.e. on the raw text from the agent,
15
+ * before mrkdwn translation). The caller composes this with
16
+ * `markdownToMrkdwn` afterwards so the translator sees a balanced
17
+ * document.
18
+ *
19
+ * What it handles, in priority order:
20
+ *
21
+ * 1. Unclosed fenced code blocks ``` … (most severe — leaks code styling
22
+ * through the rest of the Slack message)
23
+ * 2. Unclosed inline code `…`
24
+ * 3. Unclosed bold `**…` / `__…`
25
+ * 4. Unclosed italic `*…` / `_…`
26
+ * 5. Unclosed strike `~~…`
27
+ *
28
+ * Heuristics:
29
+ *
30
+ * - A marker with no content after it (e.g. trailing `**`) is **not**
31
+ * closed — the agent might be just opening the bracket; closing would
32
+ * produce a transient `****` which looks worse than `**`.
33
+ * - Markers inside code/fence regions don't count.
34
+ * - When closing, we emit closers in reverse stack order (innermost
35
+ * first) so the rendered structure is well-nested.
36
+ */
37
+ const SENTINEL_FENCE = "";
38
+ const SENTINEL_INLINE = "";
39
+ export function autoCloseOpenMarkdown(text) {
40
+ if (!text)
41
+ return text;
42
+ // ── 1. Fenced code blocks ──────────────────────────────────────────
43
+ // Pair complete fences first, then determine if there's a dangling open.
44
+ const fences = [];
45
+ let work = text.replace(/```[\s\S]*?```/g, (m) => {
46
+ fences.push(m);
47
+ return `${SENTINEL_FENCE}${fences.length - 1}${SENTINEL_FENCE}`;
48
+ });
49
+ const openFenceIdx = work.indexOf("```");
50
+ let openFenceTail = "";
51
+ if (openFenceIdx >= 0) {
52
+ openFenceTail = work.slice(openFenceIdx);
53
+ work = work.slice(0, openFenceIdx);
54
+ }
55
+ // ── 2. Inline code regions ─────────────────────────────────────────
56
+ const inlines = [];
57
+ work = work.replace(/`[^`\n]+`/g, (m) => {
58
+ inlines.push(m);
59
+ return `${SENTINEL_INLINE}${inlines.length - 1}${SENTINEL_INLINE}`;
60
+ });
61
+ // Dangling open inline backtick? Only count if there's content after it.
62
+ const openBacktickIdx = work.indexOf("`");
63
+ let openBacktickTail = "";
64
+ if (openBacktickIdx >= 0) {
65
+ openBacktickTail = work.slice(openBacktickIdx);
66
+ work = work.slice(0, openBacktickIdx);
67
+ }
68
+ // ── 3. Bold / italic / strike via stack scan ──────────────────────
69
+ const stack = scanBracketStack(work);
70
+ // Closers in pop order (innermost first) so the rendered structure
71
+ // nests correctly: e.g. `**bold _italic` → close `_` then `**`.
72
+ const closers = stack.slice().reverse().join("");
73
+ // ── 4. Reassemble ─────────────────────────────────────────────────
74
+ // Insert closers BEFORE any trailing whitespace on the work text so a
75
+ // buffer like "**bold " produces "**bold**" rather than "**bold **".
76
+ let output = work;
77
+ if (closers) {
78
+ const trail = output.match(/\s*$/)?.[0] ?? "";
79
+ output = output.slice(0, output.length - trail.length) + closers + trail;
80
+ }
81
+ // Inline backtick: if dangling open has content after it, close it.
82
+ if (openBacktickTail) {
83
+ output += openBacktickTail;
84
+ if (hasContentAfterMarker(openBacktickTail, "`")) {
85
+ output += "`";
86
+ }
87
+ }
88
+ // Restore inline regions (paired backticks that were untouched).
89
+ output = output.replace(new RegExp(`${SENTINEL_INLINE}(\\d+)${SENTINEL_INLINE}`, "g"), (_, idx) => inlines[Number(idx)] ?? "");
90
+ // Fenced code: if there's real code content (text after the optional
91
+ // language line), close the fence. A buffer like "```py" or "```py\n"
92
+ // is "just opened" and should NOT yet be closed.
93
+ if (openFenceTail) {
94
+ output += openFenceTail;
95
+ if (hasFenceCodeContent(openFenceTail)) {
96
+ output += openFenceTail.endsWith("\n") ? "```" : "\n```";
97
+ }
98
+ }
99
+ // Restore paired fences.
100
+ output = output.replace(new RegExp(`${SENTINEL_FENCE}(\\d+)${SENTINEL_FENCE}`, "g"), (_, idx) => fences[Number(idx)] ?? "");
101
+ return output;
102
+ }
103
+ /**
104
+ * Walks `text` left-to-right and returns the unbalanced bracket stack.
105
+ * Recognised markers (longer ones first): `**`, `__`, `~~`, then `*`, `_`.
106
+ *
107
+ * Standalone single-char markers don't match when they're part of the
108
+ * doubled variant — `**` is parsed as one bold marker, not two italics.
109
+ */
110
+ function scanBracketStack(text) {
111
+ const stack = [];
112
+ const tryToggle = (m) => {
113
+ if (stack[stack.length - 1] === m)
114
+ stack.pop();
115
+ else
116
+ stack.push(m);
117
+ };
118
+ let i = 0;
119
+ while (i < text.length) {
120
+ if (text.startsWith("**", i)) {
121
+ tryToggle("**");
122
+ i += 2;
123
+ }
124
+ else if (text.startsWith("__", i)) {
125
+ tryToggle("__");
126
+ i += 2;
127
+ }
128
+ else if (text.startsWith("~~", i)) {
129
+ tryToggle("~~");
130
+ i += 2;
131
+ }
132
+ else if (text[i] === "*") {
133
+ tryToggle("*");
134
+ i += 1;
135
+ }
136
+ else if (text[i] === "_") {
137
+ tryToggle("_");
138
+ i += 1;
139
+ }
140
+ else {
141
+ i += 1;
142
+ }
143
+ }
144
+ // Strip any marker at the top of the stack that has NO content after it
145
+ // (i.e. it appears at the very end of the buffer with only whitespace
146
+ // following). Closing those would produce a transient `****`-style
147
+ // artefact that looks worse than the open `**`.
148
+ while (stack.length > 0) {
149
+ const last = stack[stack.length - 1];
150
+ const lastIdx = text.lastIndexOf(last);
151
+ if (lastIdx < 0)
152
+ break;
153
+ const after = text.slice(lastIdx + last.length);
154
+ if (/^\s*$/.test(after)) {
155
+ stack.pop();
156
+ }
157
+ else {
158
+ break;
159
+ }
160
+ }
161
+ return stack;
162
+ }
163
+ function hasContentAfterMarker(tail, marker) {
164
+ if (!tail.startsWith(marker))
165
+ return false;
166
+ const after = tail.slice(marker.length);
167
+ return /\S/.test(after);
168
+ }
169
+ /**
170
+ * True if a `\`\`\`<lang>?\n<content>` buffer has actual code content
171
+ * past the optional language line. We treat the first newline as the
172
+ * boundary between the (optional) language tag and code body.
173
+ */
174
+ function hasFenceCodeContent(tail) {
175
+ if (!tail.startsWith("```"))
176
+ return false;
177
+ const after = tail.slice(3);
178
+ const nl = after.indexOf("\n");
179
+ if (nl < 0) {
180
+ // No newline yet — still on the language line. Nothing to close.
181
+ return false;
182
+ }
183
+ const code = after.slice(nl + 1);
184
+ return /\S/.test(code);
185
+ }
186
+ export function detectOpenContext(text) {
187
+ if (!text)
188
+ return { fenceLang: null, inlineCode: false, brackets: [] };
189
+ // ── Fence ────────────────────────────────────────────────────────
190
+ // Pair complete fences off the text; what remains tells us if a fence
191
+ // is still open and (if so) what its language tag is.
192
+ const paired = text.replace(/```[\s\S]*?```/g, "");
193
+ const openFenceIdx = paired.indexOf("```");
194
+ let fenceLang = null;
195
+ let remainder = paired;
196
+ if (openFenceIdx >= 0) {
197
+ // Everything from openFenceIdx onward is inside the open fence.
198
+ const tail = paired.slice(openFenceIdx);
199
+ const firstLine = tail.slice(3).split("\n", 1)[0] ?? "";
200
+ fenceLang = firstLine.trim();
201
+ remainder = paired.slice(0, openFenceIdx);
202
+ }
203
+ // ── Inline code (outside fences) ─────────────────────────────────
204
+ // Strip paired inline backticks; an odd remaining ` means open.
205
+ remainder = remainder.replace(/`[^`\n]+`/g, "");
206
+ const inlineCode = (remainder.match(/`/g) || []).length % 2 === 1;
207
+ // Cut at the last lone backtick — inside-inline content shouldn't
208
+ // contribute to bracket counting.
209
+ if (inlineCode) {
210
+ const lastBt = remainder.lastIndexOf("`");
211
+ if (lastBt >= 0)
212
+ remainder = remainder.slice(0, lastBt);
213
+ }
214
+ // ── Brackets (bold/italic/strike) ────────────────────────────────
215
+ const brackets = scanBracketStackForContext(remainder);
216
+ return { fenceLang, inlineCode, brackets };
217
+ }
218
+ /** Like scanBracketStack but exposed for context detection (no whitespace strip). */
219
+ function scanBracketStackForContext(text) {
220
+ const stack = [];
221
+ const tryToggle = (m) => {
222
+ if (stack[stack.length - 1] === m)
223
+ stack.pop();
224
+ else
225
+ stack.push(m);
226
+ };
227
+ let i = 0;
228
+ while (i < text.length) {
229
+ if (text.startsWith("**", i)) {
230
+ tryToggle("**");
231
+ i += 2;
232
+ }
233
+ else if (text.startsWith("__", i)) {
234
+ tryToggle("__");
235
+ i += 2;
236
+ }
237
+ else if (text.startsWith("~~", i)) {
238
+ tryToggle("~~");
239
+ i += 2;
240
+ }
241
+ else if (text[i] === "*") {
242
+ tryToggle("*");
243
+ i += 1;
244
+ }
245
+ else if (text[i] === "_") {
246
+ tryToggle("_");
247
+ i += 1;
248
+ }
249
+ else {
250
+ i += 1;
251
+ }
252
+ }
253
+ return stack;
254
+ }
255
+ /**
256
+ * Render the opener prefix for a continuation chunk so it can stand on
257
+ * its own as a self-renderable markdown fragment. E.g. for an open
258
+ * `` ```python `` fence, returns ` ```python\n `; for an open `**` bold,
259
+ * returns `**`.
260
+ */
261
+ export function renderContextOpener(ctx) {
262
+ // Fences are exclusive — if a fence is open, no other markers count
263
+ // (they're inside opaque code).
264
+ if (ctx.fenceLang !== null) {
265
+ return "```" + (ctx.fenceLang ?? "") + "\n";
266
+ }
267
+ let out = "";
268
+ for (const m of ctx.brackets)
269
+ out += m;
270
+ if (ctx.inlineCode)
271
+ out += "`";
272
+ return out;
273
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Slack-platform-universal context entries — knowledge the LLM needs
3
+ * about Slack itself (tagging procedure, mrkdwn vs Markdown, thread
4
+ * model). Apps spread `defaultSlackContext` into the `context:` config
5
+ * they pass to `createBot`.
6
+ *
7
+ * Each entry is exported individually too so apps can cherry-pick.
8
+ */
9
+ import type { ContextEntry } from "@copilotkit/channels";
10
+ /** Slack context entry — alias of `@copilotkit/channels`'s {@link ContextEntry}. */
11
+ export type SlackContextEntry = ContextEntry;
12
+ export declare const slackTaggingContext: SlackContextEntry;
13
+ export declare const slackFormattingContext: SlackContextEntry;
14
+ export declare const slackConversationModelContext: SlackContextEntry;
15
+ /**
16
+ * The default context entries the SDK ships. Spread into your
17
+ * `createBot({context: …})`:
18
+ *
19
+ * context: [...defaultSlackContext, ...myAppContext],
20
+ */
21
+ export declare const defaultSlackContext: ReadonlyArray<SlackContextEntry>;
22
+ //# sourceMappingURL=built-in-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"built-in-context.d.ts","sourceRoot":"","sources":["../src/built-in-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,oFAAoF;AACpF,MAAM,MAAM,iBAAiB,GAAG,YAAY,CAAC;AAE7C,eAAO,MAAM,mBAAmB,EAAE,iBAqBjC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,iBAiBpC,CAAC;AAEF,eAAO,MAAM,6BAA6B,EAAE,iBAa3C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,aAAa,CAAC,iBAAiB,CAIhE,CAAC"}
@@ -0,0 +1,65 @@
1
+ export const slackTaggingContext = {
2
+ description: "How to @-mention people on Slack — REQUIRED PROCEDURE",
3
+ value: [
4
+ "You are running on Slack. Whenever the user asks you to tag,",
5
+ "ping, @-mention, or otherwise notify a specific person by name,",
6
+ "handle, or email, you MUST follow this procedure BEFORE",
7
+ "composing your reply:",
8
+ "",
9
+ " 1. Call the `lookup_slack_user` tool with the person's name,",
10
+ " handle, or email as the `query` argument.",
11
+ " 2. If the tool returns `found: true`, paste its `mention`",
12
+ " field (e.g. `<@U05PN5700P9>`) verbatim wherever you would",
13
+ " have written the person's name. This is the ONLY way Slack",
14
+ " will actually ping them.",
15
+ " 3. If the tool returns `found: false`, write the person's",
16
+ " plain name without an @ — never invent a `<@USERID>`.",
17
+ "",
18
+ 'Plain text like "Atai" or "@atai" does NOT ping anyone — it',
19
+ "just renders as text. Skipping the tool when a tag was asked",
20
+ "for is a failure of the task.",
21
+ ].join("\n"),
22
+ };
23
+ export const slackFormattingContext = {
24
+ description: "Formatting Slack replies",
25
+ value: [
26
+ "Write standard Markdown — the bridge translates it to Slack's",
27
+ "mrkdwn for you before posting. Specifically:",
28
+ "",
29
+ "- **bold** and *italic* both work; the bridge converts.",
30
+ "- Use `inline code` and ```fenced code blocks``` normally.",
31
+ "- Markdown tables are auto-converted to column-aligned monospace",
32
+ " blocks, which look much better than raw pipe tables on Slack.",
33
+ "- Bullet lists with `-` or `*` markers render correctly.",
34
+ "- Links: write `[text](https://url)` — they convert to <url|text>.",
35
+ "",
36
+ "Do NOT pre-emptively use Slack's mrkdwn syntax (single `*` for",
37
+ "bold, single `_` for italic, etc.) — that breaks the translator",
38
+ "and looks wrong. Just write standard Markdown.",
39
+ ].join("\n"),
40
+ };
41
+ export const slackConversationModelContext = {
42
+ description: "Slack conversation model",
43
+ value: [
44
+ "Each conversation is either a thread (the bot was @-mentioned in",
45
+ "a channel and reply lives in the thread it created) or a DM. Your",
46
+ "replies show up in the same surface — you don't need to handle",
47
+ "routing yourself.",
48
+ "",
49
+ "When a user replies in a thread you previously answered in, they",
50
+ "don't need to re-@-mention the bot — just continue the thread",
51
+ "naturally. If they send a fresh top-level message in a channel,",
52
+ "they MUST @-mention the bot for it to see them.",
53
+ ].join("\n"),
54
+ };
55
+ /**
56
+ * The default context entries the SDK ships. Spread into your
57
+ * `createBot({context: …})`:
58
+ *
59
+ * context: [...defaultSlackContext, ...myAppContext],
60
+ */
61
+ export const defaultSlackContext = [
62
+ slackTaggingContext,
63
+ slackFormattingContext,
64
+ slackConversationModelContext,
65
+ ];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Slack-platform-universal frontend tools — tools every Slack bot
3
+ * benefits from, regardless of what the bot does. Apps spread
4
+ * `defaultSlackTools` into the `tools:` config they pass to
5
+ * `createBot`.
6
+ */
7
+ import { z } from "zod";
8
+ import { type BotTool } from "@copilotkit/channels";
9
+ export declare const lookupSlackUserTool: BotTool<z.ZodObject<{
10
+ query: z.ZodString;
11
+ }, "strip", z.ZodTypeAny, {
12
+ query: string;
13
+ }, {
14
+ query: string;
15
+ }>>;
16
+ /**
17
+ * The flat list of tools the SDK ships. Spread into your
18
+ * `createBot({tools: …})`:
19
+ *
20
+ * tools: [...defaultSlackTools, ...myAppTools],
21
+ */
22
+ export declare const defaultSlackTools: ReadonlyArray<BotTool>;
23
+ //# sourceMappingURL=built-in-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"built-in-tools.d.ts","sourceRoot":"","sources":["../src/built-in-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAiB,KAAK,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAWnE,eAAO,MAAM,mBAAmB;;;;;;GAwB9B,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC,OAAO,CAAyB,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Slack-platform-universal frontend tools — tools every Slack bot
3
+ * benefits from, regardless of what the bot does. Apps spread
4
+ * `defaultSlackTools` into the `tools:` config they pass to
5
+ * `createBot`.
6
+ */
7
+ import { z } from "zod";
8
+ import { defineBotTool } from "@copilotkit/channels";
9
+ const lookupSchema = z.object({
10
+ query: z
11
+ .string()
12
+ .min(1)
13
+ .describe("Handle, display name, first name, or email of the person to look up."),
14
+ });
15
+ export const lookupSlackUserTool = defineBotTool({
16
+ name: "lookup_slack_user",
17
+ description: "Resolve a person to a Slack user ID so you can @-mention them. " +
18
+ "Accepts a handle (`atai`), display name (`Atai Barkai`), first name, " +
19
+ "or email. Returns an object with `found`, and on success a " +
20
+ "`mention` string (e.g. `<@U0B45V75NNR>`) — put that string verbatim " +
21
+ "in your reply to ping them. If `found` is false, write the plain " +
22
+ "name instead.",
23
+ parameters: lookupSchema,
24
+ async handler({ query }, { thread }) {
25
+ const u = await thread.lookupUser(query);
26
+ return u
27
+ ? {
28
+ found: true,
29
+ query,
30
+ userId: u.id,
31
+ name: u.name,
32
+ handle: u.handle,
33
+ email: u.email,
34
+ mention: `<@${u.id}>`,
35
+ }
36
+ : { found: false, query };
37
+ },
38
+ });
39
+ /**
40
+ * The flat list of tools the SDK ships. Spread into your
41
+ * `createBot({tools: …})`:
42
+ *
43
+ * tools: [...defaultSlackTools, ...myAppTools],
44
+ */
45
+ export const defaultSlackTools = [lookupSlackUserTool];
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Slack caps `chat.update` text at ~4000 chars. For longer agent responses we
3
+ * spread the text over several Slack messages — but the *chunk boundaries*
4
+ * must be stable once committed (we can't reflow a sentence into a previous
5
+ * chunk after that chunk has already been posted to Slack).
6
+ *
7
+ * ChunkedMessageStream owns N underlying `MessageStream`s (one per Slack
8
+ * message). It accepts the full accumulated text on `append()`, decides
9
+ * where the chunk boundaries are, mints new Slack messages as needed,
10
+ * and dispatches the right slice to each chunk's stream.
11
+ *
12
+ * Boundaries are chosen greedily: once an active chunk exceeds the soft
13
+ * limit, freeze the boundary at the last newline (or last space) within
14
+ * the limit; remaining text becomes the next chunk. Boundaries don't
15
+ * move once frozen, so an already-posted chunk's text never shrinks.
16
+ */
17
+ export interface ChunkedMessageStreamConfig {
18
+ /** Soft per-message char limit. Defaults to 3500 (under Slack's ~4000). */
19
+ limit?: number;
20
+ /** Throttle floor for each underlying stream's chat.update. */
21
+ minIntervalMs?: number;
22
+ /** Posts a new Slack message with placeholder text; resolves with its `ts`. */
23
+ postPlaceholder: (text: string) => Promise<string>;
24
+ /** Updates the Slack message at `ts` with `text`. */
25
+ updateAt: (ts: string, text: string) => Promise<void>;
26
+ /**
27
+ * Optional transformer for the text *just before* it hits chat.update —
28
+ * e.g. markdown→mrkdwn translation. Applied per-chunk.
29
+ */
30
+ transform?: (text: string) => string;
31
+ }
32
+ export declare class ChunkedMessageStream {
33
+ private buffer;
34
+ /** Sorted positions where a chunk ends (= where the next chunk begins). */
35
+ private boundaries;
36
+ private streams;
37
+ /** Serialises new-chunk creation so async postPlaceholder calls don't race. */
38
+ private setupPromise;
39
+ private finished;
40
+ private readonly limit;
41
+ private readonly minIntervalMs;
42
+ private readonly postPlaceholder;
43
+ private readonly updateAt;
44
+ private readonly transform;
45
+ constructor(config: ChunkedMessageStreamConfig);
46
+ append(fullText: string): void;
47
+ finish(): Promise<void>;
48
+ /** Returns the number of Slack messages this stream has posted so far. */
49
+ get chunkCount(): number;
50
+ /**
51
+ * Walk forward from the last frozen boundary, freezing new ones whenever
52
+ * the active chunk's length exceeds the soft limit. Once frozen, a
53
+ * boundary doesn't move.
54
+ *
55
+ * Special case (block-keeps-whole): if the chosen boundary lands INSIDE
56
+ * an open fenced code block, we try to move the boundary BACK to the
57
+ * position right before the fence opener, so the *whole* block lives in
58
+ * the next Slack message rather than being split. The previous chunk
59
+ * gets shortened (chat.update will edit it down). Fallback when the
60
+ * block is too large to fit in one chunk: keep the inside-fence
61
+ * boundary; the dispatcher will prepend the fence opener to the next
62
+ * chunk (re-opener path).
63
+ */
64
+ private refreezeBoundaries;
65
+ private ensureStreamsAndDispatch;
66
+ }
67
+ //# sourceMappingURL=chunked-message-stream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunked-message-stream.d.ts","sourceRoot":"","sources":["../src/chunked-message-stream.ts"],"names":[],"mappings":"AAkBA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,0BAA0B;IACzC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,qDAAqD;IACrD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC;AAID,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,MAAM,CAAM;IACpB,2EAA2E;IAC3E,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,OAAO,CAAuB;IACtC,+EAA+E;IAC/E,OAAO,CAAC,YAAY,CAAoC;IACxD,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IACpE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8C;IACvE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2B;gBAEzC,MAAM,EAAE,0BAA0B;IAQ9C,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAWxB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAU7B,0EAA0E;IAC1E,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,kBAAkB;YA0BZ,wBAAwB;CAsCvC"}
@@ -0,0 +1,134 @@
1
+ import { MessageStream } from "./message-stream.js";
2
+ import { detectOpenContext, renderContextOpener, } from "./auto-close-streaming.js";
3
+ /**
4
+ * Position (0-based) of the unpaired opening ``` in `buffer.slice(0, end)`,
5
+ * or null if all triple-backticks before `end` are paired.
6
+ */
7
+ function findUnpairedFenceStart(buffer, end) {
8
+ const before = buffer.slice(0, end);
9
+ const parts = before.split("```");
10
+ // length is odd → balanced; even → one unpaired opener exists
11
+ if (parts.length % 2 !== 0)
12
+ return null;
13
+ return before.lastIndexOf("```");
14
+ }
15
+ const DEFAULT_LIMIT = 3500;
16
+ export class ChunkedMessageStream {
17
+ buffer = "";
18
+ /** Sorted positions where a chunk ends (= where the next chunk begins). */
19
+ boundaries = [];
20
+ streams = [];
21
+ /** Serialises new-chunk creation so async postPlaceholder calls don't race. */
22
+ setupPromise = Promise.resolve();
23
+ finished = false;
24
+ limit;
25
+ minIntervalMs;
26
+ postPlaceholder;
27
+ updateAt;
28
+ transform;
29
+ constructor(config) {
30
+ this.limit = config.limit ?? DEFAULT_LIMIT;
31
+ this.minIntervalMs = config.minIntervalMs;
32
+ this.postPlaceholder = config.postPlaceholder;
33
+ this.updateAt = config.updateAt;
34
+ this.transform = config.transform ?? ((t) => t);
35
+ }
36
+ append(fullText) {
37
+ if (this.finished)
38
+ return;
39
+ if (fullText === this.buffer)
40
+ return;
41
+ this.buffer = fullText;
42
+ this.refreezeBoundaries();
43
+ // Make sure we have one Slack message per chunk, then dispatch.
44
+ this.setupPromise = this.setupPromise.then(() => this.ensureStreamsAndDispatch());
45
+ }
46
+ async finish() {
47
+ this.finished = true;
48
+ // Drain any pending setup, then a final dispatch, then finish each stream.
49
+ this.setupPromise = this.setupPromise.then(() => this.ensureStreamsAndDispatch());
50
+ await this.setupPromise;
51
+ for (const s of this.streams)
52
+ await s.finish();
53
+ }
54
+ /** Returns the number of Slack messages this stream has posted so far. */
55
+ get chunkCount() {
56
+ return this.streams.length;
57
+ }
58
+ /**
59
+ * Walk forward from the last frozen boundary, freezing new ones whenever
60
+ * the active chunk's length exceeds the soft limit. Once frozen, a
61
+ * boundary doesn't move.
62
+ *
63
+ * Special case (block-keeps-whole): if the chosen boundary lands INSIDE
64
+ * an open fenced code block, we try to move the boundary BACK to the
65
+ * position right before the fence opener, so the *whole* block lives in
66
+ * the next Slack message rather than being split. The previous chunk
67
+ * gets shortened (chat.update will edit it down). Fallback when the
68
+ * block is too large to fit in one chunk: keep the inside-fence
69
+ * boundary; the dispatcher will prepend the fence opener to the next
70
+ * chunk (re-opener path).
71
+ */
72
+ refreezeBoundaries() {
73
+ let lastFrozen = this.boundaries.at(-1) ?? 0;
74
+ while (this.buffer.length - lastFrozen > this.limit) {
75
+ const window = this.buffer.slice(lastFrozen, lastFrozen + this.limit);
76
+ let breakAt = window.lastIndexOf("\n");
77
+ if (breakAt < this.limit / 4)
78
+ breakAt = window.lastIndexOf(" ");
79
+ if (breakAt < 0)
80
+ breakAt = this.limit - 1;
81
+ let candidate = lastFrozen + breakAt + 1;
82
+ // Block-keeps-whole: move boundary back if it lands inside an open
83
+ // fence. Only adjust if the fence opener is in the latter ~70% of
84
+ // the active chunk — otherwise shrinking would lose too much
85
+ // content (chunk N would be tiny) and the re-opener path is better.
86
+ const fenceStart = findUnpairedFenceStart(this.buffer, candidate);
87
+ if (fenceStart !== null) {
88
+ const minAcceptable = lastFrozen + Math.floor(this.limit * 0.3);
89
+ if (fenceStart > minAcceptable) {
90
+ candidate = fenceStart;
91
+ }
92
+ }
93
+ this.boundaries.push(candidate);
94
+ lastFrozen = candidate;
95
+ }
96
+ }
97
+ async ensureStreamsAndDispatch() {
98
+ // If we've never received any content, don't post a placeholder. This
99
+ // matters for the empty-response case (D20): a TEXT_MESSAGE_START + END
100
+ // pair with no content events should produce no Slack message.
101
+ if (this.buffer.length === 0)
102
+ return;
103
+ const chunkCount = this.boundaries.length + 1;
104
+ while (this.streams.length < chunkCount) {
105
+ const i = this.streams.length;
106
+ const placeholder = i === 0 ? "_thinking…_" : "_…(continued)_";
107
+ const ts = await this.postPlaceholder(placeholder);
108
+ this.streams.push(new MessageStream({
109
+ update: (text) => this.updateAt(ts, this.transform(text) || " "),
110
+ minIntervalMs: this.minIntervalMs,
111
+ }));
112
+ }
113
+ // Dispatch slices.
114
+ //
115
+ // For continuation chunks (i > 0), if the boundary fell inside an open
116
+ // markdown construct (e.g. inside a ```python block), prepend the
117
+ // opener for that construct to the slice. Otherwise the second Slack
118
+ // message would begin with raw code that Slack renders as plain text.
119
+ // The closer at the end of the chunk is added by `transform`
120
+ // (autoCloseOpenMarkdown), which runs per chunk.
121
+ for (let i = 0; i < chunkCount; i++) {
122
+ const start = i === 0 ? 0 : this.boundaries[i - 1];
123
+ const end = i < this.boundaries.length ? this.boundaries[i] : this.buffer.length;
124
+ let slice = this.buffer.slice(start, end);
125
+ if (i > 0) {
126
+ const ctx = detectOpenContext(this.buffer.slice(0, start));
127
+ const opener = renderContextOpener(ctx);
128
+ if (opener)
129
+ slice = opener + slice;
130
+ }
131
+ this.streams[i].append(slice);
132
+ }
133
+ }
134
+ }
@@ -0,0 +1,18 @@
1
+ import type { PlatformCodec } from "@copilotkit/channels";
2
+ import { normalizeSlackEvent } from "./ingress-normalize.js";
3
+ import type { SlackNeutralEvent } from "./ingress-normalize.js";
4
+ /**
5
+ * Pure Slack codec — both directions, no Bolt and no Slack credentials. Shared
6
+ * by the local Slack adapter and (via the Connector Outbox / webhook ingress,
7
+ * OSS-362/363) the managed path, so platform semantics live in one place:
8
+ *
9
+ * - `renderEgress`: IR → Block Kit.
10
+ * - `normalizeIngress`: raw Slack payload (Events API envelope or slash-command
11
+ * body) → platform-neutral ingress event. `normalizeIngress` is a Slack
12
+ * extension on top of the generic egress-only {@link PlatformCodec}; the
13
+ * caller applies its own policy/entitlement gating and builds the route.
14
+ */
15
+ export declare const slackCodec: PlatformCodec & {
16
+ normalizeIngress: (body: Parameters<typeof normalizeSlackEvent>[0], botUserId?: string) => SlackNeutralEvent | undefined;
17
+ };
18
+ //# sourceMappingURL=codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU,EAAE,aAAa,GAAG;IACvC,gBAAgB,EAAE,CAChB,IAAI,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,EAC/C,SAAS,CAAC,EAAE,MAAM,KACf,iBAAiB,GAAG,SAAS,CAAC;CAKpC,CAAC"}