@cursor/july 0.1.114 → 0.2.1

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 (206) hide show
  1. package/dist/channels/slack/channel-watch.d.ts +19 -3
  2. package/dist/channels/slack/channel-watch.d.ts.map +1 -1
  3. package/dist/channels/slack/channel-watch.js +48 -9
  4. package/dist/channels/slack/inbound.d.ts +7 -0
  5. package/dist/channels/slack/inbound.d.ts.map +1 -1
  6. package/dist/channels/slack/inbound.js +23 -0
  7. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  8. package/dist/channels/slack/slack-channel.js +87 -8
  9. package/dist/channels/slack/types.d.ts +11 -12
  10. package/dist/channels/slack/types.d.ts.map +1 -1
  11. package/dist/docs/404.html +2 -2
  12. package/dist/docs/assets/{app.BqkJwOZ-.js → app.D23Y-7Tp.js} +4 -4
  13. package/dist/docs/assets/chunks/@localSearchIndexroot.CzCCM7N8.js +1 -0
  14. package/dist/docs/assets/chunks/{VPLocalSearchBox.BJAi2KiV.js → VPLocalSearchBox.CWBeTFRZ.js} +1 -1
  15. package/dist/docs/assets/chunks/{arc.BZpXTgvV.js → arc.DSF2O3pm.js} +1 -1
  16. package/dist/docs/assets/chunks/{architectureDiagram-Q4EWVU46.WYI-7F-Y.js → architectureDiagram-Q4EWVU46.J52Wzbkg.js} +1 -1
  17. package/dist/docs/assets/chunks/{baseUniq.CZaUPpg0.js → baseUniq.CQS3LPCt.js} +1 -1
  18. package/dist/docs/assets/chunks/{blockDiagram-DXYQGD6D.D6UES2pD.js → blockDiagram-DXYQGD6D.Dw339Gr5.js} +1 -1
  19. package/dist/docs/assets/chunks/{c4Diagram-AHTNJAMY.cwebIe4i.js → c4Diagram-AHTNJAMY.BUdtOaRZ.js} +1 -1
  20. package/dist/docs/assets/chunks/channel.Bfu4df88.js +1 -0
  21. package/dist/docs/assets/chunks/{chunk-4BX2VUAB.fVyFnjxg.js → chunk-4BX2VUAB.CuOrkEqk.js} +1 -1
  22. package/dist/docs/assets/chunks/{chunk-4TB4RGXK.BanufG1c.js → chunk-4TB4RGXK.BJNBcY7U.js} +1 -1
  23. package/dist/docs/assets/chunks/{chunk-55IACEB6.VaSMz5-2.js → chunk-55IACEB6.VJK5LAm_.js} +1 -1
  24. package/dist/docs/assets/chunks/{chunk-EDXVE4YY.CN2diZOM.js → chunk-EDXVE4YY.BYYLihvj.js} +1 -1
  25. package/dist/docs/assets/chunks/{chunk-FMBD7UC4.g4ivypu3.js → chunk-FMBD7UC4.CmoW8BXP.js} +1 -1
  26. package/dist/docs/assets/chunks/{chunk-OYMX7WX6.GZXKn9JJ.js → chunk-OYMX7WX6.DTGY4C-M.js} +1 -1
  27. package/dist/docs/assets/chunks/{chunk-QZHKN3VN.itXxJZCd.js → chunk-QZHKN3VN.Cg5n67vl.js} +1 -1
  28. package/dist/docs/assets/chunks/{chunk-YZCP3GAM.-rw2GfvX.js → chunk-YZCP3GAM.C3GR_ia5.js} +1 -1
  29. package/dist/docs/assets/chunks/classDiagram-6PBFFD2Q.DdfgtaWs.js +1 -0
  30. package/dist/docs/assets/chunks/classDiagram-v2-HSJHXN6E.DdfgtaWs.js +1 -0
  31. package/dist/docs/assets/chunks/clone.rkmfti6d.js +1 -0
  32. package/dist/docs/assets/chunks/{cose-bilkent-S5V4N54A.CmaI5br0.js → cose-bilkent-S5V4N54A.BTRG8N3b.js} +1 -1
  33. package/dist/docs/assets/chunks/{dagre-KV5264BT.4wY9S4Kt.js → dagre-KV5264BT.Bob_bp_p.js} +1 -1
  34. package/dist/docs/assets/chunks/{diagram-5BDNPKRD.Pc3c0u9W.js → diagram-5BDNPKRD.ggPcs9uO.js} +1 -1
  35. package/dist/docs/assets/chunks/{diagram-G4DWMVQ6.CYrWz-nj.js → diagram-G4DWMVQ6.BP0qyJkp.js} +1 -1
  36. package/dist/docs/assets/chunks/{diagram-MMDJMWI5.Bgj5hukb.js → diagram-MMDJMWI5.B0X24UKr.js} +1 -1
  37. package/dist/docs/assets/chunks/{diagram-TYMM5635.DGMEXalS.js → diagram-TYMM5635.B4rXHFVt.js} +1 -1
  38. package/dist/docs/assets/chunks/{erDiagram-SMLLAGMA.GepTV9Im.js → erDiagram-SMLLAGMA._55Rt9oX.js} +1 -1
  39. package/dist/docs/assets/chunks/{flowDiagram-DWJPFMVM.DVKywg3j.js → flowDiagram-DWJPFMVM.DGP4XvR5.js} +1 -1
  40. package/dist/docs/assets/chunks/{ganttDiagram-T4ZO3ILL.C7qt9Mlo.js → ganttDiagram-T4ZO3ILL.BtXtkL4E.js} +1 -1
  41. package/dist/docs/assets/chunks/{gitGraphDiagram-UUTBAWPF.U30_r82P.js → gitGraphDiagram-UUTBAWPF.B9cPWblK.js} +1 -1
  42. package/dist/docs/assets/chunks/{graph.CyyMyAWv.js → graph.D8HzNexS.js} +1 -1
  43. package/dist/docs/assets/chunks/{infoDiagram-42DDH7IO.Dn9ACW3y.js → infoDiagram-42DDH7IO.Bw7CQUpi.js} +1 -1
  44. package/dist/docs/assets/chunks/{ishikawaDiagram-UXIWVN3A.DlIdIGOA.js → ishikawaDiagram-UXIWVN3A.MwkzF6nQ.js} +1 -1
  45. package/dist/docs/assets/chunks/{journeyDiagram-VCZTEJTY.DZj4vy4E.js → journeyDiagram-VCZTEJTY.DIGFF-3C.js} +1 -1
  46. package/dist/docs/assets/chunks/{kanban-definition-6JOO6SKY.Dl63eMUV.js → kanban-definition-6JOO6SKY.DhYef2BN.js} +1 -1
  47. package/dist/docs/assets/chunks/{layout.BLHZLWPH.js → layout.C0XUxuPi.js} +1 -1
  48. package/dist/docs/assets/chunks/{linear.aXKGKaNw.js → linear.BwNPpZex.js} +1 -1
  49. package/dist/docs/assets/chunks/{min.zWnFcpcc.js → min.CwAQdL7z.js} +1 -1
  50. package/dist/docs/assets/chunks/{mindmap-definition-QFDTVHPH.Qs4MQBea.js → mindmap-definition-QFDTVHPH.pWsSVLsP.js} +1 -1
  51. package/dist/docs/assets/chunks/{pieDiagram-DEJITSTG.BmPHgsk7.js → pieDiagram-DEJITSTG.BDJ3FbBy.js} +1 -1
  52. package/dist/docs/assets/chunks/{quadrantDiagram-34T5L4WZ.D5MQ3gwA.js → quadrantDiagram-34T5L4WZ.Co80izyB.js} +1 -1
  53. package/dist/docs/assets/chunks/{requirementDiagram-MS252O5E.CkdUFrO7.js → requirementDiagram-MS252O5E.JveKw4yx.js} +1 -1
  54. package/dist/docs/assets/chunks/{sankeyDiagram-XADWPNL6.KZrljrAV.js → sankeyDiagram-XADWPNL6.B0A7adPi.js} +1 -1
  55. package/dist/docs/assets/chunks/{sequenceDiagram-FGHM5R23.XMoEW-Lx.js → sequenceDiagram-FGHM5R23.d6JZ5Hre.js} +1 -1
  56. package/dist/docs/assets/chunks/{stateDiagram-FHFEXIEX.BmTzePLj.js → stateDiagram-FHFEXIEX.DWnL0NQl.js} +1 -1
  57. package/dist/docs/assets/chunks/stateDiagram-v2-QKLJ7IA2.ZEetPk0E.js +1 -0
  58. package/dist/docs/assets/chunks/{theme.BfQzpxsg.js → theme.MJTLx0hh.js} +2 -2
  59. package/dist/docs/assets/chunks/{timeline-definition-GMOUNBTQ.Dug0oamp.js → timeline-definition-GMOUNBTQ.CFS7Ai4c.js} +1 -1
  60. package/dist/docs/assets/chunks/{vennDiagram-DHZGUBPP.BOTHrEFu.js → vennDiagram-DHZGUBPP.CwSlnjCf.js} +1 -1
  61. package/dist/docs/assets/chunks/wardley-RL74JXVD.3gurI8YA.js +162 -0
  62. package/dist/docs/assets/chunks/{wardleyDiagram-NUSXRM2D.CoXKdfi6.js → wardleyDiagram-NUSXRM2D.B_8mvtjh.js} +1 -1
  63. package/dist/docs/assets/chunks/{xychartDiagram-5P7HB3ND.DXoSCjAW.js → xychartDiagram-5P7HB3ND.DtjU5H85.js} +1 -1
  64. package/dist/docs/assets/{guides_slack.md.Bo96y42E.js → guides_slack.md.Bjw2r2gL.js} +5 -5
  65. package/dist/docs/assets/{guides_slack.md.Bo96y42E.lean.js → guides_slack.md.Bjw2r2gL.lean.js} +1 -1
  66. package/dist/docs/assets/reference_cli.md.BvnQM8wd.js +97 -0
  67. package/dist/docs/assets/reference_cli.md.BvnQM8wd.lean.js +1 -0
  68. package/dist/docs/building-with-agents.html +35 -35
  69. package/dist/docs/deployment.html +35 -35
  70. package/dist/docs/evals.html +35 -35
  71. package/dist/docs/guides/agent-to-agent.html +35 -35
  72. package/dist/docs/guides/bitbucket.html +35 -35
  73. package/dist/docs/guides/cloud-agents.html +35 -35
  74. package/dist/docs/guides/convert-automation.html +35 -35
  75. package/dist/docs/guides/github.html +35 -35
  76. package/dist/docs/guides/gitlab.html +35 -35
  77. package/dist/docs/guides/grokbot-agents.html +35 -35
  78. package/dist/docs/guides/hooks.html +35 -35
  79. package/dist/docs/guides/improve.html +35 -35
  80. package/dist/docs/guides/jev.html +35 -35
  81. package/dist/docs/guides/mcp-oauth.html +35 -35
  82. package/dist/docs/guides/opentelemetry.html +35 -35
  83. package/dist/docs/guides/slack.html +39 -39
  84. package/dist/docs/guides/slack.md +16 -1
  85. package/dist/docs/guides/webhooks.html +35 -35
  86. package/dist/docs/hashmap.json +1 -1
  87. package/dist/docs/hillclimbing.html +35 -35
  88. package/dist/docs/index.html +35 -35
  89. package/dist/docs/llms-full.txt +699 -752
  90. package/dist/docs/llms.txt +1 -1
  91. package/dist/docs/quickstart.html +35 -35
  92. package/dist/docs/reference/agent-config.html +35 -35
  93. package/dist/docs/reference/artifacts.html +35 -35
  94. package/dist/docs/reference/channels.html +35 -35
  95. package/dist/docs/reference/cli.html +127 -125
  96. package/dist/docs/reference/cli.md +685 -753
  97. package/dist/docs/reference/connections.html +35 -35
  98. package/dist/docs/reference/evals.html +35 -35
  99. package/dist/docs/reference/extensions.html +35 -35
  100. package/dist/docs/reference/hooks.html +35 -35
  101. package/dist/docs/reference/http-api.html +35 -35
  102. package/dist/docs/reference/instructions.html +35 -35
  103. package/dist/docs/reference/playground.html +35 -35
  104. package/dist/docs/reference/project-layout.html +35 -35
  105. package/dist/docs/reference/prompt.html +35 -35
  106. package/dist/docs/reference/schedules.html +35 -35
  107. package/dist/docs/reference/sessions.html +35 -35
  108. package/dist/docs/reference/skills.html +35 -35
  109. package/dist/docs/reference/subagents.html +35 -35
  110. package/dist/docs/reference/tools.html +35 -35
  111. package/dist/docs/templates/agentic-owners.html +35 -35
  112. package/dist/docs/templates/pr-autofixer.html +35 -35
  113. package/dist/docs/templates/security-reviewer.html +35 -35
  114. package/dist/docs/templates/thermo-quality-review.html +35 -35
  115. package/dist/docs/templates/thermo-review.html +35 -35
  116. package/dist/docs/templates/triage.html +35 -35
  117. package/dist/docs/troubleshooting.html +35 -35
  118. package/dist/files-backends/cursor-hosted.d.ts +11 -17
  119. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  120. package/dist/files-backends/cursor-hosted.js +13 -41
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/internal/artifacts-store.d.ts +11 -0
  124. package/dist/internal/artifacts-store.d.ts.map +1 -1
  125. package/dist/internal/artifacts-store.js +113 -18
  126. package/dist/internal/cli-deploy.d.ts.map +1 -1
  127. package/dist/internal/cli-deploy.js +9 -1
  128. package/dist/internal/cursor/cursor-api-transport.d.ts +37 -0
  129. package/dist/internal/cursor/cursor-api-transport.d.ts.map +1 -0
  130. package/dist/internal/cursor/cursor-api-transport.js +44 -0
  131. package/dist/internal/cursor/hosted-store-secrets.d.ts +21 -0
  132. package/dist/internal/cursor/hosted-store-secrets.d.ts.map +1 -1
  133. package/dist/internal/cursor/hosted-store-secrets.js +26 -0
  134. package/dist/internal/cursor/store-api-client.d.ts +82 -0
  135. package/dist/internal/cursor/store-api-client.d.ts.map +1 -0
  136. package/dist/internal/cursor/store-api-client.js +227 -0
  137. package/dist/internal/deploy-client.d.ts +6 -0
  138. package/dist/internal/deploy-client.d.ts.map +1 -1
  139. package/dist/internal/deploy-client.js +3 -0
  140. package/dist/internal/deploy-manifest.d.ts +8 -0
  141. package/dist/internal/deploy-manifest.d.ts.map +1 -1
  142. package/dist/internal/deploy-manifest.js +7 -1
  143. package/dist/internal/discovery/agent-config.d.ts +3 -1
  144. package/dist/internal/discovery/agent-config.d.ts.map +1 -1
  145. package/dist/internal/discovery/agent-config.js +7 -4
  146. package/dist/internal/framework-storage-selection.d.ts +1 -1
  147. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  148. package/dist/internal/platform-timers.d.ts.map +1 -1
  149. package/dist/internal/platform-timers.js +20 -2
  150. package/dist/internal/reminder-runner.d.ts +79 -1
  151. package/dist/internal/reminder-runner.d.ts.map +1 -1
  152. package/dist/internal/reminder-runner.js +287 -46
  153. package/dist/internal/server.d.ts.map +1 -1
  154. package/dist/internal/server.js +7 -0
  155. package/dist/internal/session-engine.d.ts.map +1 -1
  156. package/dist/internal/session-engine.js +13 -15
  157. package/dist/internal/store-api-protocol.d.ts +134 -0
  158. package/dist/internal/store-api-protocol.d.ts.map +1 -0
  159. package/dist/internal/store-api-protocol.js +126 -0
  160. package/dist/memory.d.ts +49 -10
  161. package/dist/memory.d.ts.map +1 -1
  162. package/dist/memory.js +193 -50
  163. package/dist/playground/assets/{index-CrMWlgUU.js → index-Cs0MKsv4.js} +30 -30
  164. package/dist/playground/assets/index-DLwnR9ys.css +1 -0
  165. package/dist/playground/index.html +2 -2
  166. package/dist/reminders.d.ts +1 -1
  167. package/dist/reminders.d.ts.map +1 -1
  168. package/dist/types.d.ts +44 -10
  169. package/dist/types.d.ts.map +1 -1
  170. package/docs/guides/slack.md +16 -1
  171. package/docs/reference/cli.md +686 -754
  172. package/package.json +1 -1
  173. package/skills/setup-slack/SKILL.md +1 -1
  174. package/src/channels/slack/channel-watch.ts +57 -8
  175. package/src/channels/slack/inbound.ts +24 -0
  176. package/src/channels/slack/slack-channel.ts +127 -4
  177. package/src/channels/slack/types.ts +11 -12
  178. package/src/files-backends/cursor-hosted.ts +31 -68
  179. package/src/index.ts +1 -0
  180. package/src/internal/artifacts-store.ts +131 -25
  181. package/src/internal/cli-deploy.ts +12 -1
  182. package/src/internal/cursor/cursor-api-transport.ts +73 -0
  183. package/src/internal/cursor/hosted-store-secrets.ts +35 -0
  184. package/src/internal/cursor/store-api-client.ts +360 -0
  185. package/src/internal/deploy-client.ts +9 -0
  186. package/src/internal/deploy-manifest.ts +15 -0
  187. package/src/internal/discovery/agent-config.ts +8 -6
  188. package/src/internal/framework-storage-selection.ts +1 -1
  189. package/src/internal/platform-timers.ts +24 -2
  190. package/src/internal/reminder-runner.ts +454 -66
  191. package/src/internal/server.ts +8 -0
  192. package/src/internal/session-engine.ts +17 -22
  193. package/src/internal/store-api-protocol.ts +222 -0
  194. package/src/memory.ts +240 -59
  195. package/src/reminders.ts +1 -0
  196. package/src/types.ts +47 -10
  197. package/dist/docs/assets/chunks/@localSearchIndexroot.BnSgidYE.js +0 -1
  198. package/dist/docs/assets/chunks/channel.DdM5EfNW.js +0 -1
  199. package/dist/docs/assets/chunks/classDiagram-6PBFFD2Q.CjfGHeg2.js +0 -1
  200. package/dist/docs/assets/chunks/classDiagram-v2-HSJHXN6E.CjfGHeg2.js +0 -1
  201. package/dist/docs/assets/chunks/clone.wSOICb_f.js +0 -1
  202. package/dist/docs/assets/chunks/stateDiagram-v2-QKLJ7IA2.Cu5X28zZ.js +0 -1
  203. package/dist/docs/assets/chunks/wardley-RL74JXVD.DXy2i1LS.js +0 -162
  204. package/dist/docs/assets/reference_cli.md.DLWDz9ij.js +0 -95
  205. package/dist/docs/assets/reference_cli.md.DLWDz9ij.lean.js +0 -1
  206. package/dist/playground/assets/index-C61EWMBK.css +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cursor/july",
