@cursor/july 0.1.16 → 0.1.18

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 (273) hide show
  1. package/README.md +9 -4
  2. package/dist/bin/agent-serve.js +60 -2
  3. package/dist/channels/slack/api.d.ts.map +1 -1
  4. package/dist/channels/slack/api.js +13 -3
  5. package/dist/channels/slack/auth.d.ts +7 -1
  6. package/dist/channels/slack/auth.d.ts.map +1 -1
  7. package/dist/channels/slack/auth.js +14 -0
  8. package/dist/channels/slack/blocks.d.ts +61 -0
  9. package/dist/channels/slack/blocks.d.ts.map +1 -0
  10. package/dist/channels/slack/blocks.js +90 -0
  11. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  12. package/dist/channels/slack/cursor-account.js +1 -1
  13. package/dist/channels/slack/defaults.d.ts +2 -0
  14. package/dist/channels/slack/defaults.d.ts.map +1 -1
  15. package/dist/channels/slack/defaults.js +29 -3
  16. package/dist/channels/slack/dispatch.d.ts +3 -17
  17. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  18. package/dist/channels/slack/dispatch.js +4 -1
  19. package/dist/channels/slack/inbound.d.ts.map +1 -1
  20. package/dist/channels/slack/inbound.js +6 -1
  21. package/dist/channels/slack/index.d.ts +2 -0
  22. package/dist/channels/slack/index.d.ts.map +1 -1
  23. package/dist/channels/slack/index.js +2 -0
  24. package/dist/channels/slack/init.d.ts.map +1 -1
  25. package/dist/channels/slack/init.js +4 -1
  26. package/dist/channels/slack/interactive.d.ts +39 -3
  27. package/dist/channels/slack/interactive.d.ts.map +1 -1
  28. package/dist/channels/slack/interactive.js +153 -7
  29. package/dist/channels/slack/manifest.d.ts +13 -0
  30. package/dist/channels/slack/manifest.d.ts.map +1 -1
  31. package/dist/channels/slack/manifest.js +36 -14
  32. package/dist/channels/slack/message-body.d.ts +13 -0
  33. package/dist/channels/slack/message-body.d.ts.map +1 -0
  34. package/dist/channels/slack/message-body.js +82 -0
  35. package/dist/channels/slack/setup.d.ts +4 -1
  36. package/dist/channels/slack/setup.d.ts.map +1 -1
  37. package/dist/channels/slack/setup.js +16 -2
  38. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  39. package/dist/channels/slack/slack-channel.js +24 -3
  40. package/dist/channels/slack/thread-context.d.ts +0 -2
  41. package/dist/channels/slack/thread-context.d.ts.map +1 -1
  42. package/dist/channels/slack/thread-context.js +1 -65
  43. package/dist/channels/slack/types.d.ts +93 -1
  44. package/dist/channels/slack/types.d.ts.map +1 -1
  45. package/dist/connections.d.ts +4 -1
  46. package/dist/connections.d.ts.map +1 -1
  47. package/dist/connections.js +4 -1
  48. package/dist/docs/404.html +2 -2
  49. package/dist/docs/ab.html +3 -3
  50. package/dist/docs/assets/{app.DjHC-Ggf.js → app.BR5EdVSq.js} +1 -1
  51. package/dist/docs/assets/chunks/@localSearchIndexroot.BOzkeUi8.js +1 -0
  52. package/dist/docs/assets/chunks/{VPLocalSearchBox.DwviwmIq.js → VPLocalSearchBox.CNCzfUKC.js} +1 -1
  53. package/dist/docs/assets/chunks/{theme.BIpeYSon.js → theme.CwKvMSWv.js} +2 -2
  54. package/dist/docs/assets/{example-agents_security-reviewer.md.BZSNycMf.js → example-agents_security-reviewer.md.Dhj_m7_B.js} +1 -1
  55. package/dist/docs/assets/{guides_mcp-oauth.md.DqmEOQY1.js → guides_mcp-oauth.md.Dd8EgSem.js} +3 -2
  56. package/dist/docs/assets/{guides_mcp-oauth.md.DqmEOQY1.lean.js → guides_mcp-oauth.md.Dd8EgSem.lean.js} +1 -1
  57. package/dist/docs/assets/{guides_slack.md.FWale6Ip.js → guides_slack.md.D_xFKlnF.js} +1 -1
  58. package/dist/docs/assets/{reference_connections.md.CsSr3rDz.js → reference_connections.md.B9Q3TOve.js} +12 -5
  59. package/dist/docs/assets/{reference_connections.md.CsSr3rDz.lean.js → reference_connections.md.B9Q3TOve.lean.js} +1 -1
  60. package/dist/docs/building-with-agents.html +3 -3
  61. package/dist/docs/concepts.html +3 -3
  62. package/dist/docs/deployment.html +3 -3
  63. package/dist/docs/evals.html +3 -3
  64. package/dist/docs/example-agents/approval-buddy.html +3 -3
  65. package/dist/docs/example-agents/benny.html +3 -3
  66. package/dist/docs/example-agents/bugbot.html +3 -3
  67. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  68. package/dist/docs/example-agents/codeowners-review.html +3 -3
  69. package/dist/docs/example-agents/concierge.html +3 -3
  70. package/dist/docs/example-agents/fsd.html +3 -3
  71. package/dist/docs/example-agents/index.html +3 -3
  72. package/dist/docs/example-agents/knowledge-base.html +3 -3
  73. package/dist/docs/example-agents/oncall.html +3 -3
  74. package/dist/docs/example-agents/security-reviewer.html +5 -5
  75. package/dist/docs/example-agents/slack-agent.html +3 -3
  76. package/dist/docs/example-agents/weather-agent.html +3 -3
  77. package/dist/docs/guides/agent-to-agent.html +3 -3
  78. package/dist/docs/guides/cloud-runtime.html +3 -3
  79. package/dist/docs/guides/github.html +3 -3
  80. package/dist/docs/guides/human-in-the-loop.html +3 -3
  81. package/dist/docs/guides/mcp-oauth.html +6 -5
  82. package/dist/docs/guides/slack.html +5 -5
  83. package/dist/docs/guides/webhooks.html +3 -3
  84. package/dist/docs/hashmap.json +1 -1
  85. package/dist/docs/hillclimbing.html +3 -3
  86. package/dist/docs/index.html +3 -3
  87. package/dist/docs/quickstart.html +3 -3
  88. package/dist/docs/reference/agent-config.html +3 -3
  89. package/dist/docs/reference/channels.html +3 -3
  90. package/dist/docs/reference/cli.html +3 -3
  91. package/dist/docs/reference/connections.html +14 -7
  92. package/dist/docs/reference/hooks.html +3 -3
  93. package/dist/docs/reference/http-api.html +3 -3
  94. package/dist/docs/reference/instructions.html +3 -3
  95. package/dist/docs/reference/playground.html +3 -3
  96. package/dist/docs/reference/project-layout.html +3 -3
  97. package/dist/docs/reference/prompt.html +3 -3
  98. package/dist/docs/reference/schedules.html +3 -3
  99. package/dist/docs/reference/sessions.html +3 -3
  100. package/dist/docs/reference/skills.html +3 -3
  101. package/dist/docs/reference/subagents.html +3 -3
  102. package/dist/docs/reference/tools.html +3 -3
  103. package/dist/docs/scaffolding-agents.html +3 -3
  104. package/dist/docs/storage.html +3 -3
  105. package/dist/docs/troubleshooting.html +3 -3
  106. package/dist/evals/assertions.d.ts +190 -0
  107. package/dist/evals/assertions.d.ts.map +1 -0
  108. package/dist/evals/assertions.js +461 -0
  109. package/dist/evals/expect.d.ts +95 -0
  110. package/dist/evals/expect.d.ts.map +1 -0
  111. package/dist/evals/expect.js +194 -0
  112. package/dist/evals/judge.d.ts +133 -0
  113. package/dist/evals/judge.d.ts.map +1 -0
  114. package/dist/evals/judge.js +228 -0
  115. package/dist/evals/loaders.d.ts +51 -0
  116. package/dist/evals/loaders.d.ts.map +1 -0
  117. package/dist/evals/loaders.js +124 -0
  118. package/dist/evals/matchers.d.ts +95 -0
  119. package/dist/evals/matchers.d.ts.map +1 -0
  120. package/dist/evals/matchers.js +145 -0
  121. package/dist/evals/reporters.d.ts +70 -0
  122. package/dist/evals/reporters.d.ts.map +1 -0
  123. package/dist/evals/reporters.js +221 -0
  124. package/dist/evals/results.d.ts +103 -0
  125. package/dist/evals/results.d.ts.map +1 -0
  126. package/dist/evals/results.js +28 -0
  127. package/dist/evals/run-facts.d.ts +77 -0
  128. package/dist/evals/run-facts.d.ts.map +1 -0
  129. package/dist/evals/run-facts.js +187 -0
  130. package/dist/evals.d.ts +118 -31
  131. package/dist/evals.d.ts.map +1 -1
  132. package/dist/evals.js +46 -12
  133. package/dist/index.d.ts +1 -1
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +1 -1
  136. package/dist/internal/builtin-tools/index.d.ts +10 -0
  137. package/dist/internal/builtin-tools/index.d.ts.map +1 -0
  138. package/dist/internal/builtin-tools/index.js +27 -0
  139. package/dist/internal/builtin-tools/reminders.d.ts +9 -0
  140. package/dist/internal/builtin-tools/reminders.d.ts.map +1 -0
  141. package/dist/internal/builtin-tools/reminders.js +138 -0
  142. package/dist/internal/cli-ax.d.ts +27 -0
  143. package/dist/internal/cli-ax.d.ts.map +1 -1
  144. package/dist/internal/cli-ax.js +90 -12
  145. package/dist/internal/cli-mcp.d.ts +8 -0
  146. package/dist/internal/cli-mcp.d.ts.map +1 -1
  147. package/dist/internal/cli-mcp.js +98 -0
  148. package/dist/internal/cli-slack.d.ts +29 -1
  149. package/dist/internal/cli-slack.d.ts.map +1 -1
  150. package/dist/internal/cli-slack.js +799 -13
  151. package/dist/internal/cursor-event-relay.d.ts +30 -0
  152. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  153. package/dist/internal/cursor-event-relay.js +37 -0
  154. package/dist/internal/deploy-client.d.ts +7 -0
  155. package/dist/internal/deploy-client.d.ts.map +1 -1
  156. package/dist/internal/deploy-client.js +9 -0
  157. package/dist/internal/discovery.d.ts.map +1 -1
  158. package/dist/internal/discovery.js +70 -9
  159. package/dist/internal/distribution.d.ts.map +1 -1
  160. package/dist/internal/distribution.js +2 -0
  161. package/dist/internal/eval-judge-model.d.ts +59 -0
  162. package/dist/internal/eval-judge-model.d.ts.map +1 -0
  163. package/dist/internal/eval-judge-model.js +131 -0
  164. package/dist/internal/eval-run-store.d.ts.map +1 -1
  165. package/dist/internal/eval-run-store.js +12 -20
  166. package/dist/internal/eval-runner.d.ts +31 -25
  167. package/dist/internal/eval-runner.d.ts.map +1 -1
  168. package/dist/internal/eval-runner.js +329 -126
  169. package/dist/internal/evals-client.d.ts.map +1 -1
  170. package/dist/internal/evals-client.js +4 -1
  171. package/dist/internal/local-env.d.ts +8 -0
  172. package/dist/internal/local-env.d.ts.map +1 -0
  173. package/dist/internal/local-env.js +47 -0
  174. package/dist/internal/playground/static.d.ts +3 -0
  175. package/dist/internal/playground/static.d.ts.map +1 -1
  176. package/dist/internal/playground/static.js +3 -0
  177. package/dist/internal/reminder-runner.d.ts +7 -0
  178. package/dist/internal/reminder-runner.d.ts.map +1 -1
  179. package/dist/internal/reminder-runner.js +21 -0
  180. package/dist/internal/run-client.d.ts +15 -0
  181. package/dist/internal/run-client.d.ts.map +1 -1
  182. package/dist/internal/run-client.js +3 -2
  183. package/dist/internal/sdk-runner.d.ts +51 -1
  184. package/dist/internal/sdk-runner.d.ts.map +1 -1
  185. package/dist/internal/sdk-runner.js +176 -36
  186. package/dist/internal/server.d.ts.map +1 -1
  187. package/dist/internal/server.js +11 -1
  188. package/dist/internal/session-engine.d.ts +18 -5
  189. package/dist/internal/session-engine.d.ts.map +1 -1
  190. package/dist/internal/session-engine.js +81 -19
  191. package/dist/internal/slack-provision-client.d.ts +133 -0
  192. package/dist/internal/slack-provision-client.d.ts.map +1 -0
  193. package/dist/internal/slack-provision-client.js +286 -0
  194. package/dist/playground/assets/cursor-icons-16-f_W_ogc-.woff2 +0 -0
  195. package/dist/playground/assets/index-DRjS5klQ.js +85 -0
  196. package/dist/playground/assets/index-TCDs-UhZ.css +1 -0
  197. package/dist/playground/index.html +2 -2
  198. package/dist/types.d.ts +98 -4
  199. package/dist/types.d.ts.map +1 -1
  200. package/dist/types.js +26 -0
  201. package/docs/example-agents/security-reviewer.md +1 -1
  202. package/docs/guides/mcp-oauth.md +10 -5
  203. package/docs/guides/slack.md +3 -2
  204. package/docs/reference/connections.md +28 -18
  205. package/package.json +20 -2
  206. package/skills/create-agent/SKILL.md +15 -7
  207. package/skills/debug/SKILL.md +3 -1
  208. package/skills/mcp-auth/SKILL.md +1 -1
  209. package/skills/setup-slack/SKILL.md +177 -11
  210. package/src/bin/agent-serve.ts +63 -2
  211. package/src/channels/slack/api.ts +16 -3
  212. package/src/channels/slack/auth.ts +16 -1
  213. package/src/channels/slack/blocks.ts +139 -0
  214. package/src/channels/slack/cursor-account.ts +1 -0
  215. package/src/channels/slack/defaults.ts +32 -4
  216. package/src/channels/slack/dispatch.ts +6 -19
  217. package/src/channels/slack/inbound.ts +6 -1
  218. package/src/channels/slack/index.ts +2 -0
  219. package/src/channels/slack/init.ts +3 -0
  220. package/src/channels/slack/interactive.ts +184 -19
  221. package/src/channels/slack/manifest.ts +37 -6
  222. package/src/channels/slack/message-body.ts +90 -0
  223. package/src/channels/slack/setup.ts +16 -2
  224. package/src/channels/slack/slack-channel.ts +37 -6
  225. package/src/channels/slack/thread-context.ts +1 -69
  226. package/src/channels/slack/types.ts +105 -1
  227. package/src/connections.ts +4 -1
  228. package/src/evals/assertions.ts +704 -0
  229. package/src/evals/expect.ts +272 -0
  230. package/src/evals/judge.ts +346 -0
  231. package/src/evals/loaders.ts +113 -0
  232. package/src/evals/matchers.ts +246 -0
  233. package/src/evals/reporters.ts +274 -0
  234. package/src/evals/results.ts +132 -0
  235. package/src/evals/run-facts.ts +274 -0
  236. package/src/evals.ts +205 -42
  237. package/src/index.ts +2 -0
  238. package/src/internal/builtin-tools/index.ts +40 -0
  239. package/src/internal/builtin-tools/reminders.ts +158 -0
  240. package/src/internal/cli-ax.ts +134 -15
  241. package/src/internal/cli-mcp.ts +118 -5
  242. package/src/internal/cli-slack.ts +1017 -15
  243. package/src/internal/cursor-event-relay.ts +67 -0
  244. package/src/internal/deploy-client.ts +10 -0
  245. package/src/internal/discovery.ts +83 -5
  246. package/src/internal/distribution.ts +2 -0
  247. package/src/internal/eval-judge-model.ts +148 -0
  248. package/src/internal/eval-run-store.ts +10 -14
  249. package/src/internal/eval-runner.ts +460 -214
  250. package/src/internal/evals-client.ts +5 -0
  251. package/src/internal/local-env.ts +53 -0
  252. package/src/internal/playground/static.ts +3 -0
  253. package/src/internal/reminder-runner.ts +30 -0
  254. package/src/internal/run-client.ts +17 -1
  255. package/src/internal/sdk-runner.ts +234 -35
  256. package/src/internal/server.ts +12 -0
  257. package/src/internal/session-engine.ts +103 -21
  258. package/src/internal/slack-provision-client.ts +442 -0
  259. package/src/types.ts +125 -4
  260. package/dist/channels/slack/post-update-delivery.d.ts +0 -85
  261. package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
  262. package/dist/docs/assets/chunks/@localSearchIndexroot.DZtu0ZIc.js +0 -1
  263. package/dist/internal/json-dir-store.d.ts +0 -32
  264. package/dist/internal/json-dir-store.d.ts.map +0 -1
  265. package/dist/internal/persistence-coordinator.d.ts +0 -127
  266. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  267. package/dist/persistence.d.ts +0 -184
  268. package/dist/persistence.d.ts.map +0 -1
  269. package/dist/playground/assets/cursor-icons-16-CQ50JpfO.woff2 +0 -0
  270. package/dist/playground/assets/index-Bwl4Onx4.js +0 -85
  271. package/dist/playground/assets/index-ByV4nfch.css +0 -1
  272. /package/dist/docs/assets/{example-agents_security-reviewer.md.BZSNycMf.lean.js → example-agents_security-reviewer.md.Dhj_m7_B.lean.js} +0 -0
  273. /package/dist/docs/assets/{guides_slack.md.FWale6Ip.lean.js → guides_slack.md.D_xFKlnF.lean.js} +0 -0
