@cursor/july 0.1.74 → 0.1.75

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 (126) hide show
  1. package/dist/channels/checks.d.ts +111 -0
  2. package/dist/channels/checks.d.ts.map +1 -0
  3. package/dist/channels/checks.js +86 -0
  4. package/dist/channels/deployments/deployments-channel.d.ts +44 -0
  5. package/dist/channels/deployments/deployments-channel.d.ts.map +1 -0
  6. package/dist/channels/deployments/deployments-channel.js +92 -0
  7. package/dist/channels/deployments/index.d.ts +26 -0
  8. package/dist/channels/deployments/index.d.ts.map +1 -0
  9. package/dist/channels/deployments/index.js +25 -0
  10. package/dist/channels/deployments/types.d.ts +69 -0
  11. package/dist/channels/deployments/types.d.ts.map +1 -0
  12. package/dist/channels/deployments/types.js +6 -0
  13. package/dist/channels/github/checks.d.ts +13 -48
  14. package/dist/channels/github/checks.d.ts.map +1 -1
  15. package/dist/channels/github/checks.js +56 -64
  16. package/dist/channels/origin/api.d.ts +100 -0
  17. package/dist/channels/origin/api.d.ts.map +1 -0
  18. package/dist/channels/origin/api.js +225 -0
  19. package/dist/channels/origin/binding.d.ts +44 -0
  20. package/dist/channels/origin/binding.d.ts.map +1 -0
  21. package/dist/channels/origin/binding.js +45 -0
  22. package/dist/channels/origin/checks.d.ts +107 -0
  23. package/dist/channels/origin/checks.d.ts.map +1 -0
  24. package/dist/channels/origin/checks.js +145 -0
  25. package/dist/channels/origin/index.d.ts +3 -0
  26. package/dist/channels/origin/index.d.ts.map +1 -1
  27. package/dist/channels/origin/index.js +3 -0
  28. package/dist/channels/origin/origin-channel.d.ts +4 -4
  29. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  30. package/dist/channels/origin/origin-channel.js +12 -2
  31. package/dist/channels/origin/types.d.ts +36 -0
  32. package/dist/channels/origin/types.d.ts.map +1 -1
  33. package/dist/channels.d.ts +1 -0
  34. package/dist/channels.d.ts.map +1 -1
  35. package/dist/channels.js +1 -0
  36. package/dist/docs/404.html +2 -2
  37. package/dist/docs/ab.html +3 -3
  38. package/dist/docs/assets/{app.BETd-WqU.js → app.b8bhB2SV.js} +1 -1
  39. package/dist/docs/assets/chunks/@localSearchIndexroot.DGZeXVqn.js +1 -0
  40. package/dist/docs/assets/chunks/{VPLocalSearchBox.wEaGEmV9.js → VPLocalSearchBox.lLSeIyWw.js} +1 -1
  41. package/dist/docs/assets/chunks/{theme.DNDicFOP.js → theme.C_owesa-.js} +2 -2
  42. package/dist/docs/assets/{reference_channels.md.MN8iDy8V.js → reference_channels.md.G5GysZ7e.js} +2 -2
  43. package/dist/docs/assets/{reference_channels.md.MN8iDy8V.lean.js → reference_channels.md.G5GysZ7e.lean.js} +1 -1
  44. package/dist/docs/building-with-agents.html +3 -3
  45. package/dist/docs/concepts.html +3 -3
  46. package/dist/docs/deployment.html +3 -3
  47. package/dist/docs/evals.html +3 -3
  48. package/dist/docs/example-agents/approval-buddy.html +3 -3
  49. package/dist/docs/example-agents/benny.html +3 -3
  50. package/dist/docs/example-agents/bugbot.html +3 -3
  51. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  52. package/dist/docs/example-agents/codeowners-review.html +3 -3
  53. package/dist/docs/example-agents/concierge.html +3 -3
  54. package/dist/docs/example-agents/fsd.html +3 -3
  55. package/dist/docs/example-agents/index.html +3 -3
  56. package/dist/docs/example-agents/knowledge-base.html +3 -3
  57. package/dist/docs/example-agents/oncall.html +3 -3
  58. package/dist/docs/example-agents/security-reviewer.html +3 -3
  59. package/dist/docs/example-agents/slack-agent.html +3 -3
  60. package/dist/docs/example-agents/weather-agent.html +3 -3
  61. package/dist/docs/guides/agent-to-agent.html +3 -3
  62. package/dist/docs/guides/cloud-runtime.html +3 -3
  63. package/dist/docs/guides/convert-automation.html +3 -3
  64. package/dist/docs/guides/github.html +3 -3
  65. package/dist/docs/guides/human-in-the-loop.html +3 -3
  66. package/dist/docs/guides/mcp-oauth.html +3 -3
  67. package/dist/docs/guides/opentelemetry.html +3 -3
  68. package/dist/docs/guides/slack.html +3 -3
  69. package/dist/docs/guides/webhooks.html +3 -3
  70. package/dist/docs/hashmap.json +1 -1
  71. package/dist/docs/hillclimbing.html +3 -3
  72. package/dist/docs/index.html +3 -3
  73. package/dist/docs/quickstart.html +3 -3
  74. package/dist/docs/reference/agent-config.html +3 -3
  75. package/dist/docs/reference/artifacts.html +3 -3
  76. package/dist/docs/reference/channels.html +5 -5
  77. package/dist/docs/reference/cli.html +3 -3
  78. package/dist/docs/reference/connections.html +3 -3
  79. package/dist/docs/reference/hooks.html +3 -3
  80. package/dist/docs/reference/http-api.html +3 -3
  81. package/dist/docs/reference/instructions.html +3 -3
  82. package/dist/docs/reference/playground.html +3 -3
  83. package/dist/docs/reference/project-layout.html +3 -3
  84. package/dist/docs/reference/prompt.html +3 -3
  85. package/dist/docs/reference/schedules.html +3 -3
  86. package/dist/docs/reference/sessions.html +3 -3
  87. package/dist/docs/reference/skills.html +3 -3
  88. package/dist/docs/reference/subagents.html +3 -3
  89. package/dist/docs/reference/tools.html +3 -3
  90. package/dist/docs/scaffolding-agents.html +3 -3
  91. package/dist/docs/storage.html +3 -3
  92. package/dist/docs/templates/agentic-owners.html +3 -3
  93. package/dist/docs/templates/demo.html +3 -3
  94. package/dist/docs/templates/pr-autofixer.html +3 -3
  95. package/dist/docs/templates/security-reviewer.html +3 -3
  96. package/dist/docs/templates/triage.html +3 -3
  97. package/dist/docs/troubleshooting.html +3 -3
  98. package/dist/internal/cursor-deployment-relay.d.ts +77 -0
  99. package/dist/internal/cursor-deployment-relay.d.ts.map +1 -0
  100. package/dist/internal/cursor-deployment-relay.js +201 -0
  101. package/dist/internal/distribution.d.ts.map +1 -1
  102. package/dist/internal/distribution.js +1 -0
  103. package/dist/internal/server.d.ts.map +1 -1
  104. package/dist/internal/server.js +114 -5
  105. package/dist/playground/assets/{index-D_8I9TM0.js → index-BJy9UJrF.js} +1 -1
  106. package/dist/playground/assets/index-BrSd5vff.css +1 -0
  107. package/dist/playground/index.html +2 -2
  108. package/docs/reference/channels.md +15 -0
  109. package/package.json +7 -1
  110. package/src/channels/checks.ts +185 -0
  111. package/src/channels/deployments/deployments-channel.ts +109 -0
  112. package/src/channels/deployments/index.ts +26 -0
  113. package/src/channels/deployments/types.ts +80 -0
  114. package/src/channels/github/checks.ts +90 -115
  115. package/src/channels/origin/api.ts +318 -0
  116. package/src/channels/origin/binding.ts +96 -0
  117. package/src/channels/origin/checks.ts +250 -0
  118. package/src/channels/origin/index.ts +3 -0
  119. package/src/channels/origin/origin-channel.ts +18 -6
  120. package/src/channels/origin/types.ts +37 -0
  121. package/src/channels.ts +16 -0
  122. package/src/internal/cursor-deployment-relay.ts +274 -0
  123. package/src/internal/distribution.ts +1 -0
  124. package/src/internal/server.ts +149 -0
  125. package/dist/docs/assets/chunks/@localSearchIndexroot.DM5INS71.js +0 -1
  126. package/dist/playground/assets/index-M3nSMc6_.css +0 -1
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Pull relay for `/v0/deployment-events`.
3
+ *
4
+ * A {@link RelayAdapter} over {@link CursorRelayCore} — the same engine the
5
+ * GitHub SCM and Slack relays use, so registration heartbeats, durable
6
+ * offsets, backoff, and bounded dispatch retry are inherited rather than
7
+ * reimplemented. Only the subscribe/poll paths, the registration body, and
8
+ * the per-event filter differ.
9
+ */
10
+
11
+ import type {
12
+ DeploymentEvent,
13
+ DeploymentEventType,
14
+ DeploymentSubscription,
15
+ } from "../channels/deployments/types.js";
16
+ import { CursorRelayCore, type RelayAdapter } from "./cursor-relay-core.js";
17
+
18
+ const DEFAULT_EVENTS: readonly DeploymentEventType[] = ["deployment.succeeded"];
19
+
20
+ /** One page entry: the event fields plus its stream offset, as the sibling
21
+ * providers spell it. */
22
+ export type DeploymentRelayEvent = DeploymentEvent & { offset: string };
23
+
24
+ export interface ResolvedSubscription {
25
+ repo: string;
26
+ environments: string[];
27
+ events: Set<DeploymentEventType>;
28
+ }
29
+
30
+ interface SubscriptionStatus {
31
+ repo: string;
32
+ isSubscribed: boolean;
33
+ detail?: string;
34
+ }
35
+
36
+ export interface DeploymentEventRelayOptions {
37
+ baseUrl: string;
38
+ apiKey: string;
39
+ stateDir: string;
40
+ subscriptions: readonly ResolvedSubscription[];
41
+ dispatch: (event: DeploymentEvent) => Promise<void>;
42
+ logger: (line: string) => void;
43
+ fetchImpl?: typeof fetch;
44
+ pollWaitMs?: number;
45
+ }
46
+
47
+ /** Normalize the channel's `repos` option into concrete subscriptions. */
48
+ export function resolveDeploymentSubscriptions(options: {
49
+ repos: readonly (string | DeploymentSubscription)[];
50
+ environments?: readonly string[];
51
+ events?: readonly DeploymentEventType[];
52
+ }): ResolvedSubscription[] {
53
+ const byRepo = new Map<string, ResolvedSubscription>();
54
+ for (const entry of options.repos) {
55
+ const subscription: DeploymentSubscription =
56
+ typeof entry === "string" ? { repo: entry } : entry;
57
+ // Case-folded only: `Deployment.repository` has no documented format, and
58
+ // the server keys subscriptions the same way.
59
+ const repo = subscription.repo.trim().toLowerCase();
60
+ if (repo === "") {
61
+ throw new Error(
62
+ `deploymentsChannel: invalid repo "${subscription.repo}".`
63
+ );
64
+ }
65
+ const environments = [
66
+ ...new Set(
67
+ (subscription.environments ?? options.environments ?? [])
68
+ .map((value) => value.trim())
69
+ .filter(Boolean)
70
+ ),
71
+ ];
72
+ const events = new Set(
73
+ subscription.events ?? options.events ?? DEFAULT_EVENTS
74
+ );
75
+ if (events.size === 0) {
76
+ throw new Error(
77
+ `deploymentsChannel: repo "${repo}" subscribes to no events.`
78
+ );
79
+ }
80
+ // Later entries for the same repo widen the earlier one rather than
81
+ // silently replacing it.
82
+ const existing = byRepo.get(repo);
83
+ if (existing === undefined) {
84
+ byRepo.set(repo, { repo, environments, events });
85
+ continue;
86
+ }
87
+ for (const event of events) existing.events.add(event);
88
+ existing.environments =
89
+ existing.environments.length === 0 || environments.length === 0
90
+ ? []
91
+ : [...new Set([...existing.environments, ...environments])];
92
+ }
93
+ // Empty is legal here and means "this channel wants nothing", which is how
94
+ // an env-configured channel stays inert without a repo list. The wire-level
95
+ // empty list means deregister, so the host simply never registers it.
96
+ return [...byRepo.values()];
97
+ }
98
+
99
+ /** Whether one confirmed subscription wants this event. */
100
+ export function deploymentEventMatches(
101
+ subscription: ResolvedSubscription,
102
+ envelope: DeploymentEvent
103
+ ): boolean {
104
+ if (!subscription.events.has(envelope.eventType)) {
105
+ return false;
106
+ }
107
+ return (
108
+ subscription.environments.length === 0 ||
109
+ subscription.environments.includes(envelope.environment)
110
+ );
111
+ }
112
+
113
+ /** One channel the host can deliver a deployment event to. */
114
+ export interface DeploymentDispatchTarget {
115
+ /** Display name for logs, e.g. `executor/deployments`. */
116
+ name: string;
117
+ subscriptions: readonly ResolvedSubscription[];
118
+ deliver: (event: DeploymentEvent) => Promise<unknown>;
119
+ }
120
+
121
+ /**
122
+ * Deliver one event to every channel subscribed to it.
123
+ *
124
+ * Each channel is delivered independently: one channel's handler throwing
125
+ * must not skip the channels after it, because the relay core retries the
126
+ * whole dispatch and the channels that already ran would see the event twice.
127
+ * Throws only when every subscribed channel failed, so a retry cannot
128
+ * re-deliver to a channel that succeeded.
129
+ */
130
+ export async function fanOutDeploymentEvent(input: {
131
+ targets: readonly DeploymentDispatchTarget[];
132
+ event: DeploymentEvent;
133
+ logger: (line: string) => void;
134
+ }): Promise<void> {
135
+ const { event, logger } = input;
136
+ const matched = input.targets.filter((target) =>
137
+ target.subscriptions.some(
138
+ (subscription) =>
139
+ subscription.repo === event.repo &&
140
+ deploymentEventMatches(subscription, event)
141
+ )
142
+ );
143
+ const failures: string[] = [];
144
+ for (const target of matched) {
145
+ try {
146
+ await target.deliver(event);
147
+ } catch (error) {
148
+ failures.push(target.name);
149
+ logger(
150
+ `[agent-sdk] deployment-events: ${target.name} failed on ${event.eventId}: ${
151
+ error instanceof Error ? error.message : String(error)
152
+ }`
153
+ );
154
+ }
155
+ }
156
+ logger(
157
+ `[agent-sdk] deployment-events: ${event.eventType} ${event.repo}@${event.sha} (${event.environment}) → ${matched.length - failures.length}/${matched.length} channel(s)`
158
+ );
159
+ if (failures.length > 0 && failures.length === matched.length) {
160
+ throw new Error(
161
+ `deployment-events: every subscribed channel failed on ${event.eventId} (${failures.join(", ")})`
162
+ );
163
+ }
164
+ }
165
+
166
+ export class DeploymentEventRelay {
167
+ private readonly core: CursorRelayCore<DeploymentRelayEvent>;
168
+ private subscribed = new Map<string, ResolvedSubscription>();
169
+ private readonly loggedSkippedRepos = new Set<string>();
170
+
171
+ constructor(private readonly options: DeploymentEventRelayOptions) {
172
+ const adapter: RelayAdapter<DeploymentRelayEvent> = {
173
+ logPrefix: "deployment-events",
174
+ subscribePath: "/v0/deployment-events/subscriptions",
175
+ pollPath: "/v0/deployment-events",
176
+ buildRegistrationBody: (consumerId) => ({
177
+ consumerId,
178
+ subscriptions: options.subscriptions.map((subscription) => ({
179
+ repo: subscription.repo,
180
+ environments: subscription.environments,
181
+ events: [...subscription.events],
182
+ })),
183
+ }),
184
+ // Empty subscriptions drop this consumer's registration on clean stop
185
+ // instead of leaving it to lapse by TTL.
186
+ buildDeregistrationBody: (consumerId) => ({
187
+ consumerId,
188
+ subscriptions: [],
189
+ }),
190
+ applyRegistrationResponse: (json) => {
191
+ const result = json as { repos?: SubscriptionStatus[] };
192
+ this.subscribed = new Map();
193
+ this.loggedSkippedRepos.clear();
194
+ const lines: string[] = [];
195
+ const confirmed: string[] = [];
196
+ for (const status of result.repos ?? []) {
197
+ const wanted = options.subscriptions.find(
198
+ (subscription) => subscription.repo === status.repo
199
+ );
200
+ if (!status.isSubscribed || wanted === undefined) {
201
+ lines.push(
202
+ `not subscribed to ${status.repo}: ${status.detail ?? "unknown reason"}`
203
+ );
204
+ continue;
205
+ }
206
+ this.subscribed.set(status.repo, wanted);
207
+ confirmed.push(status.repo);
208
+ }
209
+ if (confirmed.length > 0) {
210
+ const kinds = [
211
+ ...new Set(options.subscriptions.flatMap((s) => [...s.events])),
212
+ ];
213
+ lines.push(
214
+ `subscribed to ${confirmed.join(", ")} for events: ${kinds.join(", ")}`
215
+ );
216
+ }
217
+ return lines;
218
+ },
219
+ shouldDispatch: (event) => this.shouldDispatch(event),
220
+ dispatch: ({ offset: _offset, ...envelope }) =>
221
+ options.dispatch(envelope),
222
+ };
223
+ this.core = new CursorRelayCore<DeploymentRelayEvent>({
224
+ baseUrl: options.baseUrl,
225
+ apiKey: options.apiKey,
226
+ stateDir: options.stateDir,
227
+ adapter,
228
+ logger: options.logger,
229
+ ...(options.fetchImpl === undefined
230
+ ? {}
231
+ : { fetchImpl: options.fetchImpl }),
232
+ ...(options.pollWaitMs === undefined
233
+ ? {}
234
+ : { pollWaitMs: options.pollWaitMs }),
235
+ });
236
+ }
237
+
238
+ /**
239
+ * Whether an event from this principal's stream belongs to THIS relay.
240
+ * The stream is per-principal, and one API key can back several relays
241
+ * (other hosts, or an agent running its own alongside the host's), so each
242
+ * one re-checks the events against the subscriptions the server confirmed
243
+ * for it.
244
+ */
245
+ private shouldDispatch(event: DeploymentRelayEvent): boolean {
246
+ const envelope = event;
247
+ if (
248
+ envelope === null ||
249
+ typeof envelope !== "object" ||
250
+ typeof envelope.repo !== "string"
251
+ ) {
252
+ return false;
253
+ }
254
+ const subscription = this.subscribed.get(envelope.repo);
255
+ if (subscription === undefined) {
256
+ if (!this.loggedSkippedRepos.has(envelope.repo)) {
257
+ this.loggedSkippedRepos.add(envelope.repo);
258
+ this.options.logger(
259
+ `[agent-sdk] deployment-events: skipping events for ${envelope.repo}: not in this host's subscription (another consumer of this API key likely subscribed it)`
260
+ );
261
+ }
262
+ return false;
263
+ }
264
+ return deploymentEventMatches(subscription, envelope);
265
+ }
266
+
267
+ start(): void {
268
+ this.core.start();
269
+ }
270
+
271
+ async stop(): Promise<void> {
272
+ await this.core.stop();
273
+ }
274
+ }
@@ -97,6 +97,7 @@ export const AUTHORING_ENTRY_FILES: ReadonlyArray<
97
97
  ["channels/slack", "channels/slack/index.js"],
