@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
@@ -155,6 +155,25 @@ export function buildDefaultEvents(
155
155
  const getLog = options.getLog ?? (() => defaultSlackLog);
156
156
  const strategy = createStreamDeliveryStrategy({ botToken, getLog });
157
157
 
158
+ const clearWorkingStatus = async (
159
+ channelId: string,
160
+ threadTs: string
161
+ ): Promise<void> => {
162
+ const result = await setThreadStatus({
163
+ botToken,
164
+ channelId,
165
+ threadTs,
166
+ status: "",
167
+ });
168
+ if (!result.ok) {
169
+ getLog().warn("assistant.threads.setStatus not-ok", {
170
+ error: result.error,
171
+ channel: channelId,
172
+ thread_ts: threadTs,
173
+ });
174
+ }
175
+ };
176
+
158
177
  return {
159
178
  async "turn.started"(event, channel) {
160
179
  const log = getLog();
@@ -366,15 +385,6 @@ export function buildDefaultEvents(
366
385
  return;
367
386
  }
368
387
 
369
- const clearStatus = async (): Promise<void> => {
370
- await setThreadStatus({
371
- botToken,
372
- channelId: state.channelId!,
373
- threadTs: state.threadTs!,
374
- status: "",
375
- });
376
- };
377
-
378
388
  // Empty final text: still close any open stream / clear the skeleton
379
389
  // and clear typing.
380
390
  if (event.data.text === "") {
@@ -386,7 +396,7 @@ export function buildDefaultEvents(
386
396
  });
387
397
  channel.state.streamTs = null;
388
398
  }
389
- await clearStatus();
399
+ await clearWorkingStatus(state.channelId, state.threadTs);
390
400
  return;
391
401
  }
392
402
 
@@ -405,7 +415,7 @@ export function buildDefaultEvents(
405
415
  sessionId: event.sessionId,
406
416
  });
407
417
  if (delivered) {
408
- await clearStatus();
418
+ await clearWorkingStatus(state.channelId, state.threadTs);
409
419
  return;
410
420
  }
411
421
  }
@@ -439,7 +449,7 @@ export function buildDefaultEvents(
439
449
  );
440
450
  }
441
451
 
442
- await clearStatus();
452
+ await clearWorkingStatus(state.channelId, state.threadTs);
443
453
  },
444
454
 
445
455
  async "turn.completed"(event, channel) {
@@ -453,6 +463,17 @@ export function buildDefaultEvents(
453
463
 
454
464
  await strategy.cleanupOrphan({ channel });
455
465
 
466
+ // Always drop the assistant chip. message.completed already clears
467
+ // after a normal reply, but a tool-call-only finish (or a missed
468
+ // final assistant message) never hits that path — leaving
469
+ // "Working…" stuck until Slack expires it.
470
+ if (channel.state.channelId !== null && channel.state.threadTs !== null) {
471
+ await clearWorkingStatus(
472
+ channel.state.channelId,
473
+ channel.state.threadTs
474
+ );
475
+ }
476
+
456
477
  if (
457
478
  suggestedPrompts.length === 0 ||
458
479
  channel.state.channelId === null ||
@@ -518,12 +539,7 @@ export function buildDefaultEvents(
518
539
  );
519
540
  }
520
541
  }
521
- await setThreadStatus({
522
- botToken,
523
- channelId: state.channelId,
524
- threadTs: state.threadTs,
525
- status: "",
526
- });
542
+ await clearWorkingStatus(state.channelId, state.threadTs);
527
543
  },
528
544
  };
529
545
  }
@@ -10,6 +10,11 @@ import type {
10
10
  } from "../../types.js";
11
11
  import { buildSlackBinding, slackContinuationKey } from "./api.js";
12
12
  import { firstLineTitle, maybeSetThreadTitle } from "./assistant.js";
13
+ import {
14
+ appendSlackAttachmentAnnotations,
15
+ prepareSlackAttachments,
16
+ SLACK_ATTACHMENT_ONLY_PROMPT,
17
+ } from "./attachments.js";
13
18
  import type { SlackBotMentionGate } from "./bot-mentions.js";