3
- "version": "0.1.114",
3
+ "version": "0.2.1",
4
4
  "description": "(early alpha) Filesystem-first framework for defining Cursor agents as markdown and TypeScript and serving them over channels with the Cursor SDK.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "repository": {
@@ -123,7 +123,7 @@ export default slackChannel({
123
123
  envPrefix: "JENNY",
124
124
  engagement: {
125
125
  channelPosts: {
126
- allow: ["#alerts"], // ["*"] for every joined channel
126
+ allow: ["C0123ABCDEF", "#alerts"], // ≥1 channel id required, ≤10 ids
127
127
  posts: "top-level",
128
128
  },
129
129
  },
@@ -2,7 +2,8 @@
2
2
  * Opt-in channel watch: dispatch on posts in allowlisted channels.
3
3
  *
4
4
  * Filtering rules (the anti-slop guarantees):
5
- * - Only configured channels are watched; `"*"` opts into every channel.
5
+ * - Only configured channels are watched. The allowlist is finite: watching
6
+ * every channel the bot is in is not expressible.
6
7
  * - Top-level posts only by default; thread replies never dispatch unless
7
8
  * `posts: "all"` is configured.
8
9
  * - Posts that @mention the bot are skipped here (the `app_mention` path
@@ -21,21 +22,69 @@ import type { SlackChannelPostsOptions, SlackMessage } from "./types.js";
21
22
 
22
23
  const CHANNEL_ID_PATTERN = /^[CG][A-Z0-9]+$/;
23
24
 
24
- /** Allowlist over channel ids and `#names` (names resolve lazily), or `*`. */
25
+ /**
26
+ * Channel-watch allowlist contract, mirrored by the control plane's
27
+ * deploy-time validation (proto posture: both sides validate
28
+ * independently). Ids are required because on Cursor-managed hosting they
29
+ * become channel-scoped subscription edges — the control plane drops
30
+ * channel posts outside the id set before any compute wakes, and it cannot
31
+ * resolve `#names`.
32
+ */
33
+ export const CHANNEL_POSTS_MAX_CHANNEL_IDS = 10;
34
+
35
+ /** The channel-id (`C…`/`G…`) entries of an allowlist, deduped. */
36
+ export function channelPostIdEntries(entries: readonly string[]): string[] {
37
+ const ids = new Set<string>();
38
+ for (const raw of entries) {
39
+ const entry = raw.trim();
40
+ if (CHANNEL_ID_PATTERN.test(entry)) {
41
+ ids.add(entry);
42
+ }
43
+ }
44
+ return [...ids];
45
+ }
46
+
47
+ /**
48
+ * Validate `channelPosts.allow` at definition time. Returns an actionable
49
+ * error string (to be prefixed with the option path), or undefined.
50
+ */
51
+ export function validateChannelPostsAllow(
52
+ entries: readonly string[]
53
+ ): string | undefined {
54
+ if (entries.some(entry => entry.trim() === "*")) {
55
+ return (
56
+ 'no longer supports "*": list the channels to watch by id (C…/G…), ' +
57
+ `up to ${CHANNEL_POSTS_MAX_CHANNEL_IDS} channels.`
58
+ );
59
+ }
60
+ const ids = channelPostIdEntries(entries);
61
+ if (ids.length === 0) {
62
+ return (
63
+ "must include at least one channel id (C…/G…). Ids define the " +
64
+ "watched set on Cursor-managed hosting; `#name` entries alone are " +
65
+ "not enough."
66
+ );
67
+ }
68
+ if (ids.length > CHANNEL_POSTS_MAX_CHANNEL_IDS) {
69
+ return `lists ${ids.length} channel ids; the maximum is ${CHANNEL_POSTS_MAX_CHANNEL_IDS}.`;
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ /** Allowlist over channel ids and `#names` (names resolve lazily). */
25
75
  export class SlackChannelAllowlist {
26
76
  private readonly ids = new Set<string>();
27
77
  private readonly names = new Set<string>();
28
- private all = false;
29
78
 
30
79
  constructor(entries: readonly string[]) {
31
80
  for (const raw of entries) {
32
81
  const entry = raw.trim();
33
- if (entry === "") {
82
+ // "*" is rejected at definition time (validateChannelPostsAllow);
83
+ // ignore it here so a stale config degrades to its explicit entries.
84
+ if (entry === "" || entry === "*") {
34
85
  continue;
35
86
  }
36
- if (entry === "*") {
37
- this.all = true;
38
- } else if (entry.startsWith("#")) {
87
+ if (entry.startsWith("#")) {
39
88
  this.names.add(entry.slice(1).toLowerCase());
40
89
  } else if (CHANNEL_ID_PATTERN.test(entry)) {
41
90
  this.ids.add(entry);
@@ -47,7 +96,7 @@ export class SlackChannelAllowlist {
47
96
  }
48
97
 
49
98
  matches(channelId: string): boolean {
50
- return this.all || this.ids.has(channelId);
99
+ return this.ids.has(channelId);
51
100
  }
52
101
 
53
102
  unresolvedNames(): string[] {
@@ -218,6 +218,30 @@ export function parseAssistantThreadStarted(
218
218
  };
219
219
  }
220
220
 
221
+ /**
222
+ * The receiving app's own bot user id from an Events API envelope. Slack
223
+ * stamps `authorizations[]` on every `event_callback` with the installed
224
+ * bot user the delivery is for, so a hosted guest learns its identity from
225
+ * the verified body without an `auth.test` round trip.
226
+ */
227
+ export function parseEventBodyBotUserId(body: unknown): string | undefined {
228
+ const root = asRecord(body);
229
+ if (root === null || root.type !== "event_callback") {
230
+ return undefined;
231
+ }
232
+ if (!Array.isArray(root.authorizations)) {
233
+ return undefined;
234
+ }
235
+ for (const entry of root.authorizations) {
236
+ const authorization = asRecord(entry);
237
+ const userId = asString(authorization?.user_id);
238
+ if (userId !== undefined && userId !== "") {
239
+ return userId;
240
+ }
241
+ }
242
+ return undefined;
243
+ }
244
+
221
245
  /**
222
246
  * Parse DM / IM messages. More generous than a strict channel_type check:
223
247
  * accepts `channel_type === "im"` or channel ids starting with `D`, and
@@ -25,10 +25,12 @@ import {
25
25
  } from "../../internal/continuation-identity.js";
26
26
  import { AGENT_SERVE_GENERATION_ENV_VAR } from "../../internal/cursor-relay-core.js";
27
27
  import { attachHostedAdmissionAdapter } from "../../internal/hosted-admission-adapter.js";
28
+ import type { HostedAdmissionPlatformHandoff } from "../../internal/hosted-delivery-protocol.js";
28
29
  import type {
29
30
  ArtifactsApi,
30
31
  ChannelDefinition,
31
32
  ChannelEventHandlers,
33
+ ChannelHandlerArgs,
32
34
  HostContext,
33
35
  JsonValue,
34
36
  ReceiveInput,
@@ -46,6 +48,7 @@ import {
46
48
  createChannelPostWatcher,
47
49
  resolveChannelNames,
48
50
  SlackChannelAllowlist,
51
+ validateChannelPostsAllow,
49
52
  } from "./channel-watch.js";
50
53
  import {
51
54
  credentialsFromEnv,
@@ -68,6 +71,10 @@ import {
68
71
  slackWakeChannelState,
69
72
  } from "./dispatch.js";
70
73
  import { createEventIdDedupe, type EventIdDedupe } from "./event-id-dedupe.js";
74
+ import {
75
+ parseChannelMessageEvent,
76
+ parseEventBodyBotUserId,
77
+ } from "./inbound.js";
71
78
  import {
72
79
  dispatchBlockAction,
73
80
  dispatchInteractiveFromPayload,
@@ -181,10 +188,10 @@ export function slackChannel(
181
188
  const followUpStatus = options.followUpStatus ?? !silent;
182
189
  const channelPosts = engagement.channelPosts;
183
190
  if (channelPosts !== undefined) {
184
- const entries = channelPosts.allow.filter(entry => entry.trim() !== "");
185
- if (entries.length === 0) {
191
+ const allowError = validateChannelPostsAllow(channelPosts.allow);
192
+ if (allowError !== undefined) {
186
193
  throw new Error(
187
- 'slackChannel engagement.channelPosts.allow must list at least one channel (id, #name, or "*").'
194
+ `slackChannel engagement.channelPosts.allow ${allowError}`
188
195
  );
189
196
  }
190
197
  }
@@ -245,6 +252,113 @@ export function slackChannel(
245
252
  let channelWatch: ChannelPostWatcher | undefined;
246
253
  let botMentionGate: SlackBotMentionGate | undefined;
247
254
 
255
+ // Hosted admission never runs the Socket Mode branch of `onStart`, so the
256
+ // channel watch is built per delivery there. The allowlist and its `#name`
257
+ // resolution are shared across the pod; the bot's identity comes from the
258
+ // verified envelope.
259
+ let hostedAllowlist: SlackChannelAllowlist | undefined;
260
+ let hostedAllowlistReady: Promise<void> | undefined;
261
+
262
+ async function resolveHostedAllowlistNames(
263
+ allowlist: SlackChannelAllowlist
264
+ ): Promise<void> {
265
+ const names = allowlist.unresolvedNames();
266
+ if (names.length === 0) {
267
+ return;
268
+ }
269
+ try {
270
+ const botToken = await tryResolveBotToken(credentials);
271
+ if (botToken === undefined) {
272
+ getLog().error("channel watch setup degraded", {
273
+ error: "no bot token; #name entries stay unresolved",
274
+ });
275
+ return;
276
+ }
277
+ const { WebClient } = await import("@slack/web-api");
278
+ allowlist.applyResolvedNames(
279
+ await resolveChannelNames(new WebClient(botToken), names, getLog())
280
+ );
281
+ } catch (error) {
282
+ getLog().error("channel watch setup degraded", {
283
+ error: error instanceof Error ? error.message : String(error),
284
+ });
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Only a channel post needs the watcher, so mentions and DMs never wait on
290
+ * it, and a channel already matched by id never waits on `#name` resolution.
291
+ */
292
+ async function hostedChannelWatch(
293
+ payload: unknown,
294
+ args: ChannelHandlerArgs,
295
+ platformHandoff: HostedAdmissionPlatformHandoff | undefined
296
+ ): Promise<ChannelPostWatcher | undefined> {
297
+ if (channelPosts === undefined) {
298
+ return undefined;
299
+ }
300
+ const channelEvent = parseChannelMessageEvent(payload, {
301
+ includeBotAuthored: true,
302
+ });
303
+ if (channelEvent === null || channelEvent.kind === "deleted") {
304
+ return undefined;
305
+ }
306
+ hostedAllowlist ??= new SlackChannelAllowlist(channelPosts.allow);
307
+ if (
308
+ !hostedAllowlist.matches(channelEvent.message.channelId) &&
309
+ hostedAllowlist.unresolvedNames().length > 0
310
+ ) {
311
+ hostedAllowlistReady ??= resolveHostedAllowlistNames(hostedAllowlist);
312
+ await hostedAllowlistReady;
313
+ }
314
+ const botUserId = parseEventBodyBotUserId(payload);
315
+ return createChannelPostWatcher({
316
+ // Admission answers before a debounce timer could fire; edits arrive
317
+ // as their own deliveries and the edge drops them.
318
+ options: { ...channelPosts, debounceMs: 0 },
319
+ allowlist: hostedAllowlist,
320
+ getBotUserId: () => botUserId,
321
+ log: getLog(),
322
+ dispatch: (message, eventId) => {
323
+ args.waitUntil(
324
+ dispatchInboundMessage({
325
+ kind: "channel_post",
326
+ message,
327
+ handler: onChannelPost,
328
+ propagateHandlerErrors: true,
329
+ send: args.send,
330
+ credentials,
331
+ host: args.host,
332
+ artifacts: args.artifacts,
333
+ log: getLog(),
334
+ evalCommandsEnabled,
335
+ followUpStatus,
336
+ ack: reply.ack,
337
+ blockExternals,
338
+ continuation: options.continuation,
339
+ ...(args.hasContinuationSession === undefined
340
+ ? {}
341
+ : { hasContinuationSession: args.hasContinuationSession }),
342
+ ...(args.isContinuationBusy === undefined
343
+ ? {}
344
+ : { isContinuationBusy: args.isContinuationBusy }),
345
+ ...(args.getContinuationLastBotMessageTs === undefined
346
+ ? {}
347
+ : {
348
+ getContinuationLastBotMessageTs:
349
+ args.getContinuationLastBotMessageTs,
350
+ }),
351
+ ...(args.interruptContinuation === undefined
352
+ ? {}
353
+ : { interruptContinuation: args.interruptContinuation }),
354
+ ...(eventId === undefined ? {} : { eventId }),
355
+ ...(platformHandoff === undefined ? {} : { platformHandoff }),
356
+ })
357
+ );
358
+ },
359
+ });
360
+ }
361
+
248
362
  function inboundDeps(args: {
249
363
  send: SendMessageFn;
250
364
  waitUntil: (promise: Promise<unknown>) => void;
@@ -260,7 +374,10 @@ export function slackChannel(
260
374
  ) => Promise<string | undefined>;
261
375
  interruptContinuation?: (continuationToken: string) => Promise<boolean>;
262
376
  platformHandoff?: SlackInboundDispatchDeps["platformHandoff"];
377
+ /** Per-delivery watcher (hosted); the Socket Mode watcher otherwise. */
378
+ channelWatch?: ChannelPostWatcher;
263
379
  }): SlackInboundDispatchDeps {
380
+ const watch = args.channelWatch ?? channelWatch;
264
381
  return {
265
382
  send: args.send,
266
383
  continuation: options.continuation,
@@ -299,7 +416,7 @@ export function slackChannel(
299
416
  ...(args.interruptContinuation === undefined
300
417
  ? {}
301
418
  : { interruptContinuation: args.interruptContinuation }),
302
- ...(channelWatch === undefined ? {} : { channelWatch }),
419
+ ...(watch === undefined ? {} : { channelWatch: watch }),
303
420
  ...(botMentionGate === undefined ? {} : { botMentionGate }),
304
421
  suggestedPrompts: options.suggestedPrompts,
305
422
  suggestedPromptsTitle: options.suggestedPromptsTitle,
@@ -661,6 +778,11 @@ export function slackChannel(
661
778
  // would otherwise swallow the control plane's own retry of a delivery
662
779
  // that failed after the id was marked handled.
663
780
  dispatch: async ({ payload, args, platformHandoff }) => {
781
+ const hostedWatch = await hostedChannelWatch(
782
+ payload,
783
+ args,
784
+ platformHandoff
785
+ );
664
786
  const dispatched = dispatchInboundFromEventBody(
665
787
  payload,
666
788
  inboundDeps({
@@ -675,6 +797,7 @@ export function slackChannel(
675
797
  getContinuationLastBotMessageTs: args.getContinuationLastBotMessageTs,
676
798
  interruptContinuation: args.interruptContinuation,
677
799
  ...(platformHandoff === undefined ? {} : { platformHandoff }),
800
+ ...(hostedWatch === undefined ? {} : { channelWatch: hostedWatch }),
678
801
  })
679
802
  );
680
803
  return skipped(dispatched ? "not_engaged" : "not_dispatchable");
@@ -517,28 +517,27 @@ export type SlackBlockActionHandler = (
517
517
 
518
518
  /**
519
519
  * Opt-in channel watch: dispatch on posts in configured channels without an
520
- * @mention. Use `"*"` for every channel the bot has joined.
520
+ * @mention. The watched set is a finite list mentions are global (any
521
+ * channel the bot is in), but listening without a mention names its
522
+ * channels.
521
523
  */
522
524
  export interface SlackChannelPostsOptions {
523
525
  /**
524
- * Channels to watch: ids (`C…` / `G…`), `#names`, or `"*"` for every
525
- * channel the bot is in. Must be non-empty (empty is a definition error).
526
- * `#names` are resolved to ids via `conversations.list` when the channel
527
- * starts; the bot must be a member of each watched channel to receive its
526
+ * Channels to watch. Must include at least one channel id (`C…` / `G…`)
527
+ * and at most 10 distinct ids; `"*"` is a definition error. `#name`
528
+ * entries may accompany ids (resolved via `conversations.list` on Socket
529
+ * Mode), but on Cursor-managed hosting the id entries alone define the
530
+ * watched set: each id becomes a channel-scoped subscription, and the
531
+ * control plane drops channel posts outside that set before any compute
532
+ * wakes. The bot must be a member of each watched channel to receive its
528
533
  * messages.
529
- *
530
- * `"*"` suits agents whose handler decides per message (a thread the agent
531
- * is already in, say) rather than per channel. Every delivered message then
532
- * reaches {@link SlackChannelOptions.onChannelPost}, and each one costs a
533
- * `users.info` external-sender check first unless
534
- * {@link SlackAccessPolicy.blockExternals} is off.
535
534
  */
536
535
  allow: string[];
537
536
  /**
538
537
  * Which posts dispatch. `"top-level"` (default) fires only on new
539
538
  * channel posts — never on replies inside other people's threads, so the
540
539
  * agent can take fresh intake but cannot barge into conversations.
541
- * `"all"` also dispatches thread replies (Socket Mode only).
540
+ * `"all"` also dispatches thread replies in allowlisted channels.
542
541
  */
543
542
  posts?: "top-level" | "all";
544
543
  /**
@@ -57,19 +57,19 @@ import {
57
57
  type FileSinkPutResult,
58
58
  } from "../files.js";
59
59
  import {
60
- AGENT_SERVE_FACTORY_FLUSH_PROTOCOL,
60
+ type CursorApiTransportOptions,
61
+ resolveCursorApiBaseUrl,
62
+ resolveCursorApiFetch,
63
+ resolveCursorApiKey,
64
+ } from "../internal/cursor/cursor-api-transport.js";
65
+ import {
61
66
  resolveDeploymentStoreSourceIdSync,
62
67
  resolveFrameworkStoreSourceIdSync,
63
68
  resolveStoreProtocolSync,
69
+ storeProtocolEnablesFactoryFlush,
64
70
  } from "../internal/cursor/hosted-store-secrets.js";
65
71
  import { createAgentStorePresignedUrlValidatorForBackend } from "./agent-store-presigned-url.js";
66
72
 
67
- /**
68
- * Same default as the hosted storage backend, inlined so this entrypoint does
69
- * not import CLI code.
70
- */
71
- const DEFAULT_CURSOR_EXTERNAL_API_URL = "https://api.cursor.com";
72
-
73
73
  const AGENT_SERVE_FILES_PATH = "/v0/agent-serve-files";
74
74
 
75
75
  /**
@@ -156,18 +156,12 @@ export function isCursorUserStoreFilesAvailable(
156
156
  return key !== undefined && key !== "";
157
157
  }
158
158
 
159
- export interface CursorHostedFilesOptions {
160
- /** Control-plane base URL (default: `CURSOR_API_BASE_URL`, else api.cursor.com). */
161
- baseUrl?: string;
162
- /** Bearer credential (default: `CURSOR_API_KEY`). */
163
- apiKey?: string;
164
- /**
165
- * Lazy credential, re-resolved on every call and preferred over
166
- * {@link apiKey} — hosted freeze snapshots listen unsigned and bind the
167
- * key file after serve start. Kept separate so `apiKey` stays a plain
168
- * string.
169
- */
170
- resolveApiKey?: () => string | undefined;
159
+ /**
160
+ * Base URL / credential / fetch resolution is the shared
161
+ * {@link CursorApiTransportOptions} (`internal/cursor/cursor-api-transport.ts`),
162
+ * so this sink and the domain-lane store API client resolve identically.
163
+ */
164
+ export interface CursorHostedFilesOptions extends CursorApiTransportOptions {
171
165
  /** File store to address (default: `AGENT_SERVE_STORE_SOURCE_ID`). */
172
166
  storeSourceId?: string;
173
167
  /**
@@ -178,13 +172,12 @@ export interface CursorHostedFilesOptions {
178
172
  resolveStoreSourceId?: () => string | undefined;
179
173
  /**
180
174
  * Lazy store protocol pin (default: env, then the task-bound file). When it
181
- * answers {@link AGENT_SERVE_FACTORY_FLUSH_PROTOCOL} and the source id maps
182
- * to a deployment store role, requests ride the factory store API instead
183
- * of `/v0/agent-serve-files`. Any other value keeps `/v0`.
175
+ * enables the factory flush wire (`factory-flush-v1`, or the superset
176
+ * domain-API pin `factory-api-v1`) and the source id maps to a deployment
177
+ * store role, requests ride the factory store API instead of
178
+ * `/v0/agent-serve-files`. Any other value keeps `/v0`.
184
179
  */
185
180
  resolveStoreProtocol?: () => string | undefined;
186
- /** Test seam; defaults to global fetch. */
187
- fetch?: typeof fetch;
188
181
  }
189
182
 
190
183
  /**
@@ -295,7 +288,7 @@ export function cursorHostedFiles(
295
288
  const sourceId = resolveStoreSourceId(options);
296
289
  const protocol =
297
290
  options.resolveStoreProtocol?.() ?? resolveStoreProtocolSync();
298
- if (protocol === AGENT_SERVE_FACTORY_FLUSH_PROTOCOL) {
291
+ if (storeProtocolEnablesFactoryFlush(protocol)) {
299
292
  // The factory API addresses stores by role, so map the pinned source id
300
293
  // back to the role it was pinned from. A source id matching neither (a
301
294
  // local-dev override) has no role there and stays on `/v0`, as does any
@@ -567,7 +560,7 @@ export function cursorHostedFiles(
567
560
  };
568
561
  }
569
562
  try {
570
- const response = await fetchImpl(options)(
563
+ const response = await resolveCursorApiFetch(options)(
571
564
  assertPresignedUrlSafe(options, instruction.url),
572
565
  {
573
566
  method: "PUT",
@@ -818,7 +811,7 @@ export function cursorHostedFiles(
818
811
  if (url === undefined) {
819
812
  return undefined;
820
813
  }
821
- const response = await fetchImpl(options)(
814
+ const response = await resolveCursorApiFetch(options)(
822
815
  assertPresignedUrlSafe(options, url),
823
816
  {
824
817
  method: "GET",
@@ -992,9 +985,9 @@ function assertPresignedUrlSafe(
992
985
  return url.toString();
993
986
  }
994
987
  try {
995
- createAgentStorePresignedUrlValidatorForBackend(resolveBaseUrl(options))(
996
- url
997
- );
988
+ createAgentStorePresignedUrlValidatorForBackend(
989
+ resolveCursorApiBaseUrl(options)
990
+ )(url);
998
991
  } catch (error) {
999
992
  throw new Error(
1000
993
  `cursorHostedFiles: refused presigned URL: ${
@@ -1005,14 +998,6 @@ function assertPresignedUrlSafe(
1005
998
  return url.toString();
1006
999
  }
1007
1000
 
1008
- function fetchImpl(options: CursorHostedFilesOptions): typeof fetch {
1009
- const impl = options.fetch ?? globalThis.fetch;
1010
- if (typeof impl !== "function") {
1011
- throw new Error("cursorHostedFiles requires fetch");
1012
- }
1013
- return impl;
1014
- }
1015
-
1016
1001
  /**
1017
1002
  * The file store this engine's deployment owns. Every request names it, so
1018
1003
  * the server resolves the store row and this process never handles a store id.
@@ -1034,17 +1019,6 @@ function resolveStoreSourceId(options: CursorHostedFilesOptions): string {
1034
1019
  return id;
1035
1020
  }
1036
1021
 
1037
- function resolveApiKey(options: CursorHostedFilesOptions): string {
1038
- const configured = options.resolveApiKey?.() ?? options.apiKey;
1039
- const key = (configured ?? process.env.CURSOR_API_KEY)?.trim();
1040
- if (key === undefined || key === "") {
1041
- throw new Error(
1042
- "cursorHostedFiles requires CURSOR_API_KEY (deployment pod credential on Cursor hosting)"
1043
- );
1044
- }
1045
- return key;
1046
- }
1047
-
1048
1022
  const DEV_PRESIGN_HOSTS: ReadonlySet<string> = new Set(
1049
1023
  (process.env.CURSOR_HOSTED_FILES_DEV_PRESIGN_HOSTS ?? "")
1050
1024
  .split(",")
@@ -1052,19 +1026,6 @@ const DEV_PRESIGN_HOSTS: ReadonlySet<string> = new Set(
1052
1026
  .filter(host => host.length > 0)
1053
1027
  );
1054
1028
 
1055
- function resolveBaseUrl(options: CursorHostedFilesOptions): string {
1056
- return trimTrailingSlash(
1057
- options.baseUrl ??
1058
- process.env.CURSOR_API_BASE_URL ??
1059
- DEFAULT_CURSOR_EXTERNAL_API_URL
1060
- );
1061
- }
1062
-
1063
- function trimTrailingSlash(raw: string): string {
1064
- const trimmed = raw.trim();
1065
- return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
1066
- }
1067
-
1068
1029
  /**
1069
1030
  * Public agent-serve-files API call. The pod credential is the whole authorization:
1070
1031
  * the API forwards this caller's identity to the store service, which checks it
@@ -1077,7 +1038,7 @@ async function storePost<T>(
1077
1038
  ): Promise<T> {
1078
1039
  return await postJson<T>(
1079
1040
  options,
1080
- `${resolveBaseUrl(options)}${AGENT_SERVE_FILES_PATH}${path}`,
1041
+ `${resolveCursorApiBaseUrl(options)}${AGENT_SERVE_FILES_PATH}${path}`,
1081
1042
  body
1082
1043
  );
1083
1044
  }
@@ -1088,7 +1049,7 @@ async function storeGet<T>(
1088
1049
  ): Promise<T> {
1089
1050
  return await sendJson<T>(
1090
1051
  options,
1091
- `${resolveBaseUrl(options)}${AGENT_SERVE_FILES_PATH}${path}`,
1052
+ `${resolveCursorApiBaseUrl(options)}${AGENT_SERVE_FILES_PATH}${path}`,
1092
1053
  "GET"
1093
1054
  );
1094
1055
  }
@@ -1096,7 +1057,9 @@ async function storeGet<T>(
1096
1057
  /**
1097
1058
  * Factory store API call — same base URL and pod credential as
1098
1059
  * {@link storePost}, different path family. The server resolves the
1099
- * deployment (and the addressed store role) from the credential.
1060
+ * deployment (and the addressed store role) from the credential. The
1061
+ * domain-call lane one path family deeper is NOT here: it belongs to the
1062
+ * store API client (`../internal/cursor/store-api-client.ts`).
1100
1063
  */
1101
1064
  async function flushPost<T>(
1102
1065
  options: CursorHostedFilesOptions,
@@ -1105,7 +1068,7 @@ async function flushPost<T>(
1105
1068
  ): Promise<T> {
1106
1069
  return await postJson<T>(
1107
1070
  options,
1108
- `${resolveBaseUrl(options)}${AGENTSDK_STORE_PATH}${path}`,
1071
+ `${resolveCursorApiBaseUrl(options)}${AGENTSDK_STORE_PATH}${path}`,
1109
1072
  body
1110
1073
  );
1111
1074
  }
@@ -1124,10 +1087,10 @@ async function sendJson<T>(
1124
1087
  method: "GET" | "POST",
1125
1088
  body?: Record<string, unknown>
1126
1089
  ): Promise<T> {
1127
- const response = await fetchImpl(options)(url, {
1090
+ const response = await resolveCursorApiFetch(options)(url, {
1128
1091
  method,
1129
1092
  headers: {
1130
- Authorization: `Bearer ${resolveApiKey(options)}`,
1093
+ Authorization: `Bearer ${resolveCursorApiKey(options)}`,
1131
1094
  ...(body === undefined ? {} : { "Content-Type": "application/json" }),
1132
1095
  },
1133
1096
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
package/src/index.ts CHANGED
@@ -166,6 +166,7 @@ export type {
166
166
  ReminderDefinition,
167
167
  ReminderFireContext,
168
168
  ReminderFireResult,
169
+ ReminderFollowupOptions,
169
170
  ReminderHandlerConfig,
170
171
  ReminderHostApi,
171
172
  ReminderInfo,