slackhive 0.1.37 → 0.1.39

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 (542) hide show
  1. package/.dockerignore +14 -0
  2. package/.env.example +44 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.yml +65 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.yml +38 -0
  6. package/.github/PULL_REQUEST_TEMPLATE.md +27 -0
  7. package/.github/dependabot.yml +20 -0
  8. package/.github/workflows/audit.yml +149 -0
  9. package/.github/workflows/ci.yml +135 -0
  10. package/CHANGELOG.md +52 -0
  11. package/CODE_OF_CONDUCT.md +37 -0
  12. package/CONTRIBUTING.md +204 -0
  13. package/LICENSE +21 -0
  14. package/README.md +19 -0
  15. package/SECURITY.md +47 -0
  16. package/apps/runner/Dockerfile +33 -0
  17. package/apps/runner/dist/__tests__/channel-restrictions.test.d.ts +8 -0
  18. package/apps/runner/dist/__tests__/channel-restrictions.test.js +63 -0
  19. package/apps/runner/dist/__tests__/channel-restrictions.test.js.map +1 -0
  20. package/apps/runner/dist/__tests__/claude-handler-resolve.test.d.ts +20 -0
  21. package/apps/runner/dist/__tests__/claude-handler-resolve.test.js +178 -0
  22. package/apps/runner/dist/__tests__/claude-handler-resolve.test.js.map +1 -0
  23. package/apps/runner/dist/__tests__/compile-claude-md.test.d.ts +13 -0
  24. package/apps/runner/dist/__tests__/compile-claude-md.test.js +144 -0
  25. package/apps/runner/dist/__tests__/compile-claude-md.test.js.map +1 -0
  26. package/apps/runner/dist/__tests__/memory-sync.test.d.ts +11 -0
  27. package/apps/runner/dist/__tests__/memory-sync.test.js +56 -0
  28. package/apps/runner/dist/__tests__/memory-sync.test.js.map +1 -0
  29. package/apps/runner/dist/__tests__/slack-file-support.test.d.ts +9 -0
  30. package/apps/runner/dist/__tests__/slack-file-support.test.js +271 -0
  31. package/apps/runner/dist/__tests__/slack-file-support.test.js.map +1 -0
  32. package/apps/runner/dist/__tests__/slack-formatting.test.d.ts +12 -0
  33. package/apps/runner/dist/__tests__/slack-formatting.test.js +400 -0
  34. package/apps/runner/dist/__tests__/slack-formatting.test.js.map +1 -0
  35. package/apps/runner/dist/__tests__/thread-context.test.d.ts +12 -0
  36. package/apps/runner/dist/__tests__/thread-context.test.js +182 -0
  37. package/apps/runner/dist/__tests__/thread-context.test.js.map +1 -0
  38. package/apps/runner/dist/agent-runner.d.ts +118 -0
  39. package/apps/runner/dist/agent-runner.js +352 -0
  40. package/apps/runner/dist/agent-runner.js.map +1 -0
  41. package/apps/runner/dist/claude-handler.d.ts +122 -0
  42. package/apps/runner/dist/claude-handler.js +402 -0
  43. package/apps/runner/dist/claude-handler.js.map +1 -0
  44. package/apps/runner/dist/compile-claude-md.d.ts +59 -0
  45. package/apps/runner/dist/compile-claude-md.js +291 -0
  46. package/apps/runner/dist/compile-claude-md.js.map +1 -0
  47. package/apps/runner/dist/correction-handler.d.ts +46 -0
  48. package/apps/runner/dist/correction-handler.js +162 -0
  49. package/apps/runner/dist/correction-handler.js.map +1 -0
  50. package/apps/runner/dist/correction-manager.d.ts +53 -0
  51. package/apps/runner/dist/correction-manager.js +241 -0
  52. package/apps/runner/dist/correction-manager.js.map +1 -0
  53. package/apps/runner/dist/db.d.ts +193 -0
  54. package/apps/runner/dist/db.js +492 -0
  55. package/apps/runner/dist/db.js.map +1 -0
  56. package/apps/runner/dist/index.d.ts +9 -0
  57. package/apps/runner/dist/index.js +43 -0
  58. package/apps/runner/dist/index.js.map +1 -0
  59. package/apps/runner/dist/job-scheduler.d.ts +57 -0
  60. package/apps/runner/dist/job-scheduler.js +150 -0
  61. package/apps/runner/dist/job-scheduler.js.map +1 -0
  62. package/apps/runner/dist/logger.d.ts +32 -0
  63. package/apps/runner/dist/logger.js +52 -0
  64. package/apps/runner/dist/logger.js.map +1 -0
  65. package/apps/runner/dist/mcp-process-manager.d.ts +38 -0
  66. package/apps/runner/dist/mcp-process-manager.js +189 -0
  67. package/apps/runner/dist/mcp-process-manager.js.map +1 -0
  68. package/apps/runner/dist/memory-mcp.d.ts +14 -0
  69. package/apps/runner/dist/memory-mcp.js +88 -0
  70. package/apps/runner/dist/memory-mcp.js.map +1 -0
  71. package/apps/runner/dist/memory-watcher.d.ts +78 -0
  72. package/apps/runner/dist/memory-watcher.js +220 -0
  73. package/apps/runner/dist/memory-watcher.js.map +1 -0
  74. package/apps/runner/dist/slack-handler.d.ts +120 -0
  75. package/apps/runner/dist/slack-handler.js +843 -0
  76. package/apps/runner/dist/slack-handler.js.map +1 -0
  77. package/apps/runner/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  78. package/apps/runner/package.json +42 -0
  79. package/apps/runner/src/__tests__/channel-restrictions.test.ts +75 -0
  80. package/apps/runner/src/__tests__/claude-handler-resolve.test.ts +160 -0
  81. package/apps/runner/src/__tests__/compile-claude-md.test.ts +139 -0
  82. package/apps/runner/src/__tests__/memory-sync.test.ts +59 -0
  83. package/apps/runner/src/__tests__/slack-file-support.test.ts +376 -0
  84. package/apps/runner/src/__tests__/slack-formatting.test.ts +495 -0
  85. package/apps/runner/src/__tests__/thread-context.test.ts +215 -0
  86. package/apps/runner/src/agent-runner.ts +397 -0
  87. package/apps/runner/src/claude-handler.ts +475 -0
  88. package/apps/runner/src/compile-claude-md.ts +283 -0
  89. package/apps/runner/src/correction-handler.ts +191 -0
  90. package/apps/runner/src/correction-manager.ts +285 -0
  91. package/apps/runner/src/db.ts +604 -0
  92. package/apps/runner/src/index.ts +46 -0
  93. package/apps/runner/src/job-scheduler.ts +165 -0
  94. package/apps/runner/src/logger.ts +49 -0
  95. package/apps/runner/src/mcp-process-manager.ts +195 -0
  96. package/apps/runner/src/memory-mcp.ts +85 -0
  97. package/apps/runner/src/memory-watcher.ts +215 -0
  98. package/apps/runner/src/slack-handler.ts +929 -0
  99. package/apps/runner/tsconfig.json +17 -0
  100. package/apps/runner/vitest.config.mts +17 -0
  101. package/apps/web/.eslintrc.json +3 -0
  102. package/apps/web/.next/app-build-manifest.json +323 -0
  103. package/apps/web/.next/app-path-routes-manifest.json +46 -0
  104. package/apps/web/.next/build-manifest.json +33 -0
  105. package/apps/web/.next/cache/.previewinfo +1 -0
  106. package/apps/web/.next/cache/.rscinfo +1 -0
  107. package/apps/web/.next/cache/webpack/client-production/0.pack +0 -0
  108. package/apps/web/.next/cache/webpack/client-production/1.pack +0 -0
  109. package/apps/web/.next/cache/webpack/client-production/2.pack +0 -0
  110. package/apps/web/.next/cache/webpack/client-production/3.pack +0 -0
  111. package/apps/web/.next/cache/webpack/client-production/4.pack +0 -0
  112. package/apps/web/.next/cache/webpack/client-production/index.pack +0 -0
  113. package/apps/web/.next/cache/webpack/client-production/index.pack.old +0 -0
  114. package/apps/web/.next/cache/webpack/edge-server-production/0.pack +0 -0
  115. package/apps/web/.next/cache/webpack/edge-server-production/1.pack +0 -0
  116. package/apps/web/.next/cache/webpack/edge-server-production/index.pack +0 -0
  117. package/apps/web/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  118. package/apps/web/.next/cache/webpack/server-production/0.pack +0 -0
  119. package/apps/web/.next/cache/webpack/server-production/1.pack +0 -0
  120. package/apps/web/.next/cache/webpack/server-production/2.pack +0 -0
  121. package/apps/web/.next/cache/webpack/server-production/index.pack +0 -0
  122. package/apps/web/.next/cache/webpack/server-production/index.pack.old +0 -0
  123. package/apps/web/.next/diagnostics/build-diagnostics.json +6 -0
  124. package/apps/web/.next/diagnostics/framework.json +1 -0
  125. package/apps/web/.next/package.json +1 -0
  126. package/apps/web/.next/react-loadable-manifest.json +1 -0
  127. package/apps/web/.next/server/app/_not-found/page.js +2 -0
  128. package/apps/web/.next/server/app/_not-found/page.js.nft.json +1 -0
  129. package/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  130. package/apps/web/.next/server/app/agents/[slug]/page.js +4 -0
  131. package/apps/web/.next/server/app/agents/[slug]/page.js.nft.json +1 -0
  132. package/apps/web/.next/server/app/agents/[slug]/page_client-reference-manifest.js +1 -0
  133. package/apps/web/.next/server/app/agents/new/page.js +2 -0
  134. package/apps/web/.next/server/app/agents/new/page.js.nft.json +1 -0
  135. package/apps/web/.next/server/app/agents/new/page_client-reference-manifest.js +1 -0
  136. package/apps/web/.next/server/app/api/agents/[id]/access/route.js +1 -0
  137. package/apps/web/.next/server/app/api/agents/[id]/access/route.js.nft.json +1 -0
  138. package/apps/web/.next/server/app/api/agents/[id]/access/route_client-reference-manifest.js +1 -0
  139. package/apps/web/.next/server/app/api/agents/[id]/claude-md/route.js +6 -0
  140. package/apps/web/.next/server/app/api/agents/[id]/claude-md/route.js.nft.json +1 -0
  141. package/apps/web/.next/server/app/api/agents/[id]/claude-md/route_client-reference-manifest.js +1 -0
  142. package/apps/web/.next/server/app/api/agents/[id]/logs/route.js +3 -0
  143. package/apps/web/.next/server/app/api/agents/[id]/logs/route.js.nft.json +1 -0
  144. package/apps/web/.next/server/app/api/agents/[id]/logs/route_client-reference-manifest.js +1 -0
  145. package/apps/web/.next/server/app/api/agents/[id]/manifest/route.js +1 -0
  146. package/apps/web/.next/server/app/api/agents/[id]/manifest/route.js.nft.json +1 -0
  147. package/apps/web/.next/server/app/api/agents/[id]/manifest/route_client-reference-manifest.js +1 -0
  148. package/apps/web/.next/server/app/api/agents/[id]/mcps/route.js +1 -0
  149. package/apps/web/.next/server/app/api/agents/[id]/mcps/route.js.nft.json +1 -0
  150. package/apps/web/.next/server/app/api/agents/[id]/mcps/route_client-reference-manifest.js +1 -0
  151. package/apps/web/.next/server/app/api/agents/[id]/memories/[memId]/route.js +1 -0
  152. package/apps/web/.next/server/app/api/agents/[id]/memories/[memId]/route.js.nft.json +1 -0
  153. package/apps/web/.next/server/app/api/agents/[id]/memories/[memId]/route_client-reference-manifest.js +1 -0
  154. package/apps/web/.next/server/app/api/agents/[id]/memories/route.js +1 -0
  155. package/apps/web/.next/server/app/api/agents/[id]/memories/route.js.nft.json +1 -0
  156. package/apps/web/.next/server/app/api/agents/[id]/memories/route_client-reference-manifest.js +1 -0
  157. package/apps/web/.next/server/app/api/agents/[id]/permissions/route.js +1 -0
  158. package/apps/web/.next/server/app/api/agents/[id]/permissions/route.js.nft.json +1 -0
  159. package/apps/web/.next/server/app/api/agents/[id]/permissions/route_client-reference-manifest.js +1 -0
  160. package/apps/web/.next/server/app/api/agents/[id]/reload/route.js +1 -0
  161. package/apps/web/.next/server/app/api/agents/[id]/reload/route.js.nft.json +1 -0
  162. package/apps/web/.next/server/app/api/agents/[id]/reload/route_client-reference-manifest.js +1 -0
  163. package/apps/web/.next/server/app/api/agents/[id]/restrictions/route.js +1 -0
  164. package/apps/web/.next/server/app/api/agents/[id]/restrictions/route.js.nft.json +1 -0
  165. package/apps/web/.next/server/app/api/agents/[id]/restrictions/route_client-reference-manifest.js +1 -0
  166. package/apps/web/.next/server/app/api/agents/[id]/route.js +33 -0
  167. package/apps/web/.next/server/app/api/agents/[id]/route.js.nft.json +1 -0
  168. package/apps/web/.next/server/app/api/agents/[id]/route_client-reference-manifest.js +1 -0
  169. package/apps/web/.next/server/app/api/agents/[id]/skills/[skillId]/route.js +1 -0
  170. package/apps/web/.next/server/app/api/agents/[id]/skills/[skillId]/route.js.nft.json +1 -0
  171. package/apps/web/.next/server/app/api/agents/[id]/skills/[skillId]/route_client-reference-manifest.js +1 -0
  172. package/apps/web/.next/server/app/api/agents/[id]/skills/route.js +1 -0
  173. package/apps/web/.next/server/app/api/agents/[id]/skills/route.js.nft.json +1 -0
  174. package/apps/web/.next/server/app/api/agents/[id]/skills/route_client-reference-manifest.js +1 -0
  175. package/apps/web/.next/server/app/api/agents/[id]/slack-info/route.js +1 -0
  176. package/apps/web/.next/server/app/api/agents/[id]/slack-info/route.js.nft.json +1 -0
  177. package/apps/web/.next/server/app/api/agents/[id]/slack-info/route_client-reference-manifest.js +1 -0
  178. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/restore/route.js +1 -0
  179. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/restore/route.js.nft.json +1 -0
  180. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/restore/route_client-reference-manifest.js +1 -0
  181. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/route.js +1 -0
  182. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/route.js.nft.json +1 -0
  183. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/route_client-reference-manifest.js +1 -0
  184. package/apps/web/.next/server/app/api/agents/[id]/snapshots/route.js +1 -0
  185. package/apps/web/.next/server/app/api/agents/[id]/snapshots/route.js.nft.json +1 -0
  186. package/apps/web/.next/server/app/api/agents/[id]/snapshots/route_client-reference-manifest.js +1 -0
  187. package/apps/web/.next/server/app/api/agents/[id]/start/route.js +1 -0
  188. package/apps/web/.next/server/app/api/agents/[id]/start/route.js.nft.json +1 -0
  189. package/apps/web/.next/server/app/api/agents/[id]/start/route_client-reference-manifest.js +1 -0
  190. package/apps/web/.next/server/app/api/agents/[id]/stop/route.js +1 -0
  191. package/apps/web/.next/server/app/api/agents/[id]/stop/route.js.nft.json +1 -0
  192. package/apps/web/.next/server/app/api/agents/[id]/stop/route_client-reference-manifest.js +1 -0
  193. package/apps/web/.next/server/app/api/agents/route.js +91 -0
  194. package/apps/web/.next/server/app/api/agents/route.js.nft.json +1 -0
  195. package/apps/web/.next/server/app/api/agents/route_client-reference-manifest.js +1 -0
  196. package/apps/web/.next/server/app/api/auth/login/route.js +1 -0
  197. package/apps/web/.next/server/app/api/auth/login/route.js.nft.json +1 -0
  198. package/apps/web/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -0
  199. package/apps/web/.next/server/app/api/auth/logout/route.js +1 -0
  200. package/apps/web/.next/server/app/api/auth/logout/route.js.nft.json +1 -0
  201. package/apps/web/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -0
  202. package/apps/web/.next/server/app/api/auth/me/route.js +1 -0
  203. package/apps/web/.next/server/app/api/auth/me/route.js.nft.json +1 -0
  204. package/apps/web/.next/server/app/api/auth/me/route_client-reference-manifest.js +1 -0
  205. package/apps/web/.next/server/app/api/auth/users/[id]/route.js +1 -0
  206. package/apps/web/.next/server/app/api/auth/users/[id]/route.js.nft.json +1 -0
  207. package/apps/web/.next/server/app/api/auth/users/[id]/route_client-reference-manifest.js +1 -0
  208. package/apps/web/.next/server/app/api/auth/users/route.js +1 -0
  209. package/apps/web/.next/server/app/api/auth/users/route.js.nft.json +1 -0
  210. package/apps/web/.next/server/app/api/auth/users/route_client-reference-manifest.js +1 -0
  211. package/apps/web/.next/server/app/api/env-vars/[key]/route.js +1 -0
  212. package/apps/web/.next/server/app/api/env-vars/[key]/route.js.nft.json +1 -0
  213. package/apps/web/.next/server/app/api/env-vars/[key]/route_client-reference-manifest.js +1 -0
  214. package/apps/web/.next/server/app/api/env-vars/route.js +1 -0
  215. package/apps/web/.next/server/app/api/env-vars/route.js.nft.json +1 -0
  216. package/apps/web/.next/server/app/api/env-vars/route_client-reference-manifest.js +1 -0
  217. package/apps/web/.next/server/app/api/jobs/[id]/route.js +1 -0
  218. package/apps/web/.next/server/app/api/jobs/[id]/route.js.nft.json +1 -0
  219. package/apps/web/.next/server/app/api/jobs/[id]/route_client-reference-manifest.js +1 -0
  220. package/apps/web/.next/server/app/api/jobs/[id]/runs/route.js +1 -0
  221. package/apps/web/.next/server/app/api/jobs/[id]/runs/route.js.nft.json +1 -0
  222. package/apps/web/.next/server/app/api/jobs/[id]/runs/route_client-reference-manifest.js +1 -0
  223. package/apps/web/.next/server/app/api/jobs/route.js +1 -0
  224. package/apps/web/.next/server/app/api/jobs/route.js.nft.json +1 -0
  225. package/apps/web/.next/server/app/api/jobs/route_client-reference-manifest.js +1 -0
  226. package/apps/web/.next/server/app/api/mcps/[id]/route.js +1 -0
  227. package/apps/web/.next/server/app/api/mcps/[id]/route.js.nft.json +1 -0
  228. package/apps/web/.next/server/app/api/mcps/[id]/route_client-reference-manifest.js +1 -0
  229. package/apps/web/.next/server/app/api/mcps/[id]/test/route.js +1 -0
  230. package/apps/web/.next/server/app/api/mcps/[id]/test/route.js.nft.json +1 -0
  231. package/apps/web/.next/server/app/api/mcps/[id]/test/route_client-reference-manifest.js +1 -0
  232. package/apps/web/.next/server/app/api/mcps/route.js +1 -0
  233. package/apps/web/.next/server/app/api/mcps/route.js.nft.json +1 -0
  234. package/apps/web/.next/server/app/api/mcps/route_client-reference-manifest.js +1 -0
  235. package/apps/web/.next/server/app/api/settings/route.js +1 -0
  236. package/apps/web/.next/server/app/api/settings/route.js.nft.json +1 -0
  237. package/apps/web/.next/server/app/api/settings/route_client-reference-manifest.js +1 -0
  238. package/apps/web/.next/server/app/icon.svg/route.js +1 -0
  239. package/apps/web/.next/server/app/icon.svg/route.js.nft.json +1 -0
  240. package/apps/web/.next/server/app/jobs/page.js +2 -0
  241. package/apps/web/.next/server/app/jobs/page.js.nft.json +1 -0
  242. package/apps/web/.next/server/app/jobs/page_client-reference-manifest.js +1 -0
  243. package/apps/web/.next/server/app/login/page.js +2 -0
  244. package/apps/web/.next/server/app/login/page.js.nft.json +1 -0
  245. package/apps/web/.next/server/app/login/page_client-reference-manifest.js +1 -0
  246. package/apps/web/.next/server/app/page.js +2 -0
  247. package/apps/web/.next/server/app/page.js.nft.json +1 -0
  248. package/apps/web/.next/server/app/page_client-reference-manifest.js +1 -0
  249. package/apps/web/.next/server/app/settings/env-vars/page.js +2 -0
  250. package/apps/web/.next/server/app/settings/env-vars/page.js.nft.json +1 -0
  251. package/apps/web/.next/server/app/settings/env-vars/page_client-reference-manifest.js +1 -0
  252. package/apps/web/.next/server/app/settings/mcps/page.js +2 -0
  253. package/apps/web/.next/server/app/settings/mcps/page.js.nft.json +1 -0
  254. package/apps/web/.next/server/app/settings/mcps/page_client-reference-manifest.js +1 -0
  255. package/apps/web/.next/server/app/settings/page.js +2 -0
  256. package/apps/web/.next/server/app/settings/page.js.nft.json +1 -0
  257. package/apps/web/.next/server/app/settings/page_client-reference-manifest.js +1 -0
  258. package/apps/web/.next/server/app-paths-manifest.json +46 -0
  259. package/apps/web/.next/server/chunks/1157.js +9 -0
  260. package/apps/web/.next/server/chunks/2287.js +1 -0
  261. package/apps/web/.next/server/chunks/3444.js +1 -0
  262. package/apps/web/.next/server/chunks/383.js +6 -0
  263. package/apps/web/.next/server/chunks/4012.js +58 -0
  264. package/apps/web/.next/server/chunks/6791.js +1 -0
  265. package/apps/web/.next/server/chunks/7171.js +1 -0
  266. package/apps/web/.next/server/chunks/8819.js +22 -0
  267. package/apps/web/.next/server/edge-runtime-webpack.js +2 -0
  268. package/apps/web/.next/server/edge-runtime-webpack.js.map +1 -0
  269. package/apps/web/.next/server/interception-route-rewrite-manifest.js +1 -0
  270. package/apps/web/.next/server/middleware-build-manifest.js +1 -0
  271. package/apps/web/.next/server/middleware-manifest.json +32 -0
  272. package/apps/web/.next/server/middleware-react-loadable-manifest.js +1 -0
  273. package/apps/web/.next/server/next-font-manifest.js +1 -0
  274. package/apps/web/.next/server/next-font-manifest.json +1 -0
  275. package/apps/web/.next/server/pages/_app.js +1 -0
  276. package/apps/web/.next/server/pages/_app.js.nft.json +1 -0
  277. package/apps/web/.next/server/pages/_document.js +1 -0
  278. package/apps/web/.next/server/pages/_document.js.nft.json +1 -0
  279. package/apps/web/.next/server/pages/_error.js +19 -0
  280. package/apps/web/.next/server/pages/_error.js.nft.json +1 -0
  281. package/apps/web/.next/server/pages-manifest.json +5 -0
  282. package/apps/web/.next/server/server-reference-manifest.js +1 -0
  283. package/apps/web/.next/server/server-reference-manifest.json +1 -0
  284. package/apps/web/.next/server/src/middleware.js +14 -0
  285. package/apps/web/.next/server/src/middleware.js.map +1 -0
  286. package/apps/web/.next/server/webpack-runtime.js +1 -0
  287. package/apps/web/.next/static/chunks/18-90b700ea37b686a2.js +1 -0
  288. package/apps/web/.next/static/chunks/87c73c54-24122e7b92478d00.js +1 -0
  289. package/apps/web/.next/static/chunks/9664-af80478aa73ba424.js +1 -0
  290. package/apps/web/.next/static/chunks/app/_not-found/page-b9cee17ed89ca24a.js +1 -0
  291. package/apps/web/.next/static/chunks/app/agents/[slug]/page-18369fc3fe1a9a7b.js +1 -0
  292. package/apps/web/.next/static/chunks/app/agents/new/page-bf11cf8901c7e2cd.js +1 -0
  293. package/apps/web/.next/static/chunks/app/api/agents/[id]/access/route-07f0f73ac9839899.js +1 -0
  294. package/apps/web/.next/static/chunks/app/api/agents/[id]/claude-md/route-07f0f73ac9839899.js +1 -0
  295. package/apps/web/.next/static/chunks/app/api/agents/[id]/logs/route-07f0f73ac9839899.js +1 -0
  296. package/apps/web/.next/static/chunks/app/api/agents/[id]/manifest/route-07f0f73ac9839899.js +1 -0
  297. package/apps/web/.next/static/chunks/app/api/agents/[id]/mcps/route-07f0f73ac9839899.js +1 -0
  298. package/apps/web/.next/static/chunks/app/api/agents/[id]/memories/[memId]/route-07f0f73ac9839899.js +1 -0
  299. package/apps/web/.next/static/chunks/app/api/agents/[id]/memories/route-07f0f73ac9839899.js +1 -0
  300. package/apps/web/.next/static/chunks/app/api/agents/[id]/permissions/route-07f0f73ac9839899.js +1 -0
  301. package/apps/web/.next/static/chunks/app/api/agents/[id]/reload/route-07f0f73ac9839899.js +1 -0
  302. package/apps/web/.next/static/chunks/app/api/agents/[id]/restrictions/route-07f0f73ac9839899.js +1 -0
  303. package/apps/web/.next/static/chunks/app/api/agents/[id]/route-07f0f73ac9839899.js +1 -0
  304. package/apps/web/.next/static/chunks/app/api/agents/[id]/skills/[skillId]/route-07f0f73ac9839899.js +1 -0
  305. package/apps/web/.next/static/chunks/app/api/agents/[id]/skills/route-07f0f73ac9839899.js +1 -0
  306. package/apps/web/.next/static/chunks/app/api/agents/[id]/slack-info/route-07f0f73ac9839899.js +1 -0
  307. package/apps/web/.next/static/chunks/app/api/agents/[id]/snapshots/[sid]/restore/route-07f0f73ac9839899.js +1 -0
  308. package/apps/web/.next/static/chunks/app/api/agents/[id]/snapshots/[sid]/route-07f0f73ac9839899.js +1 -0
  309. package/apps/web/.next/static/chunks/app/api/agents/[id]/snapshots/route-07f0f73ac9839899.js +1 -0
  310. package/apps/web/.next/static/chunks/app/api/agents/[id]/start/route-07f0f73ac9839899.js +1 -0
  311. package/apps/web/.next/static/chunks/app/api/agents/[id]/stop/route-07f0f73ac9839899.js +1 -0
  312. package/apps/web/.next/static/chunks/app/api/agents/route-07f0f73ac9839899.js +1 -0
  313. package/apps/web/.next/static/chunks/app/api/auth/login/route-07f0f73ac9839899.js +1 -0
  314. package/apps/web/.next/static/chunks/app/api/auth/logout/route-07f0f73ac9839899.js +1 -0
  315. package/apps/web/.next/static/chunks/app/api/auth/me/route-07f0f73ac9839899.js +1 -0
  316. package/apps/web/.next/static/chunks/app/api/auth/users/[id]/route-07f0f73ac9839899.js +1 -0
  317. package/apps/web/.next/static/chunks/app/api/auth/users/route-07f0f73ac9839899.js +1 -0
  318. package/apps/web/.next/static/chunks/app/api/env-vars/[key]/route-07f0f73ac9839899.js +1 -0
  319. package/apps/web/.next/static/chunks/app/api/env-vars/route-07f0f73ac9839899.js +1 -0
  320. package/apps/web/.next/static/chunks/app/api/jobs/[id]/route-07f0f73ac9839899.js +1 -0
  321. package/apps/web/.next/static/chunks/app/api/jobs/[id]/runs/route-07f0f73ac9839899.js +1 -0
  322. package/apps/web/.next/static/chunks/app/api/jobs/route-07f0f73ac9839899.js +1 -0
  323. package/apps/web/.next/static/chunks/app/api/mcps/[id]/route-07f0f73ac9839899.js +1 -0
  324. package/apps/web/.next/static/chunks/app/api/mcps/[id]/test/route-07f0f73ac9839899.js +1 -0
  325. package/apps/web/.next/static/chunks/app/api/mcps/route-07f0f73ac9839899.js +1 -0
  326. package/apps/web/.next/static/chunks/app/api/settings/route-07f0f73ac9839899.js +1 -0
  327. package/apps/web/.next/static/chunks/app/jobs/page-f5aa89a47c50efd8.js +1 -0
  328. package/apps/web/.next/static/chunks/app/layout-2079f4964aa7314e.js +1 -0
  329. package/apps/web/.next/static/chunks/app/login/layout-07f0f73ac9839899.js +1 -0
  330. package/apps/web/.next/static/chunks/app/login/page-aa259283dc38e8f9.js +1 -0
  331. package/apps/web/.next/static/chunks/app/page-e83437b608104dff.js +1 -0
  332. package/apps/web/.next/static/chunks/app/settings/env-vars/page-06479dbdfb78b76b.js +1 -0
  333. package/apps/web/.next/static/chunks/app/settings/mcps/page-75650686ed6490c7.js +1 -0
  334. package/apps/web/.next/static/chunks/app/settings/page-e1e62fc41ff6cddd.js +1 -0
  335. package/apps/web/.next/static/chunks/framework-811407f832a33072.js +1 -0
  336. package/apps/web/.next/static/chunks/main-3f1cddbdd67b1546.js +1 -0
  337. package/apps/web/.next/static/chunks/main-app-cebd8a6a5ccbf72d.js +1 -0
  338. package/apps/web/.next/static/chunks/pages/_app-50fa07b56b2d29ac.js +1 -0
  339. package/apps/web/.next/static/chunks/pages/_error-fed8688bdd23f211.js +1 -0
  340. package/apps/web/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  341. package/apps/web/.next/static/chunks/webpack-6c05566dba553c97.js +1 -0
  342. package/apps/web/.next/static/css/15371687405525e2.css +5 -0
  343. package/apps/web/.next/static/ikfNbLhuw7jntn35bz0lk/_buildManifest.js +1 -0
  344. package/apps/web/.next/static/ikfNbLhuw7jntn35bz0lk/_ssgManifest.js +1 -0
  345. package/apps/web/.next/trace +5 -0
  346. package/apps/web/.next/types/app/agents/[slug]/page.ts +84 -0
  347. package/apps/web/.next/types/app/agents/new/page.ts +84 -0
  348. package/apps/web/.next/types/app/api/agents/[id]/access/route.ts +347 -0
  349. package/apps/web/.next/types/app/api/agents/[id]/claude-md/route.ts +347 -0
  350. package/apps/web/.next/types/app/api/agents/[id]/logs/route.ts +347 -0
  351. package/apps/web/.next/types/app/api/agents/[id]/manifest/route.ts +347 -0
  352. package/apps/web/.next/types/app/api/agents/[id]/mcps/route.ts +347 -0
  353. package/apps/web/.next/types/app/api/agents/[id]/memories/[memId]/route.ts +347 -0
  354. package/apps/web/.next/types/app/api/agents/[id]/memories/route.ts +347 -0
  355. package/apps/web/.next/types/app/api/agents/[id]/permissions/route.ts +347 -0
  356. package/apps/web/.next/types/app/api/agents/[id]/reload/route.ts +347 -0
  357. package/apps/web/.next/types/app/api/agents/[id]/restrictions/route.ts +347 -0
  358. package/apps/web/.next/types/app/api/agents/[id]/route.ts +347 -0
  359. package/apps/web/.next/types/app/api/agents/[id]/skills/[skillId]/route.ts +347 -0
  360. package/apps/web/.next/types/app/api/agents/[id]/skills/route.ts +347 -0
  361. package/apps/web/.next/types/app/api/agents/[id]/slack-info/route.ts +347 -0
  362. package/apps/web/.next/types/app/api/agents/[id]/snapshots/[sid]/restore/route.ts +347 -0
  363. package/apps/web/.next/types/app/api/agents/[id]/snapshots/[sid]/route.ts +347 -0
  364. package/apps/web/.next/types/app/api/agents/[id]/snapshots/route.ts +347 -0
  365. package/apps/web/.next/types/app/api/agents/[id]/start/route.ts +347 -0
  366. package/apps/web/.next/types/app/api/agents/[id]/stop/route.ts +347 -0
  367. package/apps/web/.next/types/app/api/agents/route.ts +347 -0
  368. package/apps/web/.next/types/app/api/auth/login/route.ts +347 -0
  369. package/apps/web/.next/types/app/api/auth/logout/route.ts +347 -0
  370. package/apps/web/.next/types/app/api/auth/me/route.ts +347 -0
  371. package/apps/web/.next/types/app/api/auth/users/[id]/route.ts +347 -0
  372. package/apps/web/.next/types/app/api/auth/users/route.ts +347 -0
  373. package/apps/web/.next/types/app/api/env-vars/[key]/route.ts +347 -0
  374. package/apps/web/.next/types/app/api/env-vars/route.ts +347 -0
  375. package/apps/web/.next/types/app/api/jobs/[id]/route.ts +347 -0
  376. package/apps/web/.next/types/app/api/jobs/[id]/runs/route.ts +347 -0
  377. package/apps/web/.next/types/app/api/jobs/route.ts +347 -0
  378. package/apps/web/.next/types/app/api/mcps/[id]/route.ts +347 -0
  379. package/apps/web/.next/types/app/api/mcps/[id]/test/route.ts +347 -0
  380. package/apps/web/.next/types/app/api/mcps/route.ts +347 -0
  381. package/apps/web/.next/types/app/api/settings/route.ts +347 -0
  382. package/apps/web/.next/types/app/jobs/page.ts +84 -0
  383. package/apps/web/.next/types/app/login/layout.ts +84 -0
  384. package/apps/web/.next/types/app/login/page.ts +84 -0
  385. package/apps/web/.next/types/app/page.ts +84 -0
  386. package/apps/web/.next/types/app/settings/env-vars/page.ts +84 -0
  387. package/apps/web/.next/types/app/settings/mcps/page.ts +84 -0
  388. package/apps/web/.next/types/app/settings/page.ts +84 -0
  389. package/apps/web/.next/types/cache-life.d.ts +141 -0
  390. package/apps/web/.next/types/package.json +1 -0
  391. package/apps/web/.next/types/routes.d.ts +114 -0
  392. package/apps/web/.next/types/validator.ts +448 -0
  393. package/apps/web/Dockerfile +37 -0
  394. package/apps/web/next-env.d.ts +6 -0
  395. package/apps/web/next.config.js +6 -0
  396. package/apps/web/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  397. package/apps/web/package.json +48 -0
  398. package/apps/web/postcss.config.js +3 -0
  399. package/apps/web/public/logo.svg +17 -0
  400. package/apps/web/src/app/agents/[slug]/page.tsx +2235 -0
  401. package/apps/web/src/app/agents/new/page.tsx +1161 -0
  402. package/apps/web/src/app/api/agents/[id]/access/route.ts +76 -0
  403. package/apps/web/src/app/api/agents/[id]/claude-md/route.ts +111 -0
  404. package/apps/web/src/app/api/agents/[id]/logs/route.ts +84 -0
  405. package/apps/web/src/app/api/agents/[id]/manifest/route.ts +32 -0
  406. package/apps/web/src/app/api/agents/[id]/mcps/route.ts +73 -0
  407. package/apps/web/src/app/api/agents/[id]/memories/[memId]/route.ts +31 -0
  408. package/apps/web/src/app/api/agents/[id]/memories/route.ts +56 -0
  409. package/apps/web/src/app/api/agents/[id]/permissions/route.ts +74 -0
  410. package/apps/web/src/app/api/agents/[id]/reload/route.ts +33 -0
  411. package/apps/web/src/app/api/agents/[id]/restrictions/route.ts +85 -0
  412. package/apps/web/src/app/api/agents/[id]/route.ts +81 -0
  413. package/apps/web/src/app/api/agents/[id]/skills/[skillId]/route.ts +52 -0
  414. package/apps/web/src/app/api/agents/[id]/skills/route.ts +80 -0
  415. package/apps/web/src/app/api/agents/[id]/slack-info/route.ts +38 -0
  416. package/apps/web/src/app/api/agents/[id]/snapshots/[sid]/restore/route.ts +61 -0
  417. package/apps/web/src/app/api/agents/[id]/snapshots/[sid]/route.ts +53 -0
  418. package/apps/web/src/app/api/agents/[id]/snapshots/route.ts +84 -0
  419. package/apps/web/src/app/api/agents/[id]/start/route.ts +35 -0
  420. package/apps/web/src/app/api/agents/[id]/stop/route.ts +35 -0
  421. package/apps/web/src/app/api/agents/route.ts +99 -0
  422. package/apps/web/src/app/api/auth/login/route.ts +39 -0
  423. package/apps/web/src/app/api/auth/logout/route.ts +21 -0
  424. package/apps/web/src/app/api/auth/me/route.ts +24 -0
  425. package/apps/web/src/app/api/auth/users/[id]/route.ts +48 -0
  426. package/apps/web/src/app/api/auth/users/route.ts +63 -0
  427. package/apps/web/src/app/api/env-vars/[key]/route.ts +66 -0
  428. package/apps/web/src/app/api/env-vars/route.ts +59 -0
  429. package/apps/web/src/app/api/jobs/[id]/route.ts +51 -0
  430. package/apps/web/src/app/api/jobs/[id]/runs/route.ts +24 -0
  431. package/apps/web/src/app/api/jobs/route.ts +42 -0
  432. package/apps/web/src/app/api/mcps/[id]/route.ts +60 -0
  433. package/apps/web/src/app/api/mcps/[id]/test/route.ts +195 -0
  434. package/apps/web/src/app/api/mcps/route.ts +72 -0
  435. package/apps/web/src/app/api/settings/route.ts +42 -0
  436. package/apps/web/src/app/globals.css +124 -0
  437. package/apps/web/src/app/icon.svg +17 -0
  438. package/apps/web/src/app/jobs/page.tsx +543 -0
  439. package/apps/web/src/app/layout-shell.tsx +89 -0
  440. package/apps/web/src/app/layout.tsx +18 -0
  441. package/apps/web/src/app/login/layout.tsx +9 -0
  442. package/apps/web/src/app/login/page.tsx +150 -0
  443. package/apps/web/src/app/page.tsx +573 -0
  444. package/apps/web/src/app/settings/env-vars/page.tsx +216 -0
  445. package/apps/web/src/app/settings/mcps/page.tsx +763 -0
  446. package/apps/web/src/app/settings/page.tsx +528 -0
  447. package/apps/web/src/app/sidebar.tsx +345 -0
  448. package/apps/web/src/lib/__tests__/api-guard.test.ts +189 -0
  449. package/apps/web/src/lib/__tests__/auth.test.ts +262 -0
  450. package/apps/web/src/lib/__tests__/boss-registry.test.ts +323 -0
  451. package/apps/web/src/lib/__tests__/compile.test.ts +161 -0
  452. package/apps/web/src/lib/__tests__/db-agent-hierarchy.test.ts +136 -0
  453. package/apps/web/src/lib/__tests__/db-env-vars.test.ts +216 -0
  454. package/apps/web/src/lib/__tests__/db-restrictions.test.ts +117 -0
  455. package/apps/web/src/lib/__tests__/db.integration.test.ts +271 -0
  456. package/apps/web/src/lib/__tests__/diff.test.ts +102 -0
  457. package/apps/web/src/lib/__tests__/mcp-mask.test.ts +274 -0
  458. package/apps/web/src/lib/__tests__/skill-templates.test.ts +237 -0
  459. package/apps/web/src/lib/__tests__/slack-manifest.test.ts +105 -0
  460. package/apps/web/src/lib/api-guard.ts +68 -0
  461. package/apps/web/src/lib/auth-context.tsx +71 -0
  462. package/apps/web/src/lib/auth.ts +128 -0
  463. package/apps/web/src/lib/boss-registry.ts +90 -0
  464. package/apps/web/src/lib/compile.ts +51 -0
  465. package/apps/web/src/lib/db.ts +1196 -0
  466. package/apps/web/src/lib/diff.ts +43 -0
  467. package/apps/web/src/lib/mcp-mask.ts +91 -0
  468. package/apps/web/src/lib/portal.tsx +23 -0
  469. package/apps/web/src/lib/skill-templates.ts +148 -0
  470. package/apps/web/src/lib/slack-manifest.ts +85 -0
  471. package/apps/web/src/middleware.ts +68 -0
  472. package/apps/web/tailwind.config.js +6 -0
  473. package/apps/web/tsconfig.json +23 -0
  474. package/apps/web/vitest.config.mts +21 -0
  475. package/cli/.claude/settings.local.json +6 -0
  476. package/cli/README.md +281 -0
  477. package/cli/node_modules/.package-lock.json +427 -0
  478. package/cli/node_modules/commander/LICENSE +22 -0
  479. package/cli/node_modules/commander/Readme.md +1157 -0
  480. package/cli/node_modules/commander/esm.mjs +16 -0
  481. package/cli/node_modules/commander/index.js +24 -0
  482. package/cli/node_modules/commander/lib/argument.js +149 -0
  483. package/cli/node_modules/commander/lib/command.js +2509 -0
  484. package/cli/node_modules/commander/lib/error.js +39 -0
  485. package/cli/node_modules/commander/lib/help.js +520 -0
  486. package/cli/node_modules/commander/lib/option.js +330 -0
  487. package/cli/node_modules/commander/lib/suggestSimilar.js +101 -0
  488. package/cli/node_modules/commander/package-support.json +16 -0
  489. package/cli/node_modules/commander/package.json +84 -0
  490. package/cli/node_modules/commander/typings/esm.d.mts +3 -0
  491. package/cli/node_modules/commander/typings/index.d.ts +969 -0
  492. package/cli/package-lock.json +449 -0
  493. package/cli/package.json +44 -0
  494. package/cli/src/commands/init.ts +514 -0
  495. package/cli/src/commands/manage.ts +115 -0
  496. package/cli/src/index.ts +63 -0
  497. package/cli/tsconfig.json +14 -0
  498. package/docker-compose.yml +122 -0
  499. package/docs/agents/boss-agents.mdx +108 -0
  500. package/docs/agents/creating-agents.mdx +132 -0
  501. package/docs/agents/memory.mdx +113 -0
  502. package/docs/agents/tools.mdx +103 -0
  503. package/docs/configuration/env-vars.mdx +166 -0
  504. package/docs/configuration/mcp-servers.mdx +203 -0
  505. package/docs/configuration/slack-app.mdx +175 -0
  506. package/docs/docs.json +79 -0
  507. package/docs/favicon.svg +17 -0
  508. package/docs/features/history.mdx +60 -0
  509. package/docs/features/import-export.mdx +77 -0
  510. package/docs/features/logs.mdx +131 -0
  511. package/docs/features/multi-workspace.mdx +90 -0
  512. package/docs/features/scheduled-jobs.mdx +231 -0
  513. package/docs/features/users.mdx +92 -0
  514. package/docs/introduction.mdx +160 -0
  515. package/docs/logo/dark.svg +17 -0
  516. package/docs/logo/light.svg +17 -0
  517. package/docs/logo/wide-dark.svg +12 -0
  518. package/docs/logo/wide-light.svg +12 -0
  519. package/docs/quickstart.mdx +270 -0
  520. package/docs/self-hosting/docker.mdx +151 -0
  521. package/docs/self-hosting/production.mdx +176 -0
  522. package/package.json +20 -36
  523. package/packages/shared/dist/index.d.ts +8 -0
  524. package/packages/shared/dist/index.d.ts.map +1 -0
  525. package/packages/shared/dist/index.js +24 -0
  526. package/packages/shared/dist/index.js.map +1 -0
  527. package/packages/shared/dist/types.d.ts +584 -0
  528. package/packages/shared/dist/types.d.ts.map +1 -0
  529. package/packages/shared/dist/types.js +39 -0
  530. package/packages/shared/dist/types.js.map +1 -0
  531. package/packages/shared/package.json +15 -0
  532. package/packages/shared/src/db/schema.sql +354 -0
  533. package/packages/shared/src/index.ts +8 -0
  534. package/packages/shared/src/types.ts +683 -0
  535. package/packages/shared/tsconfig.json +17 -0
  536. package/scripts/dev.sh +45 -0
  537. /package/{dist → cli/dist}/commands/init.d.ts +0 -0
  538. /package/{dist → cli/dist}/commands/init.js +0 -0
  539. /package/{dist → cli/dist}/commands/manage.d.ts +0 -0
  540. /package/{dist → cli/dist}/commands/manage.js +0 -0
  541. /package/{dist → cli/dist}/index.d.ts +0 -0
  542. /package/{dist → cli/dist}/index.js +0 -0