14
19
  import type { ChannelPostWatcher } from "./channel-watch.js";
15
20
  import {
@@ -23,6 +28,7 @@ import {
23
28
  parseAppMentionEvent,
24
29
  parseChannelMessageEvent,
25
30
  parseDirectMessageEvent,
31
+ parseReactionAddedEvent,
26
32
  } from "./inbound.js";
27
33
  import { defaultSlackLog, type SlackLog } from "./log.js";
28
34
  import { isWholeMessageStop } from "./stop.js";
@@ -31,6 +37,7 @@ import {
31
37
  loadSlackThreadDeltaBlock,
32
38
  } from "./thread-context.js";
33
39
  import type {
40
+ SlackBotToken,
34
41
  SlackChannelState,
35
42
  SlackContinuationProbes,
36
43
  SlackCredentials,
@@ -41,6 +48,55 @@ import type {
41
48
 
42
49
  const FOLLOWUP_TYPING = "Saw your follow-up…";
43
50
 
51
+ function slackUserText(message: SlackMessage, override?: string): string {
52
+ if (override !== undefined) {
53
+ return override;
54
+ }
55
+ if (message.markdown.trim().length > 0) {
56
+ return message.markdown;
57
+ }
58
+ if (message.files !== undefined && message.files.length > 0) {
59
+ return SLACK_ATTACHMENT_ONLY_PROMPT;
60
+ }
61
+ return message.markdown;
62
+ }
63
+
64
+ async function slackTurnMedia(args: {
65
+ message: SlackMessage;
66
+ decision: SlackDispatchDecision;
67
+ botToken: SlackBotToken | undefined;
68
+ log: SlackLog;
69
+ }): Promise<{
70
+ userText: string;
71
+ workspaceFiles?: SlackDispatchDecision["workspaceFiles"];
72
+ images?: SlackDispatchDecision["images"];
73
+ }> {
74
+ const prepared =
75
+ args.message.files !== undefined && args.message.files.length > 0
76
+ ? await prepareSlackAttachments({
77
+ botToken: args.botToken,
78
+ files: args.message.files,
79
+ log: args.log,
80
+ })
81
+ : undefined;
82
+
83
+ const workspaceFiles = {
84
+ ...args.decision.workspaceFiles,
85
+ ...prepared?.workspaceFiles,
86
+ };
87
+ const images = [...(args.decision.images ?? []), ...(prepared?.images ?? [])];
88
+
89
+ return {
90
+ userText: appendSlackAttachmentAnnotations(
91
+ slackUserText(args.message, args.decision.message),
92
+ prepared?.annotations ?? []
93
+ ),
94
+ workspaceFiles:
95
+ Object.keys(workspaceFiles).length > 0 ? workspaceFiles : undefined,
96
+ images: images.length > 0 ? images : undefined,
97
+ };
98
+ }
99
+
44
100
  /** Blank per-session adapter state. */
45
101
  export function emptySlackChannelState(
46
102
  patch: Partial<SlackChannelState> = {}
@@ -68,6 +124,7 @@ export interface SlackInboundDispatchDeps extends SlackContinuationProbes {
68
124
  waitUntil: (promise: Promise<unknown>) => void;
69
125
  onAppMention: SlackMessageHandler;
70
126
  onDirectMessage: SlackMessageHandler;
127
+ onReactionAdded?: SlackMessageHandler;
71
128
  credentials: SlackCredentials | undefined;
72
129
  eventIds: EventIdDedupe;
73
130
  host: HostContext;
@@ -117,7 +174,12 @@ function triggeringMergeMessage(
117
174
  }
118
175
 
119
176
  export async function dispatchInboundMessage(args: {
120
- kind: "app_mention" | "direct_message" | "channel_post" | "block_action";
177
+ kind:
178
+ | "app_mention"
179
+ | "direct_message"
180
+ | "channel_post"
181
+ | "reaction_added"
182
+ | "block_action";
121
183
  message: SlackMessage;
122
184
  handler: SlackMessageHandler;
123
185
  send: SendMessageFn;
@@ -302,7 +364,22 @@ export async function dispatchInboundMessage(args: {
302
364
  return;
303
365
  }
304
366
 
305
- const userText = decision.message ?? message.markdown;
367
+ const media = await slackTurnMedia({
368
+ message,
369
+ decision,
370
+ botToken: args.credentials?.botToken,
371
+ log,
372
+ });
373
+ if (message.files !== undefined && message.files.length > 0) {
374
+ log.info("slack attachments prepared", {
375
+ kind,
376
+ channel: message.channelId,
377
+ thread_ts: message.threadTs,
378
+ images: media.images?.length ?? 0,
379
+ workspace_files: Object.keys(media.workspaceFiles ?? {}).length,
380
+ event_id: args.eventId,
381
+ });
382
+ }
306
383
  const excludeThreadTimestamps = [
307
384
  message.ts,
308
385
  ...(decision.excludeThreadTimestamps ?? []),
@@ -347,6 +424,7 @@ export async function dispatchInboundMessage(args: {
347
424
  ...(afterTs === undefined ? {} : { after_ts: afterTs }),
348
425
  });
349
426
  }
427
+
350
428
  const prompt = [
351
429
  formatSlackContextBlock({
352
430
  userId: message.author?.userId ?? "",
@@ -360,7 +438,7 @@ export async function dispatchInboundMessage(args: {
360
438
  ...(threadContext === undefined ? [] : [threadContext]),
361
439
  ...(decision.context ?? []),
362
440
  "",
363
- userText,
441
+ media.userText,
364
442
  ]
365
443
  .filter((part) => part.length > 0)
366
444
  .join("\n");
@@ -378,7 +456,8 @@ export async function dispatchInboundMessage(args: {
378
456
  }) as unknown as JsonValue,
379
457
  title: decision.title ?? `slack:${message.channelId}:${message.threadTs}`,
380
458
  workspaceDir: decision.workspaceDir,
381
- workspaceFiles: decision.workspaceFiles,
459
+ workspaceFiles: media.workspaceFiles,
460
+ images: media.images,
382
461
  cloud: decision.cloud,
383
462
  sdkAgentId: decision.sdkAgentId,
384
463
  };
@@ -443,8 +522,8 @@ export async function dispatchInboundMessage(args: {
443
522
 
444
523
  /**
445
524
  * Parse an Events API body (or Socket Mode `events_api` payload) and dispatch
446
- * app_mention / DM handlers. Deduplicates by `event_id` when present. Returns
447
- * true when a handler was scheduled.
525
+ * app_mention / DM / reaction handlers. Deduplicates by `event_id` when
526
+ * present. Returns true when a handler was scheduled.
448
527
  */
449
528
  export function dispatchInboundFromEventBody(
450
529
  body: unknown,
@@ -566,6 +645,42 @@ export function dispatchInboundFromEventBody(
566
645
  return true;
567
646
  }
568
647
 
648
+ const reaction = parseReactionAddedEvent(body);
649
+ if (reaction !== null && deps.onReactionAdded !== undefined) {
650
+ deps.waitUntil(
651
+ dispatchInboundMessage({
652
+ kind: "reaction_added",
653
+ message: reaction,
654
+ handler: deps.onReactionAdded,
655
+ send: deps.send,
656
+ credentials: deps.credentials,
657
+ host: deps.host,
658
+ artifacts: deps.artifacts,
659
+ log,
660
+ evalCommandsEnabled: false,
661
+ cursorAgentName: deps.cursorAgentName,
662
+ blockExternals: deps.blockExternals,
663
+ ...(deps.hasContinuationSession === undefined
664
+ ? {}
665
+ : { hasContinuationSession: deps.hasContinuationSession }),
666
+ ...(deps.isContinuationBusy === undefined
667
+ ? {}
668
+ : { isContinuationBusy: deps.isContinuationBusy }),
669
+ ...(deps.getContinuationLastBotMessageTs === undefined
670
+ ? {}
671
+ : {
672
+ getContinuationLastBotMessageTs:
673
+ deps.getContinuationLastBotMessageTs,
674
+ }),
675
+ ...(deps.interruptContinuation === undefined
676
+ ? {}
677
+ : { interruptContinuation: deps.interruptContinuation }),
678
+ ...(eventId === undefined ? {} : { eventId }),
679
+ })
680
+ );
681
+ return true;
682
+ }
683
+
569
684
  if (deps.channelWatch !== undefined) {
570
685
  // Parse keeps bot-authored posts; the watcher enforces the per-channel
571
686
  // policy (includeBotPosts, own-post loop safety) with skip logging.
@@ -2,6 +2,7 @@
2
2
  * Parse Slack Events API payloads into structured inbound messages.
3
3
  */
4
4
 
5
+ import { parseSlackFileRefs } from "./attachments.js";
5
6
  import { parseDelegationMetadata } from "./bot-mentions.js";
6
7
  import { slackApiMessageBody } from "./message-body.js";
7
8
  import type { SlackAuthor, SlackMessage } from "./types.js";
@@ -69,6 +70,7 @@ function buildSlackMessage(
69
70
  const body = slackApiMessageBody(event);
70
71
  const threadTs = asString(event.thread_ts) ?? ts;
71
72
  const author = parseAuthor(event);
73
+ const files = parseSlackFileRefs(event);
72
74
  return {
73
75
  text,
74
76
  body,
@@ -78,6 +80,7 @@ function buildSlackMessage(
78
80
  channelId,
79
81
  ...(teamId === undefined ? {} : { teamId }),
80
82
  ...(author === undefined ? {} : { author }),
83
+ files,
81
84
  };
82
85
  }
83
86
 
@@ -155,6 +158,63 @@ export function parseDirectMessageEvent(body: unknown): SlackMessage | null {
155
158
  return buildSlackMessage(event, asString(root.team_id));
156
159
  }
157
160
 
161
+ /**
162
+ * Parse a message `reaction_added` event into a synthetic Slack message.
163
+ * The reacted-to timestamp becomes the thread root so normal hydration loads
164
+ * the complete thread before the dedicated handler dispatches a turn.
165
+ */
166
+ export function parseReactionAddedEvent(body: unknown): SlackMessage | null {
167
+ const root = asRecord(body);
168
+ if (root === null || root.type !== "event_callback") {
169
+ return null;
170
+ }
171
+ const event = asRecord(root.event);
172
+ if (event === null || event.type !== "reaction_added") {
173
+ return null;
174
+ }
175
+ const item = asRecord(event.item);
176
+ if (item === null || item.type !== "message") {
177
+ return null;
178
+ }
179
+ const userId = asString(event.user);
180
+ const reaction = asString(event.reaction);
181
+ const channelId = asString(item.channel);
182
+ const threadTs = asString(item.ts);
183
+ if (
184
+ userId === undefined ||
185
+ userId === "" ||
186
+ reaction === undefined ||
187
+ reaction === "" ||
188
+ channelId === undefined ||
189
+ channelId === "" ||
190
+ threadTs === undefined ||
191
+ threadTs === ""
192
+ ) {
193
+ return null;
194
+ }
195
+ const ts = asString(event.event_ts) ?? threadTs;
196
+ const text = `Slack user <@${userId}> added :${reaction}: to this thread.`;
197
+ const itemUserId = asString(event.item_user);
198
+ const teamId = asString(root.team_id);
199
+ return {
200
+ text,
201
+ body: text,
202
+ markdown: slackTextToMarkdown(text),
203
+ ts,
204
+ threadTs,
205
+ channelId,
206
+ ...(teamId === undefined ? {} : { teamId }),
207
+ author: {
208
+ userId,
209
+ isBot: false,
210
+ },
211
+ reaction: {
212
+ name: reaction,
213
+ ...(itemUserId === undefined ? {} : { itemUserId }),
214
+ },
215
+ };
216
+ }
217
+
158
218
  /** Parsed `message.channels` / `message.groups` event for channel watch. */
159
219
  export type SlackChannelMessageEvent =
160
220
  | { kind: "post"; message: SlackMessage }
@@ -21,6 +21,7 @@
21
21
  export * from "./api.js";
22
22
  export * from "./approvals.js";
23
23
  export * from "./assistant.js";
24
+ export * from "./attachments.js";
24
25
  export * from "./auth.js";
25
26
  export * from "./blocks.js";
26
27
  export * from "./bot-mentions.js";
@@ -183,9 +183,23 @@ export function createStreamDeliveryStrategy(
183
183
  await stopChatStream({ botToken, channel: channelId, ts: streamTs });
184
184
  },
185
185
 
186
- async cleanupOrphan() {
187
- // The stream path closes its message in message.completed / turn.failed;
188
- // there is nothing to clean up after turn.completed.
186
+ async cleanupOrphan({ channel }) {
187
+ // message.completed / turn.failed already stop the stream. If the
188
+ // turn ended without a final assistant message, close it here so
189
+ // Slack does not leave the live reply in a streaming state.
190
+ const streamTs = channel.state.streamTs;
191
+ const channelId =
192
+ channel.state.streamChannelId ?? channel.state.channelId;
193
+ if (
194
+ streamTs === null ||
195
+ streamTs === "" ||
196
+ channelId === null ||
197
+ channelId === ""
198
+ ) {
199
+ return;
200
+ }
201
+ await stopChatStream({ botToken, channel: channelId, ts: streamTs });
202
+ channel.state.streamTs = null;
189
203
  },
190
204
 
191
205
  async fail({ channel, channelId, streamTs, errorText }) {
@@ -66,6 +66,7 @@ const BOT_SCOPES = [
66
66
  "assistant:write",
67
67
  "channels:history",
68
68
  "chat:write",
69
+ "files:read",
69
70
  "groups:history",
70
71
  "im:history",
71
72
  "im:read",
@@ -187,6 +187,11 @@ export function slackChannel<const Name extends string = string>(
187
187
  "slackChannel({ cursorAccount: true }) does not support onBlockAction — interactive Block Kit clicks are not relayed over the Cursor Slack connection. Block actions are Socket Mode only."
188
188
  );
189
189
  }
190
+ if (options.onReactionAdded !== undefined) {
191
+ throw new Error(
192
+ "slackChannel({ cursorAccount: true }) does not support onReactionAdded because the Cursor Slack relay does not forward reaction events. Reaction handling is Socket Mode only."
193
+ );
194
+ }
190
195
  }
191
196
  const accountTransport = cursorAccount
192
197
  ? createPendingSlackApiTransport()
@@ -202,6 +207,7 @@ export function slackChannel<const Name extends string = string>(
202
207
  const defaultInbound = createDefaultOnInbound(loadingMessages, policy);
203
208
  const onAppMention = options.onAppMention ?? defaultInbound;
204
209
  const onDirectMessage = options.onDirectMessage ?? defaultInbound;
210
+ const onReactionAdded = options.onReactionAdded;
205
211
  const onChannelPost =
206
212
  options.onChannelPost ??
207
213
  createDefaultOnChannelPost(loadingMessages, policy);
@@ -277,6 +283,7 @@ export function slackChannel<const Name extends string = string>(
277
283
  waitUntil: args.waitUntil,
278
284
  onAppMention,
279
285
  onDirectMessage,
286
+ ...(onReactionAdded === undefined ? {} : { onReactionAdded }),
280
287
  credentials,
281
288
  eventIds,
282
289
  host: args.host,
@@ -89,6 +89,13 @@ export interface SlackAuthor {
89
89
  botId?: string;
90
90
  }
91
91
 
92
+ export interface SlackReactionAddedInfo {
93
+ /** Slack reaction name without surrounding colons. */
94
+ name: string;
95
+ /** User id of the message author, when Slack includes it. */
96
+ itemUserId?: string;
97
+ }
98
+
92
99
  /**
93
100
  * Delegation metadata carried on bot-to-bot mention posts (Slack message
94
101
  * `metadata`, event type {@link SLACK_DELEGATION_METADATA_EVENT_TYPE}).
@@ -104,6 +111,22 @@ export interface SlackDelegationInfo {
104
111
  originSessionId?: string;
105
112
  }
106
113
 
114
+ /** File upload on an inbound Slack message (see {@link SlackMessage.files}). */
115
+ export interface SlackMessageFile {
116
+ id: string;
117
+ name?: string;
118
+ title?: string;
119
+ mimetype?: string;
120
+ filetype?: string;
121
+ mode?: string;
122
+ size?: number;
123
+ /**
124
+ * When set (cursorAccount relay), bytes were already downloaded by the
125
+ * Cursor backend — skip a second `files.info` / private URL fetch.
126
+ */
127
+ dataBase64?: string;
128
+ }
129
+
107
130
  export interface SlackMessage {
108
131
  /** Raw top-level `text` exactly as Slack sent it. Often empty on bot posts. */
109
132
  text: string;
@@ -121,8 +144,15 @@ export interface SlackMessage {
121
144
  channelId: string;
122
145
  teamId?: string;
123
146
  author?: SlackAuthor;
147
+ /** Present only for a synthetic message dispatched from `reaction_added`. */
148
+ reaction?: SlackReactionAddedInfo;
124
149
  /** Parsed delegation metadata when the post carried it (bot mentions). */
125
150
  delegation?: SlackDelegationInfo;
151
+ /**
152
+ * Slack file uploads on this message (top-level + forwarded shares).
153
+ * Downloaded during dispatch into vision images / workspace files.
154
+ */
155
+ files?: SlackMessageFile[];
126
156
  }
127
157
 
128
158
  export interface SlackDispatchDecision {
@@ -144,8 +174,17 @@ export interface SlackDispatchDecision {
144
174
  * Relative path → contents written into the session workspace before the
145
175
  * first turn (new sessions only). Prefer this for host-prepared files like
146
176
  * `pr.txt` so default skill materialization stays on the session cwd.
177
+ * Values may be utf8 strings or `{ encoding: "base64", data }` blobs.
147
178
  */
148
- workspaceFiles?: Record<string, string>;
179
+ workspaceFiles?: Record<
180
+ string,
181
+ string | { encoding: "base64"; data: string; mimeType?: string }
182
+ >;
183
+ /**
184
+ * Base64 images attached to this turn for vision (Cursor SDK `images`).
185
+ * Populated automatically from Slack file uploads during inbound dispatch.
186
+ */
187
+ images?: Array<{ data: string; mimeType: string }>;
149
188
  /** Session display title (new sessions only). */
150
189
  title?: string;
151
190
  /**
@@ -497,6 +536,13 @@ export interface SlackChannelOptions {
497
536
  engagement?: SlackEngagementOptions;
498
537
  onAppMention?: SlackMessageHandler;
499
538
  onDirectMessage?: SlackMessageHandler;
539
+ /**
540
+ * Handler for Socket Mode `reaction_added` events on messages. The Slack
541
+ * app must subscribe to `reaction_added` and have `reactions:read`.
542
+ * The handler receives a synthetic message whose thread timestamp is the
543
+ * reacted-to message and whose {@link SlackMessage.reaction} names the emoji.
544
+ */
545
+ onReactionAdded?: SlackMessageHandler;
500
546
  /**
501
547
  * Handler for watched channel posts (same contract as `onAppMention`:
502
548
  * return `null` to skip, or a {@link SlackDispatchDecision} to dispatch).
package/src/index.ts CHANGED
@@ -91,6 +91,13 @@ export {
91
91
  UnknownSessionError,
92
92
  UnknownToolError,
93
93
  } from "./internal/session-engine.js";
94
+ export { createMemoryKv, memoryCasTable } from "./kv/memory-kv.js";
95
+ export {
96
+ LedgerCasExhaustedError,
97
+ LedgerFencedError,
98
+ SessionLedger,
99
+ supportsSessionLedgerKv,
100
+ } from "./ledger.js";
94
101
  export type { MemoryBackend, TurnMemoryRecord } from "./memory.js";
95
102
  export {
96
103
  agentStoreMemoryBackend,
@@ -116,6 +123,7 @@ export { defineSchedule } from "./schedules.js";
116
123
  export { defineSkill } from "./skills.js";
117
124
  export type {
118
125
  StorageABTable,
126
+ StorageCasTable,
119
127
  StorageConfig,
120
128
  StorageContext,
121
129
  StorageDefinition,
@@ -140,6 +148,7 @@ export {
140
148
  isSymbolicConnectionTransport,
141
149
  MAX_RUNNING_TURNS_LIMIT,
142
150
  normalizeCursorAccountServers,
151
+ supportsKvCas,
143
152
  } from "./types.js";
144
153
 
145
154
  /**
@@ -46,6 +46,7 @@ import {
46
46
  initNextCommands,
47
47
  initProject,
48
48
  installScaffoldDependencies,
49
+ linkCliOntoPath,
49
50
  printInitGuide,
50
51
  } from "./init-project.js";
51
52
  import { offerInstallCursorSkills } from "./install-cursor-skills.js";
@@ -297,6 +298,10 @@ export async function cmdInit(options: AxCliOptions): Promise<number> {
297
298
  const install = await installScaffoldDependencies(result.rootDir, {
298
299
  stdio: "pipe",
299
300
  });
301
+ const linked = install.ok
302
+ ? await linkCliOntoPath(result.rootDir, { stdio: "pipe" })
303
+ : undefined;
304
+ const cliOnPath = linked?.ok === true;
300
305
  process.stdout.write(
301
306
  `${JSON.stringify(
302
307
  {
@@ -308,10 +313,17 @@ export async function cmdInit(options: AxCliOptions): Promise<number> {
308
313
  installError: install.ok
309
314
  ? null
310
315
  : install.stderr.trim() || `npm install exited ${install.exitCode}`,
316
+ cliOnPath,
317
+ cliLinkError:
318
+ install.ok && linked !== undefined && !linked.ok
319
+ ? linked.stderr.trim() ||
320
+ `npm install -g exited ${linked.exitCode}`
321
+ : null,
311
322
  next: initNextCommands({
312
323
  rootDir: result.rootDir,
313
324
  cwd,
314
325
  signedIn,
326
+ cliOnPath,
315
327
  }),
316
328
  },
317
329
  null,
@@ -333,12 +345,23 @@ export async function cmdInit(options: AxCliOptions): Promise<number> {
333
345
  });
334
346
  if (!install.ok) {
335
347
  process.stderr.write(
336
- `\nnpm install failed (exit ${install.exitCode}). Fix the install, then run ${CLI_COMMAND_NAME} dev.\n`
348
+ `\nnpm install failed (exit ${install.exitCode}). Fix the install, then retry init.\n`
337
349
  );
338
350
  return install.exitCode || 1;
339
351
  }
340
352
  process.stdout.write("\n");
341
353
 
354
+ process.stdout.write(`Installing ${CLI_COMMAND_NAME} on your PATH\n\n`);
355
+ const linked = await linkCliOntoPath(result.rootDir, { stdio: "inherit" });
356
+ const cliOnPath = linked.ok;
357
+ if (!cliOnPath) {
358
+ process.stderr.write(
359
+ `\nCould not put ${CLI_COMMAND_NAME} on PATH (${linked.stderr.trim() || `exit ${linked.exitCode}`}). Next steps include a PATH export for node_modules/.bin.\n\n`
360
+ );
361
+ } else {
362
+ process.stdout.write("\n");
363
+ }
364
+
342
365
  // Personal Cursor skills so coding agents can pick up create-agent / debug / …
343
366
  try {
344
367
  await offerInstallCursorSkills();
@@ -362,6 +385,7 @@ export async function cmdInit(options: AxCliOptions): Promise<number> {
362
385
  rootDir: result.rootDir,
363
386
  cwd,
364
387
  signedIn,
388
+ cliOnPath,
365
389
  })
366
390
  );
367
391
  return loginExit;