@cursor/july 0.1.39 → 0.1.41

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 (259) hide show
  1. package/AGENTS.md +4 -3
  2. package/dist/bin/agent-serve.js +28 -9
  3. package/dist/channels/slack/api.d.ts.map +1 -1
  4. package/dist/channels/slack/api.js +18 -8
  5. package/dist/channels/slack/attachments.d.ts +82 -0
  6. package/dist/channels/slack/attachments.d.ts.map +1 -0
  7. package/dist/channels/slack/attachments.js +520 -0
  8. package/dist/channels/slack/cursor-account.d.ts +7 -1
  9. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  10. package/dist/channels/slack/cursor-account.js +2 -2
  11. package/dist/channels/slack/defaults.d.ts.map +1 -1
  12. package/dist/channels/slack/defaults.js +26 -17
  13. package/dist/channels/slack/dispatch.d.ts +4 -3
  14. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  15. package/dist/channels/slack/dispatch.js +84 -21
  16. package/dist/channels/slack/inbound.d.ts +6 -0
  17. package/dist/channels/slack/inbound.d.ts.map +1 -1
  18. package/dist/channels/slack/inbound.js +48 -2
  19. package/dist/channels/slack/index.d.ts +1 -0
  20. package/dist/channels/slack/index.d.ts.map +1 -1
  21. package/dist/channels/slack/index.js +1 -0
  22. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  23. package/dist/channels/slack/live-delivery.js +16 -4
  24. package/dist/channels/slack/manifest.d.ts.map +1 -1
  25. package/dist/channels/slack/manifest.js +1 -0
  26. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  27. package/dist/channels/slack/slack-channel.js +7 -4
  28. package/dist/channels/slack/types.d.ts +49 -1
  29. package/dist/channels/slack/types.d.ts.map +1 -1
  30. package/dist/docs/404.html +2 -2
  31. package/dist/docs/ab.html +6 -6
  32. package/dist/docs/assets/{ab.md.6cLOW7--.js → ab.md.hdxsvX5M.js} +2 -2
  33. package/dist/docs/assets/{app.BQ8Hihdf.js → app.mIw24pb1.js} +1 -1
  34. package/dist/docs/assets/{building-with-agents.md.txrcGU2B.js → building-with-agents.md.PeZaZA1P.js} +1 -1
  35. package/dist/docs/assets/chunks/@localSearchIndexroot.BA7V8dEJ.js +1 -0
  36. package/dist/docs/assets/chunks/{VPLocalSearchBox.BBCr8Yuy.js → VPLocalSearchBox.hOzpPwrG.js} +1 -1
  37. package/dist/docs/assets/chunks/{theme.CK_NiGC-.js → theme.BtFhUYmh.js} +2 -2
  38. package/dist/docs/assets/{deployment.md.CuK5SNjN.js → deployment.md.BHVjVqRa.js} +4 -4
  39. package/dist/docs/assets/{deployment.md.CuK5SNjN.lean.js → deployment.md.BHVjVqRa.lean.js} +1 -1
  40. package/dist/docs/assets/{evals.md.BQXI3rXy.js → evals.md.qeoGgPw0.js} +3 -3
  41. package/dist/docs/assets/example-agents_index.md.xmJ87d_3.js +2 -0
  42. package/dist/docs/assets/{guides_webhooks.md.BERuBSJW.js → guides_webhooks.md.DB-r_er9.js} +47 -4
  43. package/dist/docs/assets/{guides_webhooks.md.BERuBSJW.lean.js → guides_webhooks.md.DB-r_er9.lean.js} +1 -1
  44. package/dist/docs/assets/index.md.BKpbkCa6.js +24 -0
  45. package/dist/docs/assets/{index.md.CmhptOmN.lean.js → index.md.BKpbkCa6.lean.js} +1 -1
  46. package/dist/docs/assets/{quickstart.md.C_b6ESpD.js → quickstart.md.rIbfahJM.js} +4 -4
  47. package/dist/docs/assets/{reference_cli.md.Byvrg8eu.js → reference_cli.md.DqEqQpGn.js} +5 -5
  48. package/dist/docs/assets/{reference_cli.md.Byvrg8eu.lean.js → reference_cli.md.DqEqQpGn.lean.js} +1 -1
  49. package/dist/docs/assets/{reference_hooks.md.BGDw4VLm.js → reference_hooks.md.BHeG-vjK.js} +2 -2
  50. package/dist/docs/assets/{reference_hooks.md.BGDw4VLm.lean.js → reference_hooks.md.BHeG-vjK.lean.js} +1 -1
  51. package/dist/docs/assets/{storage.md.mQDtIULc.js → storage.md.DcGSnIxX.js} +10 -4
  52. package/dist/docs/assets/{storage.md.mQDtIULc.lean.js → storage.md.DcGSnIxX.lean.js} +1 -1
  53. package/dist/docs/assets/troubleshooting.md.CkgwmVI0.js +1 -0
  54. package/dist/docs/assets/{troubleshooting.md.B5RVX_tL.lean.js → troubleshooting.md.CkgwmVI0.lean.js} +1 -1
  55. package/dist/docs/building-with-agents.html +5 -5
  56. package/dist/docs/concepts.html +3 -3
  57. package/dist/docs/deployment.html +6 -6
  58. package/dist/docs/evals.html +7 -7
  59. package/dist/docs/example-agents/approval-buddy.html +3 -3
  60. package/dist/docs/example-agents/benny.html +3 -3
  61. package/dist/docs/example-agents/bugbot.html +3 -3
  62. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  63. package/dist/docs/example-agents/codeowners-review.html +3 -3
  64. package/dist/docs/example-agents/concierge.html +3 -3
  65. package/dist/docs/example-agents/fsd.html +3 -3
  66. package/dist/docs/example-agents/index.html +5 -5
  67. package/dist/docs/example-agents/knowledge-base.html +3 -3
  68. package/dist/docs/example-agents/oncall.html +3 -3
  69. package/dist/docs/example-agents/security-reviewer.html +3 -3
  70. package/dist/docs/example-agents/slack-agent.html +3 -3
  71. package/dist/docs/example-agents/weather-agent.html +3 -3
  72. package/dist/docs/guides/agent-to-agent.html +3 -3
  73. package/dist/docs/guides/cloud-runtime.html +3 -3
  74. package/dist/docs/guides/github.html +3 -3
  75. package/dist/docs/guides/human-in-the-loop.html +3 -3
  76. package/dist/docs/guides/mcp-oauth.html +3 -3
  77. package/dist/docs/guides/slack.html +3 -3
  78. package/dist/docs/guides/webhooks.html +49 -6
  79. package/dist/docs/hashmap.json +1 -1
  80. package/dist/docs/hillclimbing.html +3 -3
  81. package/dist/docs/index.html +7 -7
  82. package/dist/docs/quickstart.html +8 -8
  83. package/dist/docs/reference/agent-config.html +3 -3
  84. package/dist/docs/reference/artifacts.html +3 -3
  85. package/dist/docs/reference/channels.html +3 -3
  86. package/dist/docs/reference/cli.html +8 -8
  87. package/dist/docs/reference/connections.html +3 -3
  88. package/dist/docs/reference/hooks.html +5 -5
  89. package/dist/docs/reference/http-api.html +3 -3
  90. package/dist/docs/reference/instructions.html +3 -3
  91. package/dist/docs/reference/playground.html +3 -3
  92. package/dist/docs/reference/project-layout.html +3 -3
  93. package/dist/docs/reference/prompt.html +3 -3
  94. package/dist/docs/reference/schedules.html +3 -3
  95. package/dist/docs/reference/sessions.html +3 -3
  96. package/dist/docs/reference/skills.html +3 -3
  97. package/dist/docs/reference/subagents.html +3 -3
  98. package/dist/docs/reference/tools.html +3 -3
  99. package/dist/docs/scaffolding-agents.html +3 -3
  100. package/dist/docs/storage.html +14 -8
  101. package/dist/docs/troubleshooting.html +5 -5
  102. package/dist/index.d.ts +4 -2
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +3 -1
  105. package/dist/internal/cli-ax.d.ts.map +1 -1
  106. package/dist/internal/cli-ax.js +22 -2
  107. package/dist/internal/cli-deploy.d.ts +6 -0
  108. package/dist/internal/cli-deploy.d.ts.map +1 -1
  109. package/dist/internal/cli-deploy.js +14 -4
  110. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
  111. package/dist/internal/cli-mcp-oauth.js +3 -1
  112. package/dist/internal/cli-mcp.js +4 -4
  113. package/dist/internal/cursor-event-relay.d.ts +45 -0
  114. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  115. package/dist/internal/cursor-event-relay.js +96 -2
  116. package/dist/internal/deploy-client.js +1 -1
  117. package/dist/internal/discovery.d.ts.map +1 -1
  118. package/dist/internal/discovery.js +35 -7
  119. package/dist/internal/distribution.d.ts.map +1 -1
  120. package/dist/internal/distribution.js +3 -1
  121. package/dist/internal/durable-session-store.d.ts +48 -0
  122. package/dist/internal/durable-session-store.d.ts.map +1 -0
  123. package/dist/internal/durable-session-store.js +86 -0
  124. package/dist/internal/hosting.d.ts +1 -1
  125. package/dist/internal/hosting.js +2 -2
  126. package/dist/internal/init-project.d.ts +34 -0
  127. package/dist/internal/init-project.d.ts.map +1 -1
  128. package/dist/internal/init-project.js +121 -2
  129. package/dist/internal/ledger-serving.d.ts +101 -0
  130. package/dist/internal/ledger-serving.d.ts.map +1 -0
  131. package/dist/internal/ledger-serving.js +174 -0
  132. package/dist/internal/listen-port.d.ts +22 -0
  133. package/dist/internal/listen-port.d.ts.map +1 -0
  134. package/dist/internal/listen-port.js +77 -0
  135. package/dist/internal/mcp-oauth.d.ts +12 -1
  136. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  137. package/dist/internal/mcp-oauth.js +25 -3
  138. package/dist/internal/playground/static.d.ts +21 -4
  139. package/dist/internal/playground/static.d.ts.map +1 -1
  140. package/dist/internal/playground/static.js +82 -18
  141. package/dist/internal/sdk-runner.d.ts.map +1 -1
  142. package/dist/internal/sdk-runner.js +10 -1
  143. package/dist/internal/server.d.ts.map +1 -1
  144. package/dist/internal/server.js +67 -49
  145. package/dist/internal/session-engine.d.ts +57 -1
  146. package/dist/internal/session-engine.d.ts.map +1 -1
  147. package/dist/internal/session-engine.js +117 -28
  148. package/dist/internal/slack-provision-client.js +1 -1
  149. package/dist/internal/workspace.d.ts +6 -2
  150. package/dist/internal/workspace.d.ts.map +1 -1
  151. package/dist/internal/workspace.js +10 -2
  152. package/dist/kv/memory-kv.d.ts +23 -0
  153. package/dist/kv/memory-kv.d.ts.map +1 -0
  154. package/dist/kv/memory-kv.js +103 -0
  155. package/dist/kv.d.ts +22 -0
  156. package/dist/kv.d.ts.map +1 -0
  157. package/dist/kv.js +20 -0
  158. package/dist/ledger/session-ledger.d.ts +204 -0
  159. package/dist/ledger/session-ledger.d.ts.map +1 -0
  160. package/dist/ledger/session-ledger.js +382 -0
  161. package/dist/ledger.d.ts +11 -0
  162. package/dist/ledger.d.ts.map +1 -0
  163. package/dist/ledger.js +9 -0
  164. package/dist/playground/assets/index-B3HBneZZ.js +85 -0
  165. package/dist/playground/assets/index-Bqk9nplh.css +1 -0
  166. package/dist/playground/index.html +2 -2
  167. package/dist/storage-backends/cursor-hosted.d.ts +25 -3
  168. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  169. package/dist/storage-backends/cursor-hosted.js +50 -2
  170. package/dist/storage-backends/file-kv.d.ts +5 -2
  171. package/dist/storage-backends/file-kv.d.ts.map +1 -1
  172. package/dist/storage-backends/file-kv.js +107 -7
  173. package/dist/storage.d.ts +60 -5
  174. package/dist/storage.d.ts.map +1 -1
  175. package/dist/storage.js +28 -4
  176. package/dist/types.d.ts +123 -3
  177. package/dist/types.d.ts.map +1 -1
  178. package/dist/types.js +14 -0
  179. package/docs/README.md +10 -7
  180. package/docs/ab.md +2 -2
  181. package/docs/building-with-agents.md +1 -1
  182. package/docs/deployment.md +34 -11
  183. package/docs/evals.md +3 -3
  184. package/docs/example-agents/index.md +1 -1
  185. package/docs/guides/webhooks.md +87 -0
  186. package/docs/quickstart.md +7 -8
  187. package/docs/reference/cli.md +15 -6
  188. package/docs/reference/hooks.md +8 -2
  189. package/docs/storage.md +67 -7
  190. package/docs/troubleshooting.md +10 -2
  191. package/package.json +20 -7
  192. package/skills/create-agent/SKILL.md +1 -1
  193. package/src/bin/agent-serve.ts +27 -11
  194. package/src/channels/slack/api.ts +19 -14
  195. package/src/channels/slack/attachments.ts +635 -0
  196. package/src/channels/slack/cursor-account.ts +8 -0
  197. package/src/channels/slack/defaults.ts +34 -18
  198. package/src/channels/slack/dispatch.ts +121 -6
  199. package/src/channels/slack/inbound.ts +60 -0
  200. package/src/channels/slack/index.ts +1 -0
  201. package/src/channels/slack/live-delivery.ts +17 -3
  202. package/src/channels/slack/manifest.ts +1 -0
  203. package/src/channels/slack/slack-channel.ts +7 -0
  204. package/src/channels/slack/types.ts +47 -1
  205. package/src/index.ts +9 -0
  206. package/src/internal/cli-ax.ts +25 -1
  207. package/src/internal/cli-deploy.ts +22 -4
  208. package/src/internal/cli-mcp-oauth.ts +3 -1
  209. package/src/internal/cli-mcp.ts +4 -4
  210. package/src/internal/cursor-event-relay.ts +168 -0
  211. package/src/internal/deploy-client.ts +1 -1
  212. package/src/internal/discovery.ts +42 -5
  213. package/src/internal/distribution.ts +3 -1
  214. package/src/internal/durable-session-store.ts +95 -0
  215. package/src/internal/hosting.ts +2 -2
  216. package/src/internal/init-project.ts +151 -2
  217. package/src/internal/ledger-serving.ts +240 -0
  218. package/src/internal/listen-port.ts +85 -0
  219. package/src/internal/mcp-oauth.ts +29 -4
  220. package/src/internal/playground/static.ts +111 -20
  221. package/src/internal/sdk-runner.ts +11 -1
  222. package/src/internal/server.ts +76 -62
  223. package/src/internal/session-engine.ts +192 -16
  224. package/src/internal/slack-provision-client.ts +1 -1
  225. package/src/internal/workspace.ts +14 -3
  226. package/src/kv/memory-kv.ts +99 -0
  227. package/src/kv.ts +27 -0
  228. package/src/ledger/session-ledger.ts +535 -0
  229. package/src/ledger.ts +29 -0
  230. package/src/storage-backends/cursor-hosted.ts +108 -3
  231. package/src/storage-backends/file-kv.ts +128 -12
  232. package/src/storage.ts +90 -6
  233. package/src/types.ts +130 -4
  234. package/dist/channels/slack/post-update-delivery.d.ts +0 -85
  235. package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
  236. package/dist/docs/assets/chunks/@localSearchIndexroot.B-VTH4As.js +0 -1
  237. package/dist/docs/assets/example-agents_index.md.DgGBwckv.js +0 -2
  238. package/dist/docs/assets/index.md.CmhptOmN.js +0 -24
  239. package/dist/docs/assets/troubleshooting.md.B5RVX_tL.js +0 -1
  240. package/dist/internal/json-dir-store.d.ts +0 -32
  241. package/dist/internal/json-dir-store.d.ts.map +0 -1
  242. package/dist/internal/persistence-coordinator.d.ts +0 -127
  243. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  244. package/dist/multi-tenant.d.ts +0 -80
  245. package/dist/multi-tenant.d.ts.map +0 -1
  246. package/dist/multi-tenant.js +0 -69
  247. package/dist/persistence.d.ts +0 -184
  248. package/dist/persistence.d.ts.map +0 -1
  249. package/dist/playground/assets/index-BRhBbnd4.css +0 -1
  250. package/dist/playground/assets/index-_2WKw9a-.js +0 -85
  251. package/dist/storage-backends/postgres-kv.d.ts +0 -82
  252. package/dist/storage-backends/postgres-kv.d.ts.map +0 -1
  253. package/dist/storage-backends/postgres-kv.js +0 -185
  254. package/src/storage-backends/postgres-kv.ts +0 -253
  255. /package/dist/docs/assets/{ab.md.6cLOW7--.lean.js → ab.md.hdxsvX5M.lean.js} +0 -0
  256. /package/dist/docs/assets/{building-with-agents.md.txrcGU2B.lean.js → building-with-agents.md.PeZaZA1P.lean.js} +0 -0
  257. /package/dist/docs/assets/{evals.md.BQXI3rXy.lean.js → evals.md.qeoGgPw0.lean.js} +0 -0
  258. /package/dist/docs/assets/{example-agents_index.md.DgGBwckv.lean.js → example-agents_index.md.xmJ87d_3.lean.js} +0 -0
  259. /package/dist/docs/assets/{quickstart.md.C_b6ESpD.lean.js → quickstart.md.rIbfahJM.lean.js} +0 -0