98
98
  ["channels/github", "channels/github/index.js"],
99
99
  ["channels/origin", "channels/origin/index.js"],
100
+ ["channels/deployments", "channels/deployments/index.js"],
100
101
  ["skills", "skills.js"],
101
102
  ["schedules", "schedules.js"],
102
103
  ["reminders", "reminders.js"],
@@ -14,6 +14,11 @@ import {
14
14
  } from "node:http";
15
15
  import type { AddressInfo } from "node:net";
16
16
  import { join } from "node:path";
17
+ import {
18
+ dispatchDeploymentEvent,
19
+ getDeploymentsChannelOptions,
20
+ } from "../channels/deployments/deployments-channel.js";
21
+ import type { DeploymentsChannelOptions } from "../channels/deployments/types.js";
17
22
  import {
18
23
  type CursorGithubRepoScope,
19
24
  createCursorAccountGithubTokenResolver,
@@ -62,6 +67,12 @@ import {
62
67
  SIGN_IN_HINT,
63
68
  } from "./cursor/credentials.js";
64
69
  import { CursorGithubCredentialProvider } from "./cursor/github-credentials.js";
70
+ import {
71
+ DeploymentEventRelay,
72
+ fanOutDeploymentEvent,
73
+ type ResolvedSubscription,
74
+ resolveDeploymentSubscriptions,
75
+ } from "./cursor-deployment-relay.js";
65
76
  import {
66
77
  CursorEventRelay,
67
78
  type CursorScmSubscription,
@@ -774,6 +785,15 @@ export async function startServer(
774
785
  (cursorGithubTargets.length > 0 || cursorOriginTargets.length > 0),
775
786
  });
776
787
 
788
+ // Cursor deployment-events relay: automatic when a mounted agent authored
789
+ // `deploymentsChannel({ ... })`.
790
+ const cursorDeploymentRelay = startCursorDeploymentRelay({
791
+ options,
792
+ runtimes,
793
+ accountKey,
794
+ logger,
795
+ });
796
+
777
797
  const server = createServer((req, res) => {
778
798
  void instrumentHttp(otelRuntime, req, res, () =>
779
799
  handleRequest(router, req, res, logger)
@@ -793,6 +813,7 @@ export async function startServer(
793
813
  logger,
794
814
  });
795
815
  } catch (error) {
816
+ await cursorDeploymentRelay?.stop();
796
817
  await cursorEventRelay?.stop();
797
818
  await cursorSlackRelay?.stop();
798
819
  cursorGithubCredentials?.stop();
@@ -953,6 +974,7 @@ export async function startServer(
953
974
  },
954
975
  close: async () => {
955
976
  playgroundDev?.stop();
977
+ await cursorDeploymentRelay?.stop();
956
978
  await cursorEventRelay?.stop();
957
979
  await cursorSlackRelay?.stop();
958
980
  for (const runtime of runtimes) {
@@ -2170,6 +2192,133 @@ function collectCursorSlackTargets(
2170
2192
  * the pack's normal inbound path. Requires a signed-in host — fail serve
2171
2193
  * fast rather than mounting channels that can never hear or speak.
2172
2194
  */
2195
+ interface CursorDeploymentTarget {
2196
+ runtime: MountedRuntime;
2197
+ channelId: string;
2198
+ options: DeploymentsChannelOptions;
2199
+ subscriptions: ResolvedSubscription[];
2200
+ }
2201
+
2202
+ /** Every mounted `deploymentsChannel`, with its resolved subscriptions. */
2203
+ function collectCursorDeploymentTargets(
2204
+ runtimes: MountedRuntime[],
2205
+ skipped: string[]
2206
+ ): CursorDeploymentTarget[] {
2207
+ const targets: CursorDeploymentTarget[] = [];
2208
+ for (const runtime of runtimes) {
2209
+ for (const channel of runtime.project.channels) {
2210
+ const options = getDeploymentsChannelOptions(channel.definition);
2211
+ if (options === undefined) {
2212
+ continue;
2213
+ }
2214
+ const subscriptions = resolveDeploymentSubscriptions(options);
2215
+ if (subscriptions.length === 0) {
2216
+ // Declared the channel but named no repositories — usually an unset
2217
+ // env var. Say so rather than starting a relay that receives nothing.
2218
+ skipped.push(`${runtime.slug || runtime.project.name}/${channel.id}`);
2219
+ continue;
2220
+ }
2221
+ targets.push({ runtime, channelId: channel.id, options, subscriptions });
2222
+ }
2223
+ }
2224
+ return targets;
2225
+ }
2226
+
2227
+ /**
2228
+ * Cursor deployment-events relay: automatic when a mounted agent authored
2229
+ * `deploymentsChannel({ ... })`. One relay per process reads
2230
+ * `/v0/deployment-events` for the union of every channel's repositories, and
2231
+ * each event is routed to the channels that asked for it — the same ownership
2232
+ * `startCursorSlackRelay` and `startCursorEventRelay` have.
2233
+ */
2234
+ function startCursorDeploymentRelay(input: {
2235
+ options: ServeOptions;
2236
+ runtimes: MountedRuntime[];
2237
+ accountKey: ResolvedApiKey | undefined;
2238
+ logger: (line: string) => void;
2239
+ }): DeploymentEventRelay | undefined {
2240
+ const { options, runtimes, accountKey, logger } = input;
2241
+ const skipped: string[] = [];
2242
+ const targets = collectCursorDeploymentTargets(runtimes, skipped);
2243
+ if (skipped.length > 0) {
2244
+ logger(
2245
+ `[agent-sdk] deployment-events: no repositories declared on ${skipped.join(", ")}; those channels receive nothing`
2246
+ );
2247
+ }
2248
+ if (targets.length === 0) {
2249
+ return undefined;
2250
+ }
2251
+ if (accountKey === undefined) {
2252
+ throw new Error(
2253
+ `This project has channels subscribed to deployment events (deploymentsChannel({ repos })) but the host is not signed in to Cursor. ${SIGN_IN_HINT}`
2254
+ );
2255
+ }
2256
+ // Union across channels: one registration per repo for the whole process,
2257
+ // however many channels named it. Per-channel environment and event-type
2258
+ // filtering happens at dispatch.
2259
+ const byRepo = new Map<string, ResolvedSubscription>();
2260
+ for (const target of targets) {
2261
+ for (const subscription of target.subscriptions) {
2262
+ const existing = byRepo.get(subscription.repo);
2263
+ if (existing === undefined) {
2264
+ byRepo.set(subscription.repo, {
2265
+ repo: subscription.repo,
2266
+ environments: [...subscription.environments],
2267
+ events: new Set(subscription.events),
2268
+ });
2269
+ continue;
2270
+ }
2271
+ for (const event of subscription.events) existing.events.add(event);
2272
+ // An empty environment list means "every environment", so it absorbs.
2273
+ existing.environments =
2274
+ existing.environments.length === 0 ||
2275
+ subscription.environments.length === 0
2276
+ ? []
2277
+ : [
2278
+ ...new Set([
2279
+ ...existing.environments,
2280
+ ...subscription.environments,
2281
+ ]),
2282
+ ];
2283
+ }
2284
+ }
2285
+ // Same host split as scm-events and slack-events: `/v0/deployment-events/*`
2286
+ // is on api.cursor.com, not api2.
2287
+ const baseStateRoot =
2288
+ options.stateRoot ??
2289
+ join(targets[0].runtime.project.rootDir, ".agent-serve");
2290
+ const relay = new DeploymentEventRelay({
2291
+ baseUrl: cursorExternalApiUrl(),
2292
+ apiKey: accountKey.apiKey,
2293
+ stateDir: join(baseStateRoot, "deployment-events"),
2294
+ subscriptions: [...byRepo.values()],
2295
+ logger,
2296
+ dispatch: async (event) =>
2297
+ fanOutDeploymentEvent({
2298
+ event,
2299
+ logger,
2300
+ targets: targets.map((target) => ({
2301
+ name: `${target.runtime.slug || target.runtime.project.name}/${target.channelId}`,
2302
+ subscriptions: target.subscriptions,
2303
+ deliver: (delivered) =>
2304
+ dispatchDeploymentEvent({
2305
+ options: target.options,
2306
+ event: delivered,
2307
+ args: buildChannelHandlerArgs({
2308
+ runtime: target.runtime,
2309
+ channelId: target.channelId,
2310
+ }),
2311
+ }),
2312
+ })),
2313
+ }),
2314
+ });
2315
+ relay.start();
2316
+ logger(
2317
+ `[agent-sdk] deployment-events: relaying ${byRepo.size} repository subscription(s) to ${targets.length} channel(s)`
2318
+ );
2319
+ return relay;
2320
+ }
2321
+
2173
2322
  async function startCursorSlackRelay(input: {
2174
2323
  options: ServeOptions;
2175
2324
  runtimes: MountedRuntime[];