@@ -25,6 +25,7 @@ import {
25
25
  } from "./thread-context.js";
26
26
  import type {
27
27
  SlackChannelState,
28
+ SlackContinuationProbes,
28
29
  SlackCredentials,
29
30
  SlackDispatchDecision,
30
31
  SlackMessage,
@@ -55,7 +56,7 @@ export function emptySlackChannelState(
55
56
  };
56
57
  }
57
58
 
58
- export interface SlackInboundDispatchDeps {
59
+ export interface SlackInboundDispatchDeps extends SlackContinuationProbes {
59
60
  send: SendMessageFn;
60
61
  waitUntil: (promise: Promise<unknown>) => void;
61
62
  onAppMention: SlackMessageHandler;
@@ -70,22 +71,6 @@ export interface SlackInboundDispatchDeps {
70
71
  directMessagesEnabled?: boolean;
71
72
  /** Opt-in channel watch; absent = channel posts are ignored. */
72
73
  channelWatch?: ChannelPostWatcher;
73
- /**
74
- * When set, full-thread hydration runs only for cold-start turns (no
75
- * existing session). Warm turns load a `<slack_thread_delta>` instead.
76
- */
77
- hasContinuationSession?: (continuationToken: string) => Promise<boolean>;
78
- /** True when a turn is already running for this Slack continuation. */
79
- isContinuationBusy?: (continuationToken: string) => Promise<boolean>;
80
- /**
81
- * Warm-delta watermark from session channel state (`lastBotMessageTs`).
82
- * When absent, delta falls back to the last matching bot post in-thread.
83
- */
84
- getContinuationLastBotMessageTs?: (
85
- continuationToken: string
86
- ) => Promise<string | undefined>;
87
- /** Cancel the active turn + clear coalesced nudges for this continuation. */
88
- interruptContinuation?: (continuationToken: string) => Promise<boolean>;
89
74
  /**
90
75
  * When false, skip the built-in `eval` directive. Defaults to true when
91
76
  * `host.evals` is present.
@@ -106,12 +91,14 @@ function triggeringMergeMessage(
106
91
  ...(message.author?.botId === undefined
107
92
  ? {}
108
93
  : { bot_id: message.author.botId }),
109
- text: message.text,
94
+ // Flattened body, so this stand-in matches what the real API record
95
+ // reduces to and is not dropped as empty by the thread-context filters.
96
+ text: message.body,
110
97
  };
111
98
  }
112
99
 
113
100
  export async function dispatchInboundMessage(args: {
114
- kind: "app_mention" | "direct_message" | "channel_post";
101
+ kind: "app_mention" | "direct_message" | "channel_post" | "block_action";
115
102
  message: SlackMessage;
116
103
  handler: SlackMessageHandler;
117
104
  send: SendMessageFn;
@@ -2,6 +2,7 @@
2
2
  * Parse Slack Events API payloads into structured inbound messages.
3
3
  */
4
4
 
5
+ import { slackApiMessageBody } from "./message-body.js";
5
6
  import type { SlackAuthor, SlackMessage } from "./types.js";
6
7
 
7
8
  type JsonRecord = Record<string, unknown>;
@@ -62,11 +63,15 @@ function buildSlackMessage(
62
63
  return null;
63
64
  }
64
65
  const text = asString(event.text) ?? "";
66
+ // Alert feeds post an empty top-level `text` with the notification in
67
+ // `attachments` / `blocks`, so `body` (not `text`) is the message content.
68
+ const body = slackApiMessageBody(event);
65
69
  const threadTs = asString(event.thread_ts) ?? ts;
66
70
  const author = parseAuthor(event);
67
71
  return {
68
72
  text,
69
- markdown: slackTextToMarkdown(text),
73
+ body,
74
+ markdown: slackTextToMarkdown(body),
70
75
  ts,
71
76
  threadTs,
72
77
  channelId,
@@ -22,6 +22,7 @@ export * from "./api.js";
22
22
  export * from "./approvals.js";
23
23
  export * from "./assistant.js";
24
24
  export * from "./auth.js";
25
+ export * from "./blocks.js";
25
26
  export * from "./channel-watch.js";
26
27
  export * from "./constants.js";
27
28
  export * from "./credentials.js";
@@ -37,6 +38,7 @@ export * from "./interactive.js";
37
38
  export * from "./links.js";
38
39
  export * from "./log.js";
39
40
  export * from "./manifest.js";
41
+ export * from "./message-body.js";
40
42
  export * from "./nudge-carrier.js";
41
43
  export * from "./nudge-store.js";
42
44
  export * from "./pr-ref.js";
@@ -81,6 +81,9 @@ ${envPrefixLine} // Streaming and thinking steps default to true.
81
81
 
82
82
  const pair = buildSlackManifestPair({
83
83
  name,
84
+ // Scaffolded for hand-import at api.slack.com; managed_app_settings is
85
+ // settable only with the manager app's user token.
86
+ managedByCursor: false,
84
87
  ...(options.description === undefined
85
88
  ? {}
86
89
  : { description: options.description }),
@@ -1,17 +1,33 @@
1
1
  /**
2
- * Socket Mode interactive payloads (Block Kit Approve/Deny).
2
+ * Socket Mode interactive payloads (Block Kit clicks).
3
+ *
4
+ * Built-in tool-approval Approve/Deny actions resolve parked calls
5
+ * ({@link dispatchInteractiveFromPayload}); everything else can be routed
6
+ * to an author handler ({@link dispatchBlockAction} via
7
+ * `slackChannel({ onBlockAction })`).
3
8
  */
4
9
 
5
10
  import type {
6
11
  ApprovalDecision,
7
12
  AuthContext,
8
13
  ChannelSession,
14
+ HostContext,
9
15
  PendingApproval,
16
+ SendMessageFn,
10
17
  } from "../../types.js";
18
+ import { updateMessage } from "./api.js";
11
19
  import { decisionFromActionId, parseApprovalActionValue } from "./approvals.js";
12
20
  import { buildSlackInteractiveApprovalAuth } from "./auth.js";
21
+ import { dispatchInboundMessage } from "./dispatch.js";
13
22
  import { defaultSlackLog, type SlackLog } from "./log.js";
14
- import type { SlackCredentials } from "./types.js";
23
+ import type {
24
+ SlackBlockAction,
25
+ SlackBlockActionHandler,
26
+ SlackBotToken,
27
+ SlackContinuationProbes,
28
+ SlackCredentials,
29
+ SlackMessage,
30
+ } from "./types.js";
15
31
 
16
32
  export interface SlackInteractiveDispatchDeps {
17
33
  credentials: SlackCredentials | undefined;
@@ -35,7 +51,7 @@ export async function dispatchInteractiveFromPayload(
35
51
  deps: SlackInteractiveDispatchDeps
36
52
  ): Promise<boolean> {
37
53
  const log = deps.log ?? defaultSlackLog;
38
- const parsed = parseBlockActionsPayload(payload);
54
+ const parsed = parseSlackBlockAction(payload);
39
55
  if (parsed === null) {
40
56
  return false;
41
57
  }
@@ -124,16 +140,14 @@ export async function dispatchInteractiveFromPayload(
124
140
  return true;
125
141
  }
126
142
 
127
- interface ParsedBlockAction {
128
- actionId: string;
129
- value: string;
130
- channelId: string;
131
- threadTs: string;
132
- userId: string;
133
- messageTs: string;
134
- }
135
-
136
- function parseBlockActionsPayload(payload: unknown): ParsedBlockAction | null {
143
+ /**
144
+ * Parse a Socket Mode `block_actions` payload into a {@link SlackBlockAction}.
145
+ * Returns null for non-`block_actions` payloads (view submissions, shortcuts)
146
+ * and malformed bodies.
147
+ */
148
+ export function parseSlackBlockAction(
149
+ payload: unknown
150
+ ): SlackBlockAction | null {
137
151
  if (typeof payload !== "object" || payload === null) {
138
152
  return null;
139
153
  }
@@ -150,10 +164,7 @@ function parseBlockActionsPayload(payload: unknown): ParsedBlockAction | null {
150
164
  return null;
151
165
  }
152
166
  const action = first as Record<string, unknown>;
153
- if (
154
- typeof action.action_id !== "string" ||
155
- typeof action.value !== "string"
156
- ) {
167
+ if (typeof action.action_id !== "string") {
157
168
  return null;
158
169
  }
159
170
 
@@ -169,6 +180,10 @@ function parseBlockActionsPayload(payload: unknown): ParsedBlockAction | null {
169
180
  typeof record.user === "object" && record.user !== null
170
181
  ? (record.user as Record<string, unknown>)
171
182
  : {};
183
+ const team =
184
+ typeof record.team === "object" && record.team !== null
185
+ ? (record.team as Record<string, unknown>)
186
+ : {};
172
187
 
173
188
  const channelId = typeof channel.id === "string" ? channel.id : "";
174
189
  const messageTs = typeof message.ts === "string" ? message.ts : "";
@@ -182,13 +197,163 @@ function parseBlockActionsPayload(payload: unknown): ParsedBlockAction | null {
182
197
  ? (record.container as { thread_ts: string }).thread_ts
183
198
  : messageTs;
184
199
  const userId = typeof user.id === "string" ? user.id : "";
200
+ const userName =
201
+ typeof user.username === "string"
202
+ ? user.username
203
+ : typeof user.name === "string"
204
+ ? user.name
205
+ : undefined;
206
+ const teamId =
207
+ typeof team.id === "string"
208
+ ? team.id
209
+ : typeof user.team_id === "string"
210
+ ? user.team_id
211
+ : undefined;
212
+ const label =
213
+ typeof action.text === "object" &&
214
+ action.text !== null &&
215
+ typeof (action.text as { text?: unknown }).text === "string"
216
+ ? (action.text as { text: string }).text
217
+ : undefined;
218
+ const actionTs = typeof action.action_ts === "string" ? action.action_ts : "";
219
+ const messageBlocks = Array.isArray(message.blocks)
220
+ ? message.blocks
221
+ : undefined;
185
222
 
186
223
  return {
187
224
  actionId: action.action_id,
188
- value: action.value,
225
+ blockId: typeof action.block_id === "string" ? action.block_id : "",
226
+ value: typeof action.value === "string" ? action.value : "",
227
+ label,
189
228
  channelId,
190
229
  threadTs,
191
- userId,
192
230
  messageTs,
231
+ messageBlocks,
232
+ userId,
233
+ userName,
234
+ teamId,
235
+ actionTs,
193
236
  };
194
237
  }
238
+
239
+ /**
240
+ * Swap the clicked message's actions row for a context note (`chat.update`).
241
+ * Strips only the row the click came from (`action.blockId`); every other
242
+ * block survives. Soft: false when the payload carried no message blocks or
243
+ * the update failed — callers treat the dispatched turn as the durable
244
+ * record and the card as best-effort UI.
245
+ */
246
+ export async function resolveBlockActionCard(args: {
247
+ botToken: SlackBotToken | undefined;
248
+ action: SlackBlockAction;
249
+ note: string;
250
+ }): Promise<boolean> {
251
+ const { action, note } = args;
252
+ if (action.messageBlocks === undefined) {
253
+ return false;
254
+ }
255
+ const clicked = (block: unknown): boolean =>
256
+ typeof block === "object" &&
257
+ block !== null &&
258
+ (block as { type?: unknown }).type === "actions" &&
259
+ (block as { block_id?: unknown }).block_id === action.blockId;
260
+ const result = await updateMessage({
261
+ botToken: args.botToken,
262
+ channel: action.channelId,
263
+ ts: action.messageTs,
264
+ text: note,
265
+ blocks: [
266
+ ...action.messageBlocks.filter((block) => !clicked(block)),
267
+ { type: "context", elements: [{ type: "mrkdwn", text: note }] },
268
+ ],
269
+ });
270
+ return result.ok;
271
+ }
272
+
273
+ export interface SlackBlockActionDispatchDeps extends SlackContinuationProbes {
274
+ handler: SlackBlockActionHandler;
275
+ send: SendMessageFn;
276
+ credentials: SlackCredentials | undefined;
277
+ host: HostContext;
278
+ log?: SlackLog;
279
+ }
280
+
281
+ /**
282
+ * The click as an inbound-message stand-in: the synthetic ts is the click's
283
+ * `action_ts` (unique per click, never a real thread message), so ask-dedupe
284
+ * claims and `<slack_thread>` exclusions stay per-click.
285
+ */
286
+ function blockActionAsMessage(action: SlackBlockAction): SlackMessage {
287
+ const text = `[clicked: ${action.label ?? action.actionId}]`;
288
+ return {
289
+ text,
290
+ body: text,
291
+ markdown: text,
292
+ ts: action.actionTs,
293
+ threadTs: action.threadTs,
294
+ channelId: action.channelId,
295
+ teamId: action.teamId,
296
+ author: {
297
+ userId: action.userId,
298
+ userName: action.userName,
299
+ isBot: false,
300
+ },
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Route a non-approval `block_actions` payload to the author handler and
306
+ * dispatch its decision as a turn on the click's thread. Returns true when
307
+ * the payload was a block action (dispatched or deliberately skipped).
308
+ */
309
+ export async function dispatchBlockAction(
310
+ payload: unknown,
311
+ deps: SlackBlockActionDispatchDeps
312
+ ): Promise<boolean> {
313
+ const log = deps.log ?? defaultSlackLog;
314
+ const action = parseSlackBlockAction(payload);
315
+ if (action === null) {
316
+ return false;
317
+ }
318
+ if (action.channelId === "" || action.threadTs === "") {
319
+ log.warn("block action skipped (no conversation)", {
320
+ action_id: action.actionId,
321
+ user: action.userId,
322
+ });
323
+ return true;
324
+ }
325
+ // Slack stamps every real click; without one the synthetic message ts
326
+ // would alias another click's dedupe claim.
327
+ if (action.actionTs === "") {
328
+ log.warn("block action skipped (no action_ts)", {
329
+ action_id: action.actionId,
330
+ user: action.userId,
331
+ });
332
+ return true;
333
+ }
334
+
335
+ log.info("block action", {
336
+ action_id: action.actionId,
337
+ channel: action.channelId,
338
+ thread_ts: action.threadTs,
339
+ message_ts: action.messageTs,
340
+ user: action.userId,
341
+ });
342
+
343
+ await dispatchInboundMessage({
344
+ kind: "block_action",
345
+ message: blockActionAsMessage(action),
346
+ handler: (ctx) => deps.handler(ctx, action),
347
+ send: deps.send,
348
+ credentials: deps.credentials,
349
+ host: deps.host,
350
+ log,
351
+ // Click text is synthetic — never treat it as a stop/eval directive.
352
+ evalCommandsEnabled: false,
353
+ hasContinuationSession: deps.hasContinuationSession,
354
+ isContinuationBusy: deps.isContinuationBusy,
355
+ getContinuationLastBotMessageTs: deps.getContinuationLastBotMessageTs,
356
+ interruptContinuation: deps.interruptContinuation,
357
+ });
358
+ return true;
359
+ }
@@ -3,10 +3,21 @@
3
3
  *
4
4
  * Import at https://api.slack.com/apps -> Create New App -> From a manifest.
5
5
  * Always Socket Mode — no Events Request URL.
6
+ *
7
+ * Mirrored by backend/server/src/agentServe/agentServeSlackManifest.ts (the
8
+ * backend cannot import this published package); keep the twins in lockstep.
6
9
  */
7
10
 
8
11
  import type { SlackManifestOptions } from "./types.js";
9
12
 
13
+ /**
14
+ * Where Slack points users who open a Cursor-provisioned app's settings
15
+ * page. Manifest builders know neither the team slug nor the deployment
16
+ * id, so this is the dashboard tab root rather than a per-agent deep link.
17
+ */
18
+ export const SLACK_EXTERNAL_APP_MANAGEMENT_URL =
19
+ "https://cursor.com/dashboard/deployed-agents";
20
+
10
21
  export interface SlackAppManifest {
11
22
  display_information: {
12
23
  name: string;
@@ -43,6 +54,10 @@ export interface SlackAppManifest {
43
54
  socket_mode_enabled: boolean;
44
55
  token_rotation_enabled: boolean;
45
56
  is_mcp_enabled: boolean;
57
+ managed_app_settings?: {
58
+ is_install_from_slack_disabled: boolean;
59
+ external_app_management_url: string;
60
+ };
46
61
  };
47
62
  }
48
63
 
@@ -64,6 +79,17 @@ const BOT_SCOPES = [
64
79
 
65
80
  const BOT_EVENTS = ["app_mention", "message.im"] as const;
66
81
 
82
+ /** Slack's display_information.description bound. */
83
+ const MAX_DESCRIPTION_LENGTH = 140;
84
+ const DESCRIPTION_SUFFIX = " — an AI agent built with Cursor";
85
+
86
+ /** Marketplace review rejects managed apps whose manifest omits a description. */
87
+ function defaultDescription(name: string): string {
88
+ const room = MAX_DESCRIPTION_LENGTH - DESCRIPTION_SUFFIX.length;
89
+ // Spread splits by code point: truncation cannot halve a surrogate pair.
90
+ return `${[...name].slice(0, room).join("")}${DESCRIPTION_SUFFIX}`;
91
+ }
92
+
67
93
  function appSlug(name: string): string {
68
94
  const base = name
69
95
  .toLowerCase()
@@ -104,6 +130,7 @@ export function buildSlackManifest(
104
130
  ? `${options.name} (dev)`
105
131
  : options.name;
106
132
  const botName = options.botDisplayName ?? toBotDisplayName(options.name, env);
133
+ const description = options.description ?? defaultDescription(options.name);
107
134
  const includeAssistant = options.assistant !== false;
108
135
 
109
136
  const botScopes = includeAssistant
@@ -121,9 +148,7 @@ export function buildSlackManifest(
121
148
  return {
122
149
  display_information: {
123
150
  name: displayName,
124
- ...(options.description === undefined
125
- ? {}
126
- : { description: options.description }),
151
+ description,
127
152
  },
128
153
  features: {
129
154
  bot_user: {
@@ -138,9 +163,7 @@ export function buildSlackManifest(
138
163
  ...(includeAssistant
139
164
  ? {
140
165
  assistant_view: {
141
- assistant_description:
142
- options.description ??
143
- `${options.name} agent served by agent-serve`,
166
+ assistant_description: description,
144
167
  suggested_prompts: suggested,
145
168
  },
146
169
  }
@@ -165,6 +188,14 @@ export function buildSlackManifest(
165
188
  socket_mode_enabled: true,
166
189
  token_rotation_enabled: false,
167
190
  is_mcp_enabled: false,
191
+ ...(options.managedByCursor === false
192
+ ? {}
193
+ : {
194
+ managed_app_settings: {
195
+ is_install_from_slack_disabled: true,
196
+ external_app_management_url: SLACK_EXTERNAL_APP_MANAGEMENT_URL,
197
+ },
198
+ }),
168
199
  },
169
200
  };
170
201
  }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Flatten a Slack message payload into readable text.
3
+ *
4
+ * Works on both raw Events API payloads and `conversations.*` API records.
5
+ * Alert feeds (Datadog, PagerDuty, …) post with an empty top-level `text`
6
+ * and put the whole notification in `attachments` / Block Kit `blocks`, so
7
+ * anything reading `text` alone sees an empty message.
8
+ */
9
+
10
+ type JsonRecord = Record<string, unknown>;
11
+
12
+ function asRecord(value: unknown): JsonRecord | null {
13
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
14
+ return null;
15
+ }
16
+ return value as JsonRecord;
17
+ }
18
+
19
+ function asString(value: unknown): string | undefined {
20
+ return typeof value === "string" ? value : undefined;
21
+ }
22
+
23
+ /** Pull readable text out of Slack attachment / Block Kit payloads. */
24
+ export function slackApiMessageBody(message: JsonRecord): string {
25
+ const parts: string[] = [];
26
+ const text = asString(message.text);
27
+ if (text !== undefined && text.trim() !== "") {
28
+ parts.push(text);
29
+ }
30
+
31
+ const attachments = Array.isArray(message.attachments)
32
+ ? message.attachments
33
+ : [];
34
+ for (const attachment of attachments) {
35
+ const rec = asRecord(attachment);
36
+ if (rec === null) {
37
+ continue;
38
+ }
39
+ for (const key of [
40
+ "pretext",
41
+ "title",
42
+ "text",
43
+ "fallback",
44
+ "footer",
45
+ ] as const) {
46
+ const value = asString(rec[key]);
47
+ if (value !== undefined && value.trim() !== "") {
48
+ parts.push(value);
49
+ }
50
+ }
51
+ const titleLink = asString(rec.title_link);
52
+ if (titleLink !== undefined && titleLink.trim() !== "") {
53
+ parts.push(titleLink);
54
+ }
55
+ }
56
+
57
+ const blocks = Array.isArray(message.blocks) ? message.blocks : [];
58
+ for (const block of blocks) {
59
+ const rec = asRecord(block);
60
+ if (rec === null) {
61
+ continue;
62
+ }
63
+ const blockText = asRecord(rec.text);
64
+ const blockTextValue = asString(blockText?.text);
65
+ if (blockTextValue !== undefined && blockTextValue.trim() !== "") {
66
+ parts.push(blockTextValue);
67
+ }
68
+ const fields = Array.isArray(rec.fields) ? rec.fields : [];
69
+ for (const field of fields) {
70
+ const fieldRec = asRecord(field);
71
+ const fieldText = asString(fieldRec?.text);
72
+ if (fieldText !== undefined && fieldText.trim() !== "") {
73
+ parts.push(fieldText);
74
+ }
75
+ }
76
+ }
77
+
78
+ // Dedupe identical chunks (Datadog often repeats text/fallback across fields).
79
+ const seen = new Set<string>();
80
+ const deduped: string[] = [];
81
+ for (const part of parts) {
82
+ const trimmed = part.trim();
83
+ if (trimmed === "" || seen.has(trimmed)) {
84
+ continue;
85
+ }
86
+ seen.add(trimmed);
87
+ deduped.push(trimmed);
88
+ }
89
+ return deduped.join("\n");
90
+ }
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * Slack setup guide printed by `agent-serve slack setup`.
3
3
  *
4
- * Mirrors the Cursor skill at skills/setup-slack/SKILL.md.
4
+ * Mirrors the Cursor skill at skills/setup-slack/SKILL.md: connection
5
+ * decision first (Cursor-hosted vs dedicated Socket Mode app), then the
6
+ * dedicated-app phases. {@link SLACK_SETUP_PHASES} stays the dedicated-app
7
+ * checklist — its ids key `.agent-serve/slack/setup-status.json`.
5
8
  */
6
9
 
7
10
  export interface SlackSetupPhase {
@@ -75,8 +78,19 @@ export const SLACK_SETUP_PHASES: SlackSetupPhase[] = [
75
78
 
76
79
  export function formatSlackSetupGuide(): string {
77
80
  const lines: string[] = [
78
- "agent-serve — Slack setup (Socket Mode)",
81
+ "agent-serve — Slack setup",
79
82
  "",
83
+ "Two connections exist; decide before running the phases below.",
84
+ "",
85
+ " A) Cursor-hosted connection (recommended default, zero Slack admin):",
86
+ ' slackChannel({ cursorAccount: true, agentName: "MyAgent" })',
87
+ " Sign in (agent-serve login / CURSOR_API_KEY), serve, then mention",
88
+ ' "@Cursor MyAgent ..." in the linked workspace. No app, no tokens.',
89
+ " B) Dedicated Slack app (Socket Mode) — the phases below. Choose it",
90
+ " when you need your own bot user, channel watching",
91
+ " (engagement.channelPosts), or approval buttons / interactivity.",
92
+ "",
93
+ "Phases 0-5 cover connection B only.",
80
94
  "Goal: importable dev + prod Slack apps over Socket Mode (no public URL).",
81
95
  "App-Level Token (xapp-...) + Bot Token (xoxb-...) required.",
82
96
  "",
@@ -45,7 +45,10 @@ import {
45
45
  type SlackInboundDispatchDeps,
46
46
  } from "./dispatch.js";
47
47
  import { createEventIdDedupe } from "./event-id-dedupe.js";
48
- import { dispatchInteractiveFromPayload } from "./interactive.js";
48
+ import {
49
+ dispatchBlockAction,
50
+ dispatchInteractiveFromPayload,
51
+ } from "./interactive.js";
49
52
  import { createSlackLogger, type SlackLog } from "./log.js";
50
53
  import {
51
54
  type SlackSocketModeHandle,
@@ -120,6 +123,11 @@ export function slackChannel<const Name extends string = string>(
120
123
  "slackChannel({ cursorAccount: true }) does not support interactivity — interactive Block Kit clicks are not relayed over the Cursor Slack connection. Interactivity is Socket Mode only."
121
124
  );
122
125
  }
126
+ if (options.onBlockAction !== undefined) {
127
+ throw new Error(
128
+ "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."
129
+ );
130
+ }
123
131
  }
124
132
  const accountTransport = cursorAccount
125
133
  ? createPendingSlackApiTransport()
@@ -151,7 +159,9 @@ export function slackChannel<const Name extends string = string>(
151
159
  const eventIds = createEventIdDedupe();
152
160
  const toolApprovals = options.toolApprovals === true;
153
161
  const interactivity = options.interactivity === true;
154
- const routeInteractive = toolApprovals || interactivity;
162
+ const onBlockAction = options.onBlockAction;
163
+ const routeInteractive =
164
+ toolApprovals || interactivity || onBlockAction !== undefined;
155
165
 
156
166
  const runtime: { log: SlackLog } = {
157
167
  log: createSlackLogger(options.logger),
@@ -408,12 +418,33 @@ export function slackChannel<const Name extends string = string>(
408
418
  };
409
419
  if (routeInteractive) {
410
420
  socketOptions.onInteractive = async (payload) => {
411
- await dispatchInteractiveFromPayload(payload, {
421
+ // Framework tool-approval Approve/Deny clicks resolve first;
422
+ // anything else (including approval-shaped clicks whose value
423
+ // fails to parse) falls through to the author's block-action hook.
424
+ const handledAsApproval = await dispatchInteractiveFromPayload(
425
+ payload,
426
+ {
427
+ credentials,
428
+ getSession: args.getSession,
429
+ resolveApproval: args.resolveApproval,
430
+ waitUntil: args.waitUntil,
431
+ log: getLog(),
432
+ }
433
+ );
434
+ if (handledAsApproval || onBlockAction === undefined) {
435
+ return;
436
+ }
437
+ await dispatchBlockAction(payload, {
438
+ handler: onBlockAction,
439
+ send: args.send,
412
440
  credentials,
413
- getSession: args.getSession,
414
- resolveApproval: args.resolveApproval,
415
- waitUntil: args.waitUntil,
441
+ host: args.host,
416
442
  log: getLog(),
443
+ hasContinuationSession: args.hasContinuationSession,
444
+ isContinuationBusy: args.isContinuationBusy,
445
+ getContinuationLastBotMessageTs:
446
+ args.getContinuationLastBotMessageTs,
447
+ interruptContinuation: args.interruptContinuation,
417
448
  });
418
449
  };
419
450
  }