@@ -0,0 +1,635 @@
1
+ /**
2
+ * Download and classify Slack file uploads for agent turns.
3
+ *
4
+ * Images become vision inputs (SDK `images`). Documents and videos land under
5
+ * `slack-attachments/` in the session workspace so the model can Read them.
6
+ */
7
+
8
+ import { callSlackApiSoft, isSlackApiTransport } from "./api.js";
9
+ import { tryResolveBotToken } from "./credentials.js";
10
+ import type { SlackLog } from "./log.js";
11
+ import type { SlackBotToken, SlackMessageFile } from "./types.js";
12
+
13
+ export const SLACK_ATTACHMENTS_DIR = "slack-attachments";
14
+ export const SLACK_ATTACHMENT_ONLY_PROMPT =
15
+ "The user shared Slack attachment(s) with no message text. Review the attached files.";
16
+
17
+ const DEFAULT_MAX_FILE_BYTES = 15 * 1024 * 1024;
18
+ const DEFAULT_MAX_TOTAL_BYTES = 35 * 1024 * 1024;
19
+ const DEFAULT_MAX_FILES = 8;
20
+ const DEFAULT_DOWNLOAD_TIMEOUT_MS = 10_000;
21
+
22
+ const TEXT_SLACK_FILETYPES = new Set([
23
+ "text",
24
+ "markdown",
25
+ "md",
26
+ "diff",
27
+ "csv",
28
+ "tsv",
29
+ "json",
30
+ "xml",
31
+ "yaml",
32
+ "c",
33
+ "csharp",
34
+ "cpp",
35
+ "css",
36
+ "clojure",
37
+ "coffeescript",
38
+ "dart",
39
+ "dockerfile",
40
+ "email",
41
+ "erlang",
42
+ "fsharp",
43
+ "go",
44
+ "groovy",
45
+ "html",
46
+ "haskell",
47
+ "java",
48
+ "javascript",
49
+ "kotlin",
50
+ "latex",
51
+ "lisp",
52
+ "lua",
53
+ "matlab",
54
+ "objc",
55
+ "ocaml",
56
+ "pascal",
57
+ "perl",
58
+ "php",
59
+ "powershell",
60
+ "python",
61
+ "r",
62
+ "ruby",
63
+ "rust",
64
+ "sql",
65
+ "sass",
66
+ "scala",
67
+ "scheme",
68
+ "shell",
69
+ "swift",
70
+ "typescript",
71
+ "vb",
72
+ "vbscript",
73
+ ]);
74
+
75
+ type JsonRecord = Record<string, unknown>;
76
+
77
+ function asRecord(value: unknown): JsonRecord | null {
78
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
79
+ return null;
80
+ }
81
+ return value as JsonRecord;
82
+ }
83
+
84
+ function asString(value: unknown): string | undefined {
85
+ return typeof value === "string" ? value : undefined;
86
+ }
87
+
88
+ function asNumber(value: unknown): number | undefined {
89
+ return typeof value === "number" && Number.isFinite(value)
90
+ ? value
91
+ : undefined;
92
+ }
93
+
94
+ function normalizeMimeType(mimeType: string | undefined): string {
95
+ return (mimeType ?? "").split(";")[0].trim().toLowerCase();
96
+ }
97
+
98
+ function getLowercaseFileExtension(filename: string | undefined): string {
99
+ if (filename === undefined || filename.length === 0) {
100
+ return "";
101
+ }
102
+ const extensionStart = filename.lastIndexOf(".");
103
+ if (extensionStart < 0 || extensionStart === filename.length - 1) {
104
+ return "";
105
+ }
106
+ return filename.slice(extensionStart + 1).toLowerCase();
107
+ }
108
+
109
+ /** Keep path-safe basename pieces (mirrors cloud-agent sanitizeFilename). */
110
+ export function sanitizeSlackFilename(name: string): string {
111
+ const trimmed = name.trim().replace(/[/\\?%*:|"<>]/g, "_");
112
+ return trimmed.length > 0 ? trimmed.slice(0, 180) : "file";
113
+ }
114
+
115
+ export function isSupportedImageFormat(mimeType: string): boolean {
116
+ return [
117
+ "image/jpeg",
118
+ "image/jpg",
119
+ "image/png",
120
+ "image/gif",
121
+ "image/webp",
122
+ ].includes(normalizeMimeType(mimeType));
123
+ }
124
+
125
+ export function isSupportedDocumentFormat(args: {
126
+ mimeType: string;
127
+ filename?: string;
128
+ }): boolean {
129
+ const mimeType = normalizeMimeType(args.mimeType);
130
+ const extension = getLowercaseFileExtension(args.filename);
131
+ const supportedExtensions = new Set([
132
+ "7z",
133
+ "bz2",
134
+ "doc",
135
+ "docx",
136
+ "gz",
137
+ "pdf",
138
+ "ppt",
139
+ "pptx",
140
+ "rar",
141
+ "tar",
142
+ "tbz",
143
+ "tbz2",
144
+ "tgz",
145
+ "txt",
146
+ "md",
147
+ "markdown",
148
+ "csv",
149
+ "xlsx",
150
+ "xz",
151
+ "json",
152
+ "jsonl",
153
+ "xml",
154
+ "yaml",
155
+ "yml",
156
+ "toml",
157
+ "txz",
158
+ "log",
159
+ "tzst",
160
+ "zip",
161
+ "zst",
162
+ ]);
163
+ const supportedMimeTypes = new Set([
164
+ "application/gzip",
165
+ "application/msword",
166
+ "application/pdf",
167
+ "application/json",
168
+ "application/jsonl",
169
+ "application/vnd.rar",
170
+ "application/vnd.ms-powerpoint",
171
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
172
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
173
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
174
+ "application/xml",
175
+ "application/x-7z-compressed",
176
+ "application/x-bzip2",
177
+ "application/x-compressed-tar",
178
+ "application/x-gzip",
179
+ "application/x-gtar",
180
+ "application/x-rar-compressed",
181
+ "application/x-tar",
182
+ "application/x-xz",
183
+ "application/x-yaml",
184
+ "application/x-zip-compressed",
185
+ "application/yaml",
186
+ "application/zip",
187
+ "application/toml",
188
+ "application/zstd",
189
+ "application/x-zstd",
190
+ "text/csv",
191
+ "text/markdown",
192
+ "text/plain",
193
+ "text/xml",
194
+ "text/x-log",
195
+ "text/x-markdown",
196
+ "text/yaml",
197
+ ]);
198
+ return (
199
+ mimeType.startsWith("text/") ||
200
+ supportedMimeTypes.has(mimeType) ||
201
+ supportedExtensions.has(extension)
202
+ );
203
+ }
204
+
205
+ export function isSupportedVideoFormat(mimeType: string): boolean {
206
+ return normalizeMimeType(mimeType).startsWith("video/");
207
+ }
208
+
209
+ /** Raw Slack file object (message `files[]` or forwarded share). */
210
+ export interface RawSlackFile {
211
+ id?: unknown;
212
+ mimetype?: unknown;
213
+ filetype?: unknown;
214
+ mode?: unknown;
215
+ name?: unknown;
216
+ title?: unknown;
217
+ size?: unknown;
218
+ url_private_download?: unknown;
219
+ dataBase64?: unknown;
220
+ }
221
+
222
+ export type SlackFileClassification =
223
+ | { kind: "image"; mimeType: string }
224
+ | { kind: "video"; mimeType: string }
225
+ | { kind: "document"; mimeType: string; isTextLike: boolean };
226
+
227
+ function hasTextSlackHint(file: RawSlackFile): boolean {
228
+ if (file.mode === "snippet") {
229
+ return true;
230
+ }
231
+ const filetype =
232
+ typeof file.filetype === "string" ? file.filetype.trim().toLowerCase() : "";
233
+ return TEXT_SLACK_FILETYPES.has(filetype);
234
+ }
235
+
236
+ /**
237
+ * Classify a Slack file as image / document / video. Tolerates empty
238
+ * `mimetype` on snippets (Slack fills it asynchronously).
239
+ */
240
+ export function classifySlackFile(
241
+ file: RawSlackFile
242
+ ): SlackFileClassification | undefined {
243
+ const populatedMimeType =
244
+ typeof file.mimetype === "string" ? normalizeMimeType(file.mimetype) : "";
245
+ const textHint = hasTextSlackHint(file);
246
+ const mimeType =
247
+ populatedMimeType.length > 0
248
+ ? populatedMimeType
249
+ : textHint
250
+ ? "text/plain"
251
+ : "";
252
+ if (mimeType.length === 0) {
253
+ return undefined;
254
+ }
255
+ if (isSupportedImageFormat(mimeType)) {
256
+ return { kind: "image", mimeType };
257
+ }
258
+ const filename =
259
+ typeof file.name === "string"
260
+ ? file.name
261
+ : typeof file.title === "string"
262
+ ? file.title
263
+ : undefined;
264
+ const isTextLike = textHint || mimeType.startsWith("text/");
265
+ if (isTextLike || isSupportedDocumentFormat({ mimeType, filename })) {
266
+ return { kind: "document", mimeType, isTextLike };
267
+ }
268
+ if (isSupportedVideoFormat(mimeType)) {
269
+ return { kind: "video", mimeType };
270
+ }
271
+ return undefined;
272
+ }
273
+
274
+ function isForwardedShareAttachment(value: unknown): value is JsonRecord {
275
+ const record = asRecord(value);
276
+ return record !== null && record.is_share === true;
277
+ }
278
+
279
+ /** Top-level uploads plus files nested in forwarded `is_share` attachments. */
280
+ export function collectSlackMessageFiles(message: unknown): RawSlackFile[] {
281
+ const record = asRecord(message);
282
+ if (record === null) {
283
+ return [];
284
+ }
285
+ const files: RawSlackFile[] = Array.isArray(record.files)
286
+ ? [...(record.files as RawSlackFile[])]
287
+ : [];
288
+ if (Array.isArray(record.attachments)) {
289
+ for (const attachment of record.attachments) {
290
+ if (
291
+ isForwardedShareAttachment(attachment) &&
292
+ Array.isArray(attachment.files)
293
+ ) {
294
+ files.push(...(attachment.files as RawSlackFile[]));
295
+ }
296
+ }
297
+ }
298
+ return files;
299
+ }
300
+
301
+ export function parseSlackFileRefs(
302
+ message: unknown
303
+ ): SlackMessageFile[] | undefined {
304
+ const files = collectSlackMessageFiles(message);
305
+ const refs: SlackMessageFile[] = [];
306
+ for (const file of files) {
307
+ const id = asString(file.id);
308
+ if (id === undefined || id.length === 0) {
309
+ continue;
310
+ }
311
+ refs.push({
312
+ id,
313
+ name: asString(file.name),
314
+ title: asString(file.title),
315
+ mimetype: asString(file.mimetype),
316
+ filetype: asString(file.filetype),
317
+ mode: asString(file.mode),
318
+ size: asNumber(file.size),
319
+ dataBase64: asString(file.dataBase64),
320
+ });
321
+ }
322
+ return refs.length > 0 ? refs : undefined;
323
+ }
324
+
325
+ export interface SlackTurnImage {
326
+ data: string;
327
+ mimeType: string;
328
+ }
329
+
330
+ /** Relative path → utf8 or base64-blob workspace entry. */
331
+ export type SlackWorkspaceBlob =
332
+ | string
333
+ | { encoding: "base64"; data: string; mimeType?: string };
334
+
335
+ export interface PreparedSlackAttachments {
336
+ images: SlackTurnImage[];
337
+ workspaceFiles: Record<string, SlackWorkspaceBlob>;
338
+ /** Prompt markers like `[Image 1: shot.png]`. */
339
+ annotations: string[];
340
+ }
341
+
342
+ function attachmentFilename(file: SlackMessageFile): string {
343
+ return sanitizeSlackFilename(file.name ?? file.title ?? file.id);
344
+ }
345
+
346
+ function looksLikeHtml(bytes: Uint8Array): boolean {
347
+ const head = Buffer.from(bytes.subarray(0, 100))
348
+ .toString("utf8")
349
+ .toLowerCase();
350
+ return head.includes("<html");
351
+ }
352
+
353
+ function isTextLikeBytes(args: {
354
+ mimeType: string;
355
+ isTextLike: boolean;
356
+ bytes: Uint8Array;
357
+ }): boolean {
358
+ if (args.isTextLike || args.mimeType.startsWith("text/")) {
359
+ return true;
360
+ }
361
+ // Reject obvious binary before attempting utf8 decode for docs.
362
+ for (let i = 0; i < Math.min(args.bytes.length, 512); i++) {
363
+ if (args.bytes[i] === 0) {
364
+ return false;
365
+ }
366
+ }
367
+ return (
368
+ args.mimeType === "application/json" ||
369
+ args.mimeType === "application/jsonl" ||
370
+ args.mimeType === "application/xml" ||
371
+ args.mimeType === "application/yaml" ||
372
+ args.mimeType === "application/x-yaml" ||
373
+ args.mimeType === "application/toml"
374
+ );
375
+ }
376
+
377
+ async function downloadSlackFileBytes(args: {
378
+ botToken: SlackBotToken | undefined;
379
+ fileId: string;
380
+ fallbackMimeType: string;
381
+ maxBytes: number;
382
+ downloadTimeoutMs: number;
383
+ }): Promise<{ data: Uint8Array; mimeType: string; filename: string } | null> {
384
+ // cursorAccount transport cannot mint a Bearer for files.slack.com; the
385
+ // relay must pre-attach `dataBase64` on the file ref instead.
386
+ if (isSlackApiTransport(args.botToken)) {
387
+ return null;
388
+ }
389
+
390
+ const info = await callSlackApiSoft({
391
+ botToken: args.botToken,
392
+ operation: "files.info",
393
+ body: { file: args.fileId },
394
+ });
395
+ if (info.ok !== true) {
396
+ return null;
397
+ }
398
+ const file = asRecord(info.file);
399
+ if (file === null) {
400
+ return null;
401
+ }
402
+ const downloadUrl = asString(file.url_private_download);
403
+ if (downloadUrl === undefined || downloadUrl.length === 0) {
404
+ return null;
405
+ }
406
+ const fileSize = asNumber(file.size);
407
+ if (fileSize !== undefined && fileSize > args.maxBytes) {
408
+ return null;
409
+ }
410
+
411
+ const botToken = await tryResolveBotToken({ botToken: args.botToken });
412
+ const headers: Record<string, string> = {};
413
+ if (botToken !== undefined && botToken.length > 0) {
414
+ headers.Authorization = `Bearer ${botToken}`;
415
+ }
416
+
417
+ let response: Response;
418
+ try {
419
+ response = await fetch(downloadUrl, {
420
+ headers,
421
+ signal: AbortSignal.timeout(args.downloadTimeoutMs),
422
+ });
423
+ } catch {
424
+ return null;
425
+ }
426
+ if (!response.ok) {
427
+ return null;
428
+ }
429
+ const contentLengthHeader = response.headers.get("content-length");
430
+ if (contentLengthHeader !== null && contentLengthHeader.length > 0) {
431
+ const contentLength = Number.parseInt(contentLengthHeader, 10);
432
+ if (!Number.isNaN(contentLength) && contentLength > args.maxBytes) {
433
+ return null;
434
+ }
435
+ }
436
+
437
+ const buffer = Buffer.from(await response.arrayBuffer());
438
+ if (buffer.length > args.maxBytes) {
439
+ return null;
440
+ }
441
+ const actualContentType = response.headers.get("content-type");
442
+ if (
443
+ normalizeMimeType(actualContentType ?? undefined) === "text/html" ||
444
+ looksLikeHtml(buffer)
445
+ ) {
446
+ return null;
447
+ }
448
+ const responseMimeType = normalizeMimeType(actualContentType ?? undefined);
449
+ const metadataMimeType = normalizeMimeType(
450
+ asString(file.mimetype) ?? args.fallbackMimeType
451
+ );
452
+ const effectiveMimeType =
453
+ responseMimeType.length > 0 &&
454
+ responseMimeType !== "application/octet-stream"
455
+ ? responseMimeType
456
+ : metadataMimeType;
457
+ return {
458
+ data: new Uint8Array(buffer),
459
+ mimeType:
460
+ effectiveMimeType.length > 0 ? effectiveMimeType : args.fallbackMimeType,
461
+ filename: sanitizeSlackFilename(
462
+ asString(file.name) ?? asString(file.title) ?? args.fileId
463
+ ),
464
+ };
465
+ }
466
+
467
+ export interface PrepareSlackAttachmentsOptions {
468
+ botToken?: SlackBotToken;
469
+ files: readonly SlackMessageFile[];
470
+ maxFiles?: number;
471
+ maxFileBytes?: number;
472
+ maxTotalBytes?: number;
473
+ downloadTimeoutMs?: number;
474
+ log?: SlackLog;
475
+ }
476
+
477
+ /**
478
+ * Download (or use pre-attached) Slack files and split them into vision
479
+ * images + workspace blobs for the turn.
480
+ */
481
+ export async function prepareSlackAttachments(
482
+ options: PrepareSlackAttachmentsOptions
483
+ ): Promise<PreparedSlackAttachments> {
484
+ const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
485
+ const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
486
+ const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
487
+ const downloadTimeoutMs =
488
+ options.downloadTimeoutMs ?? DEFAULT_DOWNLOAD_TIMEOUT_MS;
489
+
490
+ const loaded = await Promise.all(
491
+ options.files.slice(0, maxFiles).map(async (file) => {
492
+ const classification = classifySlackFile(file);
493
+ if (classification === undefined) {
494
+ return undefined;
495
+ }
496
+ if (file.size !== undefined && file.size > maxFileBytes) {
497
+ options.log?.info("slack attachment skipped (size)", {
498
+ file_id: file.id,
499
+ size: file.size,
500
+ max: maxFileBytes,
501
+ });
502
+ return undefined;
503
+ }
504
+
505
+ if (file.dataBase64 !== undefined && file.dataBase64.length > 0) {
506
+ return {
507
+ file,
508
+ classification,
509
+ bytes: new Uint8Array(Buffer.from(file.dataBase64, "base64")),
510
+ mimeType: classification.mimeType,
511
+ filename: attachmentFilename(file),
512
+ };
513
+ }
514
+
515
+ const downloaded = await downloadSlackFileBytes({
516
+ botToken: options.botToken,
517
+ fileId: file.id,
518
+ fallbackMimeType: classification.mimeType,
519
+ maxBytes: maxFileBytes,
520
+ downloadTimeoutMs,
521
+ });
522
+ if (downloaded === null) {
523
+ options.log?.info("slack attachment download failed", {
524
+ file_id: file.id,
525
+ kind: classification.kind,
526
+ });
527
+ return undefined;
528
+ }
529
+ return {
530
+ file,
531
+ classification,
532
+ bytes: downloaded.data,
533
+ mimeType: downloaded.mimeType,
534
+ filename: downloaded.filename,
535
+ };
536
+ })
537
+ );
538
+
539
+ const images: SlackTurnImage[] = [];
540
+ const workspaceFiles: Record<string, SlackWorkspaceBlob> = {};
541
+ const annotations: string[] = [];
542
+ let totalBytes = 0;
543
+ let imageIndex = 0;
544
+ let documentIndex = 0;
545
+ let videoIndex = 0;
546
+
547
+ for (const item of loaded) {
548
+ if (item === undefined) {
549
+ continue;
550
+ }
551
+ const { file, classification, bytes, mimeType, filename } = item;
552
+ if (bytes.length === 0) {
553
+ options.log?.info("slack attachment download failed", {
554
+ file_id: file.id,
555
+ kind: classification.kind,
556
+ });
557
+ continue;
558
+ }
559
+ if (
560
+ bytes.length > maxFileBytes ||
561
+ totalBytes + bytes.length > maxTotalBytes
562
+ ) {
563
+ options.log?.info("slack attachment skipped (budget)", {
564
+ file_id: file.id,
565
+ bytes: bytes.length,
566
+ });
567
+ continue;
568
+ }
569
+ totalBytes += bytes.length;
570
+
571
+ if (classification.kind === "image") {
572
+ if (!isSupportedImageFormat(mimeType)) {
573
+ continue;
574
+ }
575
+ imageIndex++;
576
+ images.push({
577
+ data: Buffer.from(bytes).toString("base64"),
578
+ mimeType,
579
+ });
580
+ annotations.push(`[Image ${imageIndex}: ${filename}]`);
581
+ continue;
582
+ }
583
+
584
+ const relativePath = `${SLACK_ATTACHMENTS_DIR}/${filename}`;
585
+ const textLike =
586
+ classification.kind === "document" &&
587
+ isTextLikeBytes({
588
+ mimeType,
589
+ isTextLike: classification.isTextLike,
590
+ bytes,
591
+ });
592
+
593
+ if (textLike) {
594
+ workspaceFiles[relativePath] = Buffer.from(bytes).toString("utf8");
595
+ } else {
596
+ workspaceFiles[relativePath] = {
597
+ encoding: "base64",
598
+ data: Buffer.from(bytes).toString("base64"),
599
+ mimeType,
600
+ };
601
+ }
602
+
603
+ if (classification.kind === "document") {
604
+ documentIndex++;
605
+ annotations.push(
606
+ `[Document ${documentIndex}: ${filename} → ${relativePath}]`
607
+ );
608
+ } else {
609
+ videoIndex++;
610
+ annotations.push(`[Video ${videoIndex}: ${filename} → ${relativePath}]`);
611
+ }
612
+ }
613
+
614
+ return { images, workspaceFiles, annotations };
615
+ }
616
+
617
+ /** Merge attachment annotations into the user prompt. */
618
+ export function appendSlackAttachmentAnnotations(
619
+ prompt: string,
620
+ annotations: readonly string[]
621
+ ): string {
622
+ if (annotations.length === 0) {
623
+ return prompt;
624
+ }
625
+ const block = [
626
+ "<slack_attachments>",
627
+ ...annotations,
628
+ "</slack_attachments>",
629
+ ].join("\n");
630
+ const trimmed = prompt.trim();
631
+ if (trimmed.length === 0) {
632
+ return `${block}\n\n${SLACK_ATTACHMENT_ONLY_PROMPT}`;
633
+ }
634
+ return `${block}\n\n${trimmed}`;
635
+ }
@@ -22,6 +22,7 @@ import type {
22
22
  SlackChannelOptions,
23
23
  SlackCredentials,
24
24
  SlackMessage,
25
+ SlackMessageFile,
25
26
  SlackMessageHandler,
26
27
  } from "./types.js";
27
28
 
@@ -72,6 +73,12 @@ export interface CursorSlackEnvelope {
72
73
  matchedBy: "name" | "thread";
73
74
  eventId?: string;
74
75
  receivedAtMs: number;
76
+ /**
77
+ * Slack file uploads on the triggering message. When `dataBase64` is set
78
+ * the Cursor backend already downloaded bytes (cursorAccount has no bot
79
+ * token for files.slack.com).
80
+ */
81
+ files?: SlackMessageFile[];
75
82
  }
76
83
 
77
84
  /** Serve-time runtime handle for one cursor-account Slack channel. */
@@ -149,6 +156,7 @@ export async function dispatchCursorSlackEnvelope(args: {
149
156
  : {
150
157
  author: { userId: envelope.slackUserId, isBot: false },
151
158
  }),
159
+ files: envelope.files,
152
160
  };
153
161
 
154
162
  // DMs to @Cursor route as direct messages; everything else (explicit