@@ -0,0 +1,929 @@
1
+ /**
2
+ * @fileoverview Slack event handler for a single agent's Bolt App.
3
+ *
4
+ * Key behaviours:
5
+ * - Abort/cancel: new message in same thread cancels the in-flight request
6
+ * - Tool status: shows friendly "Querying Redshift…" live in the status message
7
+ * - Fallback text: uses lastAssistantText if no messages were sent during stream
8
+ * - result message: extracts final result from SDK result.subtype === 'success'
9
+ * - Reaction cycling: thinking_face → gear → white_check_mark / x
10
+ * - Markdown→Slack formatting: **bold** → *bold*, headings, tables, code blocks
11
+ * - Block Kit: tables rendered as native Slack table blocks with section chunks
12
+ *
13
+ * @module runner/slack-handler
14
+ */
15
+
16
+ import type { App, KnownEventFromType } from '@slack/bolt';
17
+ import type { Agent, Restriction } from '@slackhive/shared';
18
+ import type { ClaudeHandler } from './claude-handler';
19
+ import { CorrectionHandler } from './correction-handler';
20
+ import { agentLogger } from './logger';
21
+ import type { Logger } from 'winston';
22
+ import type { ContentBlockParam } from '@anthropic-ai/sdk/resources';
23
+
24
+ const MAX_THREAD_CONTEXT_MESSAGES = 20;
25
+ const MAX_THREAD_CONTEXT_CHARS = 8_000;
26
+
27
+ /** Friendly labels shown in the status message while a tool is running. */
28
+ const MCP_TOOL_LABELS: Record<string, string> = {
29
+ 'mcp__redshift-mcp__query': 'Querying Redshift',
30
+ 'mcp__redshift-mcp__describe_table': 'Inspecting table structure',
31
+ 'mcp__redshift-mcp__find_column': 'Searching for columns',
32
+ 'mcp__mcp-server-openmetadata-PRD__search_entities': 'Searching metadata catalog',
33
+ 'mcp__mcp-server-openmetadata-PRD__suggest_entities': 'Looking up suggestions',
34
+ 'mcp__mcp-server-openmetadata-PRD__get_table_by_name': 'Getting table details',
35
+ 'mcp__mcp-server-openmetadata-PRD__get_table': 'Getting table details',
36
+ 'mcp__mcp-server-openmetadata-PRD__list_tables': 'Listing tables',
37
+ 'mcp__mcp-server-openmetadata-PRD__get_metric': 'Getting metric definition',
38
+ 'mcp__mcp-server-openmetadata-PRD__get_metric_by_name': 'Getting metric definition',
39
+ 'mcp__mcp-server-openmetadata-PRD__list_metrics': 'Listing metrics',
40
+ 'mcp__mcp-server-openmetadata-PRD__get_glossary': 'Getting glossary',
41
+ 'mcp__mcp-server-openmetadata-PRD__get_glossary_by_name': 'Getting glossary',
42
+ 'mcp__mcp-server-openmetadata-PRD__list_glossaries': 'Listing glossaries',
43
+ 'mcp__mcp-server-openmetadata-PRD__get_glossary_term': 'Getting glossary term',
44
+ 'mcp__mcp-server-openmetadata-PRD__list_glossary_terms': 'Listing glossary terms',
45
+ 'mcp__mcp-server-openmetadata-PRD__get_schema': 'Getting schema info',
46
+ 'mcp__mcp-server-openmetadata-PRD__get_schema_by_name': 'Getting schema info',
47
+ 'mcp__mcp-server-openmetadata-PRD__list_schemas': 'Listing schemas',
48
+ 'mcp__mcp-server-openmetadata-PRD__get_database': 'Getting database info',
49
+ 'mcp__mcp-server-openmetadata-PRD__get_database_by_name': 'Getting database info',
50
+ 'mcp__mcp-server-openmetadata-PRD__list_databases': 'Listing databases',
51
+ 'mcp__mcp-server-openmetadata-PRD__get_lineage': 'Getting data lineage',
52
+ 'mcp__mcp-server-openmetadata-PRD__get_lineage_by_name': 'Getting data lineage',
53
+ 'mcp__mcp-server-openmetadata-PRD__search_field_query': 'Searching fields',
54
+ 'mcp__mcp-server-openmetadata-PRD__search_aggregate': 'Searching aggregations',
55
+ 'mcp__mcp-server-openmetadata-PRD__get_tag': 'Getting tag info',
56
+ 'mcp__mcp-server-openmetadata-PRD__get_tag_by_name': 'Getting tag info',
57
+ 'mcp__mcp-server-openmetadata-PRD__list_tags': 'Listing tags',
58
+ 'mcp__mcp-server-openmetadata-PRD__get_classification': 'Getting classification',
59
+ 'mcp__mcp-server-openmetadata-PRD__list_classifications': 'Listing classifications',
60
+ 'mcp__mcp-server-openmetadata-PRD__get_usage_by_entity': 'Getting usage stats',
61
+ 'mcp__mcp-server-openmetadata-PRD__get_entity_usage_summary': 'Getting usage summary',
62
+ 'mcp__mcp-server-openmetadata-PRD__get_data_quality_report': 'Getting data quality report',
63
+ };
64
+
65
+ /**
66
+ * Registers all Slack event handlers for a single agent's Bolt App.
67
+ *
68
+ * Handles:
69
+ * - `app_mention` — responds when mentioned in a channel
70
+ * - `message` — responds to direct messages
71
+ * - `member_joined_channel` — posts a welcome message when added to a channel
72
+ *
73
+ * @param {App} app - The Slack Bolt App instance for this agent.
74
+ * @param {Agent} agent - The agent configuration record.
75
+ * @param {ClaudeHandler} claudeHandler - The Claude SDK session manager.
76
+ * @returns {void}
77
+ */
78
+ export function registerSlackHandlers(
79
+ app: App,
80
+ agent: Agent,
81
+ claudeHandler: ClaudeHandler,
82
+ restrictions: Restriction | null = null,
83
+ ): void {
84
+ const log = agentLogger(agent.slug);
85
+ const correctionHandler = new CorrectionHandler(agent);
86
+
87
+ /** Track in-flight abort controllers per session so new messages cancel old ones. */
88
+ const activeControllers = new Map<string, AbortController>();
89
+
90
+ /** Track current emoji reaction per session to avoid duplicate add calls. */
91
+ const currentReactions = new Map<string, string>();
92
+
93
+ /**
94
+ * Swaps the emoji reaction on a message without leaving duplicate reactions.
95
+ * Removes the current reaction (if any) before adding the new one.
96
+ * Failures are silently ignored as reactions are non-critical UI feedback.
97
+ *
98
+ * @param {WebClient} client - Slack Web API client.
99
+ * @param {string} channelId - Slack channel ID.
100
+ * @param {string} messageTs - Timestamp of the message to react to.
101
+ * @param {string} sessionKey - Session key used to track current reaction.
102
+ * @param {string} emoji - Emoji name to set (without colons).
103
+ * @returns {Promise<void>}
104
+ */
105
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
+ async function updateReaction(
107
+ client: any,
108
+ channelId: string,
109
+ messageTs: string,
110
+ sessionKey: string,
111
+ emoji: string
112
+ ) {
113
+ const current = currentReactions.get(sessionKey);
114
+ if (current === emoji) return;
115
+ try {
116
+ if (current) {
117
+ await client.reactions.remove({ channel: channelId, timestamp: messageTs, name: current }).catch(() => {});
118
+ }
119
+ await client.reactions.add({ channel: channelId, timestamp: messageTs, name: emoji });
120
+ currentReactions.set(sessionKey, emoji);
121
+ } catch { /* non-fatal */ }
122
+ }
123
+
124
+ app.event('app_mention', async ({ event, client }) => {
125
+ await handleMessage({
126
+ app, agent, claudeHandler, correctionHandler, client, log,
127
+ activeControllers, currentReactions, updateReaction,
128
+ userId: event.user ?? 'unknown',
129
+ channelId: event.channel,
130
+ threadTs: event.thread_ts ?? event.ts,
131
+ messageTs: event.ts,
132
+ rawText: event.text ?? '',
133
+ files: (event as any).files ?? [],
134
+ restrictions,
135
+ });
136
+ });
137
+
138
+ app.message(async ({ message, client }) => {
139
+ const msg = message as KnownEventFromType<'message'>;
140
+ if (!('channel' in msg) || !msg.channel?.startsWith('D')) return;
141
+ if (!('user' in msg)) return;
142
+ await handleMessage({
143
+ app, agent, claudeHandler, correctionHandler, client, log,
144
+ activeControllers, currentReactions, updateReaction,
145
+ userId: (msg as any).user,
146
+ channelId: (msg as any).channel,
147
+ threadTs: (msg as any).thread_ts,
148
+ messageTs: (msg as any).ts,
149
+ rawText: (msg as any).text ?? '',
150
+ files: (msg as any).files ?? [],
151
+ restrictions,
152
+ });
153
+ });
154
+
155
+ app.event('member_joined_channel', async ({ event, client }) => {
156
+ if (!agent.slackBotUserId || event.user !== agent.slackBotUserId) return;
157
+ // If the bot joined a restricted channel, post a notice and leave
158
+ if (isChannelRestricted(event.channel, restrictions)) {
159
+ try {
160
+ await client.chat.postMessage({
161
+ channel: event.channel,
162
+ text: `Sorry, I'm only configured to operate in specific channels. Please contact an admin if you'd like to add me here.`,
163
+ });
164
+ await client.conversations.leave({ channel: event.channel });
165
+ } catch { /* non-fatal */ }
166
+ return;
167
+ }
168
+ try {
169
+ await client.chat.postMessage({
170
+ channel: event.channel,
171
+ text: `👋 Hi! I'm *${agent.name}*. ${agent.description ?? ''}\n\nMention me to get started.`,
172
+ });
173
+ } catch { /* non-fatal */ }
174
+ });
175
+ }
176
+
177
+ // =============================================================================
178
+ // Core message handler
179
+ // =============================================================================
180
+
181
+ export interface SlackFile {
182
+ id: string;
183
+ name?: string;
184
+ title?: string;
185
+ mimetype?: string;
186
+ filetype?: string;
187
+ url_private_download?: string;
188
+ size?: number;
189
+ }
190
+
191
+ /**
192
+ * Returns true if the channel is blocked by the agent's restrictions.
193
+ * If restrictions is null or allowedChannels is empty, the channel is allowed.
194
+ *
195
+ * @param {string} channelId - Slack channel ID of the incoming message.
196
+ * @param {Restriction | null} restrictions - Agent's restriction config.
197
+ * @returns {boolean} True if the message should be silently ignored.
198
+ */
199
+ export function isChannelRestricted(channelId: string, restrictions: Restriction | null): boolean {
200
+ if (!restrictions || restrictions.allowedChannels.length === 0) return false;
201
+ return !restrictions.allowedChannels.includes(channelId);
202
+ }
203
+
204
+ interface HandleMessageOpts {
205
+ app: App;
206
+ agent: Agent;
207
+ claudeHandler: ClaudeHandler;
208
+ correctionHandler: CorrectionHandler;
209
+ client: any;
210
+ log: Logger;
211
+ activeControllers: Map<string, AbortController>;
212
+ currentReactions: Map<string, string>;
213
+ updateReaction: (client: any, channelId: string, messageTs: string, sessionKey: string, emoji: string) => Promise<void>;
214
+ userId: string;
215
+ channelId: string;
216
+ threadTs?: string;
217
+ messageTs: string;
218
+ rawText: string;
219
+ files?: SlackFile[];
220
+ restrictions: Restriction | null;
221
+ }
222
+
223
+ async function handleMessage(opts: HandleMessageOpts): Promise<void> {
224
+ const { app, agent, claudeHandler, correctionHandler, client, log, activeControllers, currentReactions,
225
+ updateReaction, userId, channelId, threadTs, messageTs, rawText, files, restrictions } = opts;
226
+
227
+ const userText = stripBotMention(rawText, agent.slackBotUserId).trim();
228
+ if (!userText && (!files || files.length === 0)) return;
229
+
230
+ // Silently ignore messages from channels not in the allowed list
231
+ if (isChannelRestricted(channelId, restrictions)) return;
232
+
233
+ // Route correction/help commands before normal processing
234
+ // Commands use agent slug prefix: {slug}:correct, {slug}:corrections, {slug}:help
235
+ if (correctionHandler.isCommand(userText)) {
236
+ await correctionHandler.handle(
237
+ { userId, channelId, threadTs, messageTs },
238
+ userText,
239
+ client,
240
+ );
241
+ return;
242
+ }
243
+
244
+ const sessionKey = claudeHandler.getSessionKey(userId, channelId, threadTs);
245
+
246
+ log.info('Processing message', { userId, channelId, threadTs, sessionKey, textLength: userText.length });
247
+
248
+ // Abort any in-flight request for this session (user sent a new message)
249
+ activeControllers.get(sessionKey)?.abort();
250
+ const abortController = new AbortController();
251
+ activeControllers.set(sessionKey, abortController);
252
+
253
+ // Thinking reaction + initial status message
254
+ await updateReaction(client, channelId, messageTs, sessionKey, 'thinking_face');
255
+
256
+ let statusTs: string | undefined;
257
+ try {
258
+ const posted = await client.chat.postMessage({ channel: channelId, thread_ts: threadTs, text: '*Thinking...*' });
259
+ statusTs = posted.ts as string | undefined;
260
+ } catch (err) {
261
+ log.error('Failed to post status message', { error: err });
262
+ return;
263
+ }
264
+
265
+ const prompt = await buildPrompt(client, channelId, threadTs, userText, agent, log, files);
266
+
267
+ let sentMessages: string[] = [];
268
+ let lastAssistantText: string | null = null;
269
+ let lastToolResultText: string | null = null;
270
+
271
+ try {
272
+ for await (const message of claudeHandler.streamQuery(prompt, sessionKey, abortController)) {
273
+ if (abortController.signal.aborted) break;
274
+
275
+ if (message.type === 'assistant') {
276
+ const content: any[] = (message as any).message?.content ?? [];
277
+ const hasToolUse = content.some((b: any) => b.type === 'tool_use');
278
+
279
+ // Extract text blocks
280
+ const textContent = content.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('');
281
+ if (textContent) lastAssistantText = textContent;
282
+
283
+ if (hasToolUse) {
284
+ // Show live tool status in the status message
285
+ await updateReaction(client, channelId, messageTs, sessionKey, 'gear');
286
+ const toolStatus = formatToolStatus(content);
287
+ if (statusTs && toolStatus) {
288
+ await client.chat.update({ channel: channelId, ts: statusTs, text: toolStatus }).catch(() => {});
289
+ }
290
+ } else if (textContent) {
291
+ // Text-only assistant message — send immediately
292
+ sentMessages.push(textContent);
293
+
294
+ // Extract code blocks for file upload, send text without them
295
+ const { textWithoutCode, codeBlocks } = extractCodeBlocks(textContent);
296
+ const displayText = codeBlocks.length > 0 ? textWithoutCode.trim() : textContent;
297
+
298
+ if (displayText) {
299
+ for (const payload of buildMessagePayloads(displayText, false)) {
300
+ await postMessageWithFallback(client, {
301
+ channel: channelId,
302
+ thread_ts: threadTs,
303
+ text: payload.text,
304
+ ...(payload.blocks && { blocks: payload.blocks }),
305
+ }, log);
306
+ }
307
+ }
308
+
309
+ // Upload code blocks as downloadable file snippets
310
+ if (codeBlocks.length > 0) {
311
+ await uploadCodeSnippets(client, codeBlocks, channelId, threadTs);
312
+ }
313
+ }
314
+ } else if (message.type === 'user') {
315
+ // Capture tool result text for fallback
316
+ const userContent = (message as any).message?.content;
317
+ if (Array.isArray(userContent)) {
318
+ for (const part of userContent) {
319
+ if (part.type === 'tool_result' && typeof part.content === 'string' && part.content.length > 0) {
320
+ lastToolResultText = part.content;
321
+ } else if (part.type === 'tool_result' && Array.isArray(part.content)) {
322
+ const textParts = part.content.filter((p: any) => p.type === 'text').map((p: any) => p.text);
323
+ if (textParts.length > 0) lastToolResultText = textParts.join('');
324
+ }
325
+ }
326
+ }
327
+ } else if (message.type === 'result') {
328
+ log.info('Query completed', {
329
+ cost: (message as any).total_cost_usd,
330
+ duration_ms: (message as any).duration_ms,
331
+ status: (message as any).subtype,
332
+ num_turns: (message as any).num_turns,
333
+ });
334
+
335
+ if ((message as any).subtype === 'success') {
336
+ const finalResult = (message as any).result as string | undefined;
337
+ if (finalResult && !sentMessages.includes(finalResult)) {
338
+ sentMessages.push(finalResult);
339
+
340
+ // Extract code blocks for file upload
341
+ const { textWithoutCode, codeBlocks } = extractCodeBlocks(finalResult);
342
+ const displayText = codeBlocks.length > 0 ? textWithoutCode.trim() : finalResult;
343
+
344
+ if (displayText) {
345
+ for (const payload of buildMessagePayloads(displayText, true)) {
346
+ await postMessageWithFallback(client, {
347
+ channel: channelId,
348
+ thread_ts: threadTs,
349
+ text: payload.text,
350
+ ...(payload.blocks && { blocks: payload.blocks }),
351
+ }, log);
352
+ }
353
+ }
354
+
355
+ // Upload code blocks as downloadable file snippets
356
+ if (codeBlocks.length > 0) {
357
+ await uploadCodeSnippets(client, codeBlocks, channelId, threadTs);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ // Fallback: if Claude produced no messages, use lastAssistantText or tool result
365
+ if (sentMessages.length === 0) {
366
+ const fallback = lastAssistantText ?? lastToolResultText ?? '_No response generated._';
367
+ log.info('No messages sent, using fallback', {
368
+ source: lastAssistantText ? 'lastAssistantText' : lastToolResultText ? 'lastToolResultText' : 'default',
369
+ });
370
+ for (const payload of buildMessagePayloads(fallback, true)) {
371
+ await postMessageWithFallback(client, {
372
+ channel: channelId,
373
+ thread_ts: threadTs,
374
+ text: payload.text,
375
+ ...(payload.blocks && { blocks: payload.blocks }),
376
+ }, log);
377
+ }
378
+ }
379
+
380
+ // Update status message to Done and set ✅ reaction
381
+ if (statusTs) {
382
+ await client.chat.update({ channel: channelId, ts: statusTs, text: '*Done*' }).catch(() => {});
383
+ }
384
+ await updateReaction(client, channelId, messageTs, sessionKey, 'white_check_mark');
385
+
386
+
387
+ } catch (error: any) {
388
+ if (error?.name === 'AbortError') {
389
+ log.debug('Request aborted', { sessionKey });
390
+ if (statusTs) await client.chat.update({ channel: channelId, ts: statusTs, text: '*Cancelled*' }).catch(() => {});
391
+ await updateReaction(client, channelId, messageTs, sessionKey, 'stop_button');
392
+ } else {
393
+ log.error('Error streaming Claude response', { sessionKey, error: error?.message });
394
+ const errText = `❌ Something went wrong. Please try again.\n\`${error?.message ?? 'Unknown error'}\``;
395
+ if (statusTs) await client.chat.update({ channel: channelId, ts: statusTs, text: errText }).catch(() => {});
396
+ await updateReaction(client, channelId, messageTs, sessionKey, 'x');
397
+ }
398
+ } finally {
399
+ activeControllers.delete(sessionKey);
400
+ setTimeout(() => currentReactions.delete(sessionKey), 5 * 60 * 1000);
401
+ }
402
+ }
403
+
404
+ // =============================================================================
405
+ // Slack posting helpers
406
+ // =============================================================================
407
+
408
+ /**
409
+ * Posts a message to Slack; if blocks are rejected with `invalid_blocks`,
410
+ * retries as plain text so the user still sees the response.
411
+ */
412
+ async function postMessageWithFallback(
413
+ client: any,
414
+ opts: { channel: string; thread_ts?: string; text: string; blocks?: any[] },
415
+ log: Logger,
416
+ ) {
417
+ try {
418
+ await client.chat.postMessage(opts);
419
+ } catch (err: any) {
420
+ if (err?.data?.error === 'invalid_blocks' && opts.blocks) {
421
+ log.warn('Slack rejected blocks, falling back to plain text', {
422
+ error: err?.data?.error,
423
+ blockTypes: opts.blocks.map((b: any) => b.type),
424
+ textPreview: opts.text.slice(0, 200),
425
+ });
426
+ await client.chat.postMessage({ channel: opts.channel, thread_ts: opts.thread_ts, text: opts.text });
427
+ } else {
428
+ throw err;
429
+ }
430
+ }
431
+ }
432
+
433
+ // =============================================================================
434
+ // Message formatting
435
+ // =============================================================================
436
+
437
+ /**
438
+ * Builds one or more Slack message payloads from Claude's response text.
439
+ * Each markdown table gets its own payload because Slack only supports
440
+ * one native table block per message.
441
+ *
442
+ * @param {string} text - Raw text from Claude.
443
+ * @param {boolean} isFinal - Whether this is the final message.
444
+ * @returns {{ text: string; blocks?: any[] }[]} Array of Slack-ready payloads.
445
+ */
446
+ export function buildMessagePayloads(text: string, isFinal: boolean): { text: string; blocks?: any[] }[] {
447
+ const payloads: { text: string; blocks?: any[] }[] = [];
448
+ let remaining = text;
449
+
450
+ while (remaining.trim()) {
451
+ const extracted = extractFirstMarkdownTable(remaining);
452
+
453
+ if (!extracted) {
454
+ payloads.push({ text: formatMessage(remaining, isFinal) });
455
+ break;
456
+ }
457
+
458
+ const parsed = parseMarkdownTable(extracted.tableLines);
459
+ if (parsed.headers.length === 0) {
460
+ payloads.push({ text: formatMessage(remaining, isFinal) });
461
+ break;
462
+ }
463
+
464
+ const blocks: any[] = [];
465
+ const beforeText = formatMessage(extracted.before.trim(), false);
466
+ if (beforeText) {
467
+ for (const chunk of splitTextForBlocks(beforeText)) {
468
+ blocks.push({ type: 'section', text: { type: 'mrkdwn', text: chunk } });
469
+ }
470
+ }
471
+ blocks.push(buildSlackTableBlock(parsed));
472
+
473
+ const fallback = formatMessage(
474
+ extracted.before.trim() + '\n' + extracted.tableLines.join('\n'),
475
+ false,
476
+ );
477
+ payloads.push({ text: fallback, blocks });
478
+
479
+ remaining = extracted.after;
480
+ }
481
+
482
+ return payloads.length > 0 ? payloads : [{ text: formatMessage(text, isFinal) }];
483
+ }
484
+
485
+ /**
486
+ * Formats markdown text for Slack mrkdwn:
487
+ * - Preserves code blocks as-is (strips language hints)
488
+ * - Converts headings → *bold*
489
+ * - Removes HR lines
490
+ * - **bold** → *bold*
491
+ * - __italic__ → _italic_
492
+ * - Auto-wraps bare markdown tables in code blocks
493
+ */
494
+ export function formatMessage(text: string, _isFinal: boolean): string {
495
+ const codeBlocks: string[] = [];
496
+ // Use a placeholder that cannot be matched by the __italic__ regex.
497
+ // \x00 is not present in normal text and breaks the /__([^_]+)__/ pattern.
498
+ let formatted = text.replace(/```[\s\S]*?```/g, (match) => {
499
+ codeBlocks.push(match);
500
+ return `\x00CB${codeBlocks.length - 1}\x00`;
501
+ });
502
+
503
+ // Auto-wrap bare markdown tables in code blocks
504
+ formatted = formatted.replace(
505
+ /(?:^|\n)((?:[ \t]*\S.+\|.+[ \t]*\n?){2,})/g,
506
+ (_match, tableBlock) => `\n\`\`\`\n${tableBlock.trim()}\n\`\`\`\n`
507
+ );
508
+
509
+ formatted = formatted.replace(/^#{1,6}\s+(.+)$/gm, '*$1*');
510
+ formatted = formatted.replace(/^(?:---+|\*\*\*+|___+)\s*$/gm, '');
511
+ formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '*$1*');
512
+ formatted = formatted.replace(/__([^_]+)__/g, '_$1_');
513
+
514
+ // Restore code blocks (strip language hints)
515
+ formatted = formatted.replace(/\x00CB(\d+)\x00/g, (_, index) => {
516
+ const block = codeBlocks[parseInt(index)];
517
+ return block.replace(/^```\w+\n/, '```\n');
518
+ });
519
+
520
+ return formatted;
521
+ }
522
+
523
+ /** Splits text into ≤3000-char chunks for Slack section blocks. */
524
+ export function splitTextForBlocks(text: string): string[] {
525
+ const MAX = 3000;
526
+ if (text.length <= MAX) return [text];
527
+ const chunks: string[] = [];
528
+ let remaining = text;
529
+ while (remaining.length > 0) {
530
+ if (remaining.length <= MAX) { chunks.push(remaining); break; }
531
+ let splitAt = remaining.lastIndexOf('\n', MAX);
532
+ if (splitAt <= 0) splitAt = MAX;
533
+ chunks.push(remaining.slice(0, splitAt));
534
+ remaining = remaining.slice(splitAt).replace(/^\n/, '');
535
+ }
536
+ return chunks;
537
+ }
538
+
539
+ export function isSeparatorLine(line: string): boolean {
540
+ return /^\s*\|?[-:\s|]+\|?\s*$/.test(line) && line.includes('-');
541
+ }
542
+
543
+ export function extractFirstMarkdownTable(text: string): { before: string; tableLines: string[]; after: string } | null {
544
+ const codeBlockTableRe = /```(?:\w*)\n((?:[ \t]*.+\|.+[ \t]*\n?){2,})```/;
545
+ const bareTableRe = /(?:^|\n)((?:[ \t]*\|.+\|[ \t]*(?:\n|$)){2,})/;
546
+ const loosePipeRe = /(?:^|\n)((?:[ \t]*\S.+\|.+(?:\n|$)){2,})/;
547
+
548
+ const candidates: { match: RegExpExecArray; content: string }[] = [];
549
+ const cbMatch = codeBlockTableRe.exec(text);
550
+ const bareMatch = bareTableRe.exec(text);
551
+ const looseMatch = loosePipeRe.exec(text);
552
+ if (cbMatch) candidates.push({ match: cbMatch, content: cbMatch[1] });
553
+ if (bareMatch) candidates.push({ match: bareMatch, content: bareMatch[1] });
554
+ if (looseMatch) candidates.push({ match: looseMatch, content: looseMatch[1] });
555
+ candidates.sort((a, b) => a.match.index - b.match.index);
556
+
557
+ for (const { match, content } of candidates) {
558
+ const lines = content.trim().split('\n').map(l => l.trim());
559
+ if (lines.length < 2) continue;
560
+ if (!lines.some(l => isSeparatorLine(l))) continue;
561
+ const fullMatch = match[0];
562
+ const startIdx = match.index + (fullMatch.startsWith('\n') ? 1 : 0);
563
+ const endIdx = match.index + fullMatch.length;
564
+ return { before: text.slice(0, startIdx), tableLines: lines, after: text.slice(endIdx) };
565
+ }
566
+ return null;
567
+ }
568
+
569
+ export function parseMarkdownTable(lines: string[]): { headers: string[]; rows: string[][]; alignments: ('left' | 'center' | 'right')[] } {
570
+ const splitRow = (line: string): string[] =>
571
+ line.replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim());
572
+
573
+ const headers = splitRow(lines[0]);
574
+ const sepIdx = lines.findIndex(l => isSeparatorLine(l));
575
+ const sepCells = sepIdx >= 0 ? splitRow(lines[sepIdx]) : [];
576
+ const alignments: ('left' | 'center' | 'right')[] = sepCells.map(cell => {
577
+ const t = cell.trim();
578
+ if (t.startsWith(':') && t.endsWith(':')) return 'center';
579
+ if (t.endsWith(':')) return 'right';
580
+ return 'left';
581
+ });
582
+ while (alignments.length < headers.length) alignments.push('left');
583
+ const rows: string[][] = [];
584
+ for (let i = 0; i < lines.length; i++) {
585
+ if (i === 0 || i === sepIdx) continue;
586
+ rows.push(splitRow(lines[i]));
587
+ }
588
+ return { headers, rows, alignments };
589
+ }
590
+
591
+ export function buildSlackTableBlock(parsed: { headers: string[]; rows: string[][]; alignments: ('left' | 'center' | 'right')[] }): Record<string, any> {
592
+ const maxCols = Math.min(parsed.headers.length, 20);
593
+ const buildRow = (cells: string[]) =>
594
+ Array.from({ length: maxCols }, (_, i) => ({ type: 'raw_text', text: (cells[i] || '').toString() }));
595
+ return {
596
+ type: 'table',
597
+ rows: [buildRow(parsed.headers), ...parsed.rows.slice(0, 99).map(r => buildRow(r))],
598
+ column_settings: parsed.alignments.slice(0, maxCols).map(a => ({ align: a })),
599
+ };
600
+ }
601
+
602
+ // =============================================================================
603
+ // Code snippet uploads
604
+ // =============================================================================
605
+
606
+ /**
607
+ * Strips all non-ASCII characters from code content.
608
+ * Prevents invisible Unicode characters from breaking copy-pasted queries.
609
+ *
610
+ * Ported from nlq-claude-slack-bot/src/slack-handler.ts:890
611
+ */
612
+ function sanitizeCodeContent(code: string): string {
613
+ return code.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '');
614
+ }
615
+
616
+ /**
617
+ * Extracts fenced code blocks from text, returning text without code blocks
618
+ * and the extracted blocks separately.
619
+ *
620
+ * Ported from nlq-claude-slack-bot/src/slack-handler.ts:898
621
+ */
622
+ function extractCodeBlocks(text: string): { textWithoutCode: string; codeBlocks: { lang: string; code: string }[] } {
623
+ const codeBlocks: { lang: string; code: string }[] = [];
624
+ const textWithoutCode = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
625
+ codeBlocks.push({ lang: lang || 'sql', code: code.trim() });
626
+ return '';
627
+ });
628
+ return { textWithoutCode, codeBlocks };
629
+ }
630
+
631
+ /**
632
+ * Uploads code blocks as Slack file snippets for clean copy-paste.
633
+ * Sanitizes content to remove invisible Unicode characters.
634
+ *
635
+ * Ported from nlq-claude-slack-bot/src/slack-handler.ts:910
636
+ *
637
+ * @param client - Slack Web API client
638
+ * @param codeBlocks - Extracted code blocks with language and content
639
+ * @param channelId - Channel to upload to
640
+ * @param threadTs - Thread timestamp for threading the upload
641
+ */
642
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
643
+ async function uploadCodeSnippets(
644
+ client: any,
645
+ codeBlocks: { lang: string; code: string }[],
646
+ channelId: string,
647
+ threadTs?: string,
648
+ ): Promise<void> {
649
+ for (let i = 0; i < codeBlocks.length; i++) {
650
+ const { lang, code } = codeBlocks[i];
651
+ const sanitized = sanitizeCodeContent(code);
652
+ const extension = lang === 'sql' ? 'sql' : (lang || 'txt');
653
+ const name = codeBlocks.length === 1
654
+ ? `query.${extension}`
655
+ : `query_${i + 1}.${extension}`;
656
+ try {
657
+ await client.filesUploadV2({
658
+ channel_id: channelId,
659
+ thread_ts: threadTs,
660
+ content: sanitized,
661
+ filename: name,
662
+ title: name,
663
+ });
664
+ } catch {
665
+ // Non-fatal — code is still visible inline in the message
666
+ }
667
+ }
668
+ }
669
+
670
+ // =============================================================================
671
+ // Other helpers
672
+ // =============================================================================
673
+
674
+ /**
675
+ * Builds a human-readable status string for the first tool_use block in a
676
+ * Claude assistant message. Used to keep the user informed while a tool runs.
677
+ *
678
+ * Returns a Slack mrkdwn string like `*Querying Redshift*\n\`\`\`sql\n...\n\`\`\``
679
+ * for known tools, `*Working...*` for unknown tools, or null if no tool block.
680
+ *
681
+ * @param {unknown[]} content - The `content` array from a Claude assistant message.
682
+ * @returns {string | null} Slack-formatted status text, or null if no tool was used.
683
+ */
684
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
685
+ export function formatToolStatus(content: any[]): string | null {
686
+ for (const block of content) {
687
+ if (block.type !== 'tool_use') continue;
688
+ const label = MCP_TOOL_LABELS[block.name];
689
+ if (label) {
690
+ if (block.name === 'mcp__redshift-mcp__query' && block.input?.sql) {
691
+ const sql = String(block.input.sql).slice(0, 500);
692
+ return `*${label}*\n\`\`\`sql\n${sql}\n\`\`\``;
693
+ }
694
+ if (block.input?.query) return `*${label}:* \`${String(block.input.query).slice(0, 100)}\``;
695
+ if (block.input?.fqn || block.input?.name) return `*${label}:* \`${block.input.fqn ?? block.input.name}\``;
696
+ return `*${label}...*`;
697
+ }
698
+ return `*Working...*`;
699
+ }
700
+ return null;
701
+ }
702
+
703
+ /**
704
+ * Builds the full prompt to send to Claude by prepending thread context.
705
+ *
706
+ * Fetches the preceding messages in the thread (up to MAX_THREAD_CONTEXT_MESSAGES)
707
+ * and prefixes them as `[Thread context]` so Claude can follow the conversation.
708
+ * Silently falls back to the bare user text if the Slack API call fails.
709
+ *
710
+ * @param {unknown} client - Slack Web API client.
711
+ * @param {string} channelId - Channel containing the thread.
712
+ * @param {string | undefined} threadTs - Thread timestamp, or undefined for non-thread DMs.
713
+ * @param {string} userText - The user's message with bot mentions stripped.
714
+ * @param {Agent} agent - The agent (used for speaker labelling in context).
715
+ * @param {Logger} log - Logger instance.
716
+ * @param {SlackFile[]} [files] - Files attached to the message.
717
+ * @returns {Promise<string | ContentBlockParam[]>} Prompt for `claudeHandler.streamQuery`.
718
+ */
719
+
720
+ /** Max bytes to read from a single text file (512 KB). */
721
+ const MAX_TEXT_FILE_BYTES = 512 * 1024;
722
+ /** Max bytes to download for image/PDF files (20 MB). */
723
+ const MAX_BINARY_FILE_BYTES = 20 * 1024 * 1024;
724
+
725
+ const TEXT_MIMETYPES = new Set([
726
+ 'text/plain', 'text/csv', 'text/html', 'text/xml', 'text/markdown',
727
+ 'text/x-python', 'text/x-script.python', 'text/javascript',
728
+ 'application/json', 'application/xml', 'application/x-yaml',
729
+ 'application/x-ndjson', 'application/sql',
730
+ ]);
731
+
732
+ const TEXT_FILETYPES = new Set([
733
+ 'text', 'csv', 'json', 'yaml', 'xml', 'html', 'markdown', 'md',
734
+ 'python', 'py', 'javascript', 'js', 'typescript', 'ts', 'go',
735
+ 'ruby', 'rb', 'java', 'kotlin', 'swift', 'cpp', 'c', 'rust',
736
+ 'sh', 'bash', 'zsh', 'sql', 'r', 'scala', 'php', 'toml', 'ini',
737
+ 'conf', 'cfg', 'env', 'diff', 'patch', 'log',
738
+ ]);
739
+
740
+ const IMAGE_MIMETYPES: Record<string, 'image/jpeg' | 'image/png' | 'image/webp'> = {
741
+ 'image/jpeg': 'image/jpeg',
742
+ 'image/jpg': 'image/jpeg',
743
+ 'image/png': 'image/png',
744
+ 'image/webp': 'image/webp',
745
+ };
746
+
747
+ const IMAGE_FILETYPES: Record<string, 'image/jpeg' | 'image/png' | 'image/webp'> = {
748
+ jpg: 'image/jpeg',
749
+ jpeg: 'image/jpeg',
750
+ png: 'image/png',
751
+ webp: 'image/webp',
752
+ };
753
+
754
+ export function getFileKind(file: SlackFile): 'text' | 'image' | 'pdf' | 'unsupported' {
755
+ const mt = file.mimetype ?? '';
756
+ const ft = (file.filetype ?? '').toLowerCase();
757
+ if (mt === 'application/pdf' || ft === 'pdf') return 'pdf';
758
+ if (mt in IMAGE_MIMETYPES || ft in IMAGE_FILETYPES) return 'image';
759
+ if (TEXT_MIMETYPES.has(mt) || mt.startsWith('text/') || TEXT_FILETYPES.has(ft)) return 'text';
760
+ return 'unsupported';
761
+ }
762
+
763
+ async function fetchSlackFile(client: any, url: string): Promise<ArrayBuffer> {
764
+ const token: string = (client as any).token ?? (client as any)._token ?? '';
765
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
766
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
767
+ return response.arrayBuffer();
768
+ }
769
+
770
+ export async function downloadFile(
771
+ client: any,
772
+ file: SlackFile,
773
+ log: Logger
774
+ ): Promise<{ kind: 'text'; content: string } | { kind: 'block'; block: ContentBlockParam } | null> {
775
+ const kind = getFileKind(file);
776
+ if (kind === 'unsupported') {
777
+ log.debug('Skipping unsupported file type', { name: file.name, mimetype: file.mimetype, filetype: file.filetype });
778
+ return null;
779
+ }
780
+ if (!file.url_private_download) return null;
781
+
782
+ const label = file.name ?? file.title ?? file.id;
783
+
784
+ try {
785
+ if (kind === 'text') {
786
+ if (file.size && file.size > MAX_TEXT_FILE_BYTES) {
787
+ log.warn('Text file too large, truncating', { name: file.name, size: file.size });
788
+ }
789
+ const buffer = await fetchSlackFile(client, file.url_private_download);
790
+ let text = new TextDecoder().decode(buffer.slice(0, MAX_TEXT_FILE_BYTES));
791
+ if (buffer.byteLength > MAX_TEXT_FILE_BYTES) text += '\n[... truncated at 512 KB ...]';
792
+ return { kind: 'text', content: `[File: ${label}]\n${text}` };
793
+ }
794
+
795
+ if (file.size && file.size > MAX_BINARY_FILE_BYTES) {
796
+ log.warn('Binary file too large to send to Claude', { name: file.name, size: file.size });
797
+ return { kind: 'text', content: `[File "${label}" is too large to process (${Math.round((file.size ?? 0) / 1024 / 1024)} MB, limit 20 MB)]` };
798
+ }
799
+
800
+ const buffer = await fetchSlackFile(client, file.url_private_download);
801
+ const base64 = Buffer.from(buffer).toString('base64');
802
+
803
+ if (kind === 'image') {
804
+ const mt = file.mimetype ?? '';
805
+ const ft = (file.filetype ?? '').toLowerCase();
806
+ const mediaType = IMAGE_MIMETYPES[mt] ?? IMAGE_FILETYPES[ft] ?? 'image/jpeg';
807
+ return {
808
+ kind: 'block',
809
+ block: {
810
+ type: 'image',
811
+ source: { type: 'base64', media_type: mediaType, data: base64 },
812
+ } as ContentBlockParam,
813
+ };
814
+ }
815
+
816
+ return {
817
+ kind: 'block',
818
+ block: {
819
+ type: 'document',
820
+ source: { type: 'base64', media_type: 'application/pdf', data: base64 },
821
+ title: label,
822
+ } as ContentBlockParam,
823
+ };
824
+ } catch (err) {
825
+ log.warn('Error downloading file', { name: file.name, error: err });
826
+ return null;
827
+ }
828
+ }
829
+
830
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
831
+ export async function buildPrompt(
832
+ client: any, channelId: string, threadTs: string | undefined,
833
+ userText: string, agent: Agent, log: Logger,
834
+ files?: SlackFile[]
835
+ ): Promise<string | ContentBlockParam[]> {
836
+ // Fetch thread context
837
+ let threadContext = '';
838
+ if (threadTs) {
839
+ try {
840
+ const replies = await client.conversations.replies({ channel: channelId, ts: threadTs, limit: MAX_THREAD_CONTEXT_MESSAGES });
841
+ const messages: any[] = replies.messages ?? [];
842
+ const contextMessages = messages.slice(0, -1);
843
+ if (contextMessages.length > 0) {
844
+ const userCache: Record<string, string> = {};
845
+ const getUserLabel = async (userId: string) => {
846
+ if (userCache[userId]) return userCache[userId];
847
+ try {
848
+ const info = await client.users.info({ user: userId });
849
+ const name = info.user?.display_name || info.user?.real_name || userId;
850
+ userCache[userId] = `${name} (${userId})`;
851
+ } catch { userCache[userId] = userId; }
852
+ return userCache[userId];
853
+ };
854
+ const contextLines = await Promise.all(contextMessages.map(async (m: any) => {
855
+ const speaker = m.bot_id ? `${agent.name}` : await getUserLabel(m.user);
856
+ const parts: string[] = [`${speaker}: ${stripBotMention(m.text ?? '', agent.slackBotUserId)}`];
857
+
858
+ // Include forwarded/shared message attachments (text + images)
859
+ if (m.attachments?.length) {
860
+ for (const att of m.attachments) {
861
+ const attParts: string[] = [];
862
+ if (att.author_name || att.from_url) attParts.push(`[Forwarded from ${att.author_name ?? att.from_url}]`);
863
+ if (att.pretext) attParts.push(att.pretext);
864
+ if (att.text) attParts.push(att.text);
865
+ if (att.fallback && !att.text) attParts.push(att.fallback);
866
+ if (att.image_url) attParts.push(`[Attached image: ${att.image_url}]`);
867
+ if (attParts.length) parts.push(attParts.join('\n'));
868
+ }
869
+ }
870
+
871
+ // Include files shared in thread history (images shown as note)
872
+ if (m.files?.length) {
873
+ for (const f of m.files) {
874
+ const label = f.name ?? f.title ?? f.id;
875
+ if (f.mimetype?.startsWith('image/')) {
876
+ parts.push(`[Shared image: ${label}]`);
877
+ } else if (f.name) {
878
+ parts.push(`[Shared file: ${label}]`);
879
+ }
880
+ }
881
+ }
882
+
883
+ return parts.join('\n');
884
+ }));
885
+ let context = contextLines.join('\n');
886
+ if (context.length > MAX_THREAD_CONTEXT_CHARS) context = '...' + context.slice(-MAX_THREAD_CONTEXT_CHARS);
887
+ threadContext = `[Thread context]\n${context}\n\n`;
888
+ }
889
+ } catch (err) {
890
+ log.warn('Failed to fetch thread context', { error: err });
891
+ }
892
+ }
893
+
894
+ // Download files — split into text chunks and binary blocks
895
+ const textChunks: string[] = [];
896
+ const binaryBlocks: ContentBlockParam[] = [];
897
+
898
+ if (files && files.length > 0) {
899
+ const results = await Promise.all(files.map(f => downloadFile(client, f, log)));
900
+ for (const result of results) {
901
+ if (!result) continue;
902
+ if (result.kind === 'text') textChunks.push(result.content);
903
+ else binaryBlocks.push(result.block);
904
+ }
905
+ }
906
+
907
+ const textPrompt = `${threadContext}${textChunks.length > 0 ? textChunks.join('\n\n') + '\n\n' : ''}${userText}`.trim();
908
+
909
+ if (binaryBlocks.length > 0) {
910
+ const blocks: ContentBlockParam[] = [];
911
+ if (textPrompt) blocks.push({ type: 'text', text: textPrompt });
912
+ blocks.push(...binaryBlocks);
913
+ return blocks;
914
+ }
915
+
916
+ return textPrompt;
917
+ }
918
+
919
+ /**
920
+ * Removes `<@BOT_USER_ID>` mention tokens from a message string.
921
+ *
922
+ * @param {string} text - Raw Slack message text.
923
+ * @param {string} [botUserId] - The bot's Slack user ID. No-op if undefined.
924
+ * @returns {string} Text with all bot mention tokens stripped and trimmed.
925
+ */
926
+ export function stripBotMention(text: string, botUserId?: string): string {
927
+ if (!botUserId) return text;
928
+ return text.replace(new RegExp(`<@${botUserId}>\\s*`, 'g'), '').trim();
929
+ }