@cursor/july 0.1.84 → 0.1.86

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 (116) hide show
  1. package/dist/channels/deployments/deployments-channel.d.ts +2 -2
  2. package/dist/channels/deployments/deployments-channel.d.ts.map +1 -1
  3. package/dist/channels/deployments/deployments-channel.js +7 -6
  4. package/dist/channels/deployments/index.d.ts +2 -3
  5. package/dist/channels/deployments/index.d.ts.map +1 -1
  6. package/dist/channels/deployments/index.js +2 -3
  7. package/dist/channels/deployments/types.d.ts +14 -14
  8. package/dist/channels/deployments/types.d.ts.map +1 -1
  9. package/dist/channels/github/github-channel.d.ts +4 -0
  10. package/dist/channels/github/github-channel.d.ts.map +1 -1
  11. package/dist/channels/github/github-channel.js +18 -11
  12. package/dist/channels/origin/origin-channel.d.ts +6 -0
  13. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  14. package/dist/channels/origin/origin-channel.js +24 -11
  15. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  16. package/dist/channels/slack/dispatch.js +12 -2
  17. package/dist/docs/404.html +2 -2
  18. package/dist/docs/ab.html +3 -3
  19. package/dist/docs/assets/{app.kqdC1N7p.js → app.DUOPbN18.js} +1 -1
  20. package/dist/docs/assets/chunks/@localSearchIndexroot.CxCtxfDE.js +1 -0
  21. package/dist/docs/assets/chunks/{VPLocalSearchBox.DXDE2WgG.js → VPLocalSearchBox.CmWbGcGk.js} +1 -1
  22. package/dist/docs/assets/chunks/{theme.DFYqbyta.js → theme.S57OeOLA.js} +2 -2
  23. package/dist/docs/assets/{reference_channels.md.G5GysZ7e.js → reference_channels.md.CboFd5IH.js} +1 -1
  24. package/dist/docs/building-with-agents.html +3 -3
  25. package/dist/docs/concepts.html +3 -3
  26. package/dist/docs/deployment.html +3 -3
  27. package/dist/docs/evals.html +3 -3
  28. package/dist/docs/example-agents/approval-buddy.html +3 -3
  29. package/dist/docs/example-agents/benny.html +3 -3
  30. package/dist/docs/example-agents/bugbot.html +3 -3
  31. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  32. package/dist/docs/example-agents/codeowners-review.html +3 -3
  33. package/dist/docs/example-agents/concierge.html +3 -3
  34. package/dist/docs/example-agents/fsd.html +3 -3
  35. package/dist/docs/example-agents/index.html +3 -3
  36. package/dist/docs/example-agents/knowledge-base.html +3 -3
  37. package/dist/docs/example-agents/oncall.html +3 -3
  38. package/dist/docs/example-agents/security-reviewer.html +3 -3
  39. package/dist/docs/example-agents/slack-agent.html +3 -3
  40. package/dist/docs/example-agents/weather-agent.html +3 -3
  41. package/dist/docs/guides/agent-to-agent.html +3 -3
  42. package/dist/docs/guides/cloud-runtime.html +3 -3
  43. package/dist/docs/guides/convert-automation.html +3 -3
  44. package/dist/docs/guides/github.html +3 -3
  45. package/dist/docs/guides/human-in-the-loop.html +3 -3
  46. package/dist/docs/guides/mcp-oauth.html +3 -3
  47. package/dist/docs/guides/opentelemetry.html +3 -3
  48. package/dist/docs/guides/slack.html +3 -3
  49. package/dist/docs/guides/webhooks.html +3 -3
  50. package/dist/docs/hashmap.json +1 -1
  51. package/dist/docs/hillclimbing.html +3 -3
  52. package/dist/docs/index.html +3 -3
  53. package/dist/docs/quickstart.html +3 -3
  54. package/dist/docs/reference/agent-config.html +3 -3
  55. package/dist/docs/reference/artifacts.html +3 -3
  56. package/dist/docs/reference/channels.html +5 -5
  57. package/dist/docs/reference/cli.html +3 -3
  58. package/dist/docs/reference/connections.html +3 -3
  59. package/dist/docs/reference/hooks.html +3 -3
  60. package/dist/docs/reference/http-api.html +3 -3
  61. package/dist/docs/reference/instructions.html +3 -3
  62. package/dist/docs/reference/playground.html +3 -3
  63. package/dist/docs/reference/project-layout.html +3 -3
  64. package/dist/docs/reference/prompt.html +3 -3
  65. package/dist/docs/reference/schedules.html +3 -3
  66. package/dist/docs/reference/sessions.html +3 -3
  67. package/dist/docs/reference/skills.html +3 -3
  68. package/dist/docs/reference/subagents.html +3 -3
  69. package/dist/docs/reference/tools.html +3 -3
  70. package/dist/docs/scaffolding-agents.html +3 -3
  71. package/dist/docs/storage.html +3 -3
  72. package/dist/docs/templates/agentic-owners.html +3 -3
  73. package/dist/docs/templates/demo.html +3 -3
  74. package/dist/docs/templates/pr-autofixer.html +3 -3
  75. package/dist/docs/templates/security-reviewer.html +3 -3
  76. package/dist/docs/templates/triage.html +3 -3
  77. package/dist/docs/troubleshooting.html +3 -3
  78. package/dist/internal/continuation-identity.d.ts +10 -0
  79. package/dist/internal/continuation-identity.d.ts.map +1 -1
  80. package/dist/internal/continuation-identity.js +10 -0
  81. package/dist/internal/cursor-deployment-relay.d.ts +4 -4
  82. package/dist/internal/cursor-deployment-relay.d.ts.map +1 -1
  83. package/dist/internal/cursor-deployment-relay.js +33 -32
  84. package/dist/internal/handleAgentServeTrigger.d.ts +5 -0
  85. package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
  86. package/dist/internal/handleAgentServeTrigger.js +11 -3
  87. package/dist/internal/local-control-plane.d.ts +19 -0
  88. package/dist/internal/local-control-plane.d.ts.map +1 -1
  89. package/dist/internal/local-control-plane.js +17 -0
  90. package/dist/internal/local-ingress.d.ts +35 -0
  91. package/dist/internal/local-ingress.d.ts.map +1 -0
  92. package/dist/internal/local-ingress.js +44 -0
  93. package/dist/internal/server.d.ts.map +1 -1
  94. package/dist/internal/server.js +151 -75
  95. package/dist/playground/assets/{index-Cspgk9jC.js → index-Bqn91tW4.js} +45 -45
  96. package/dist/playground/assets/index-C0_5hOsf.css +1 -0
  97. package/dist/playground/index.html +2 -2
  98. package/dist/types.d.ts +2 -2
  99. package/docs/reference/channels.md +13 -11
  100. package/package.json +1 -1
  101. package/src/channels/deployments/deployments-channel.ts +7 -6
  102. package/src/channels/deployments/index.ts +2 -3
  103. package/src/channels/deployments/types.ts +14 -14
  104. package/src/channels/github/github-channel.ts +18 -21
  105. package/src/channels/origin/origin-channel.ts +26 -15
  106. package/src/channels/slack/dispatch.ts +11 -7
  107. package/src/internal/continuation-identity.ts +10 -0
  108. package/src/internal/cursor-deployment-relay.ts +38 -36
  109. package/src/internal/handleAgentServeTrigger.ts +31 -2
  110. package/src/internal/local-control-plane.ts +33 -0
  111. package/src/internal/local-ingress.ts +90 -0
  112. package/src/internal/server.ts +194 -40
  113. package/src/types.ts +2 -2
  114. package/dist/docs/assets/chunks/@localSearchIndexroot.BqhvUA76.js +0 -1
  115. package/dist/playground/assets/index-ChETdG-_.css +0 -1
  116. /package/dist/docs/assets/{reference_channels.md.G5GysZ7e.lean.js → reference_channels.md.CboFd5IH.lean.js} +0 -0
@@ -2,15 +2,37 @@
2
2
  * In-process control plane started with local `serve`. Owns schedule clocks
3
3
  * and reminder documents + clocks, then fires into the kit the same way the
4
4
  * Cursor backend does over HTTP when `--no-control-plane` is set.
5
+ *
6
+ * v2 ingest lives here: `routeContinuation` stamps the mailbox (July twin
7
+ * of hosted `routeVerifiedIngressContinuation`), and `attachScmEvents`
8
+ * owns the `/v0/scm-events` long poll. v1 keeps engine-side scm-events.
9
+ * Hosted `--no-control-plane` v2 skips that poll — the hosted CP delivers.
5
10
  */
6
11
 
7
12
  import { dirname, join } from "node:path";
8
13
  import { type CronExpression, parseCron } from "./cron.js";
14
+ import {
15
+ type LocalIngressContinuation,
16
+ type RouteLocalIngressContinuationInput,
17
+ routeLocalIngressContinuation,
18
+ } from "./local-ingress.js";
9
19
  import type { PlatformTimers } from "./platform-timers.js";
10
20
  import { MAX_TIMER_MS } from "./reminder-schedule.js";
11
21
  import { type ReminderRecord, ReminderStore } from "./reminder-store.js";
12
22
 
13
23
  export interface LocalControlPlane extends PlatformTimers {
24
+ /**
25
+ * Stamp a v2 mailbox from the channel selector + live payload. Same
26
+ * contract as hosted verified ingress. `/v1` is a no-op.
27
+ */
28
+ routeContinuation(
29
+ input: RouteLocalIngressContinuationInput
30
+ ): LocalIngressContinuation;
31
+ /**
32
+ * Take ownership of the v2 `/v0/scm-events` long poll. Starts the relay.
33
+ * v1 scm-events stay on the engine serve path.
34
+ */
35
+ attachScmEvents(relay: { start(): void; stop(): Promise<void> }): void;
14
36
  /** Arm persisted reminder clocks after the kit fire callback is live. */
15
37
  startClocks(): void;
16
38
  stop(): void;
@@ -40,6 +62,7 @@ export async function createLocalControlPlane(opts: {
40
62
  const reminders = new Map<string, ReminderRecord>();
41
63
  let scheduleTimer: NodeJS.Timeout | undefined;
42
64
  let lastFiredMinute = -1;
65
+ let scmRelay: { start(): void; stop(): Promise<void> } | undefined;
43
66
 
44
67
  if (store !== undefined) {
45
68
  for (const record of await store.list()) {
@@ -138,6 +161,12 @@ export async function createLocalControlPlane(opts: {
138
161
  }
139
162
 
140
163
  return {
164
+ routeContinuation: routeLocalIngressContinuation,
165
+ attachScmEvents(relay) {
166
+ scmRelay = relay;
167
+ relay.start();
168
+ logger("[agent-sdk] cursor-events: control-plane ingest started");
169
+ },
141
170
  async upsertSchedule(input) {
142
171
  schedules.set(input.name, parseCron(input.cron));
143
172
  armScheduleTicker();
@@ -183,6 +212,10 @@ export async function createLocalControlPlane(opts: {
183
212
  for (const id of [...reminderTimers.keys()]) {
184
213
  clearReminderTimer(id);
185
214
  }
215
+ if (scmRelay !== undefined) {
216
+ void scmRelay.stop();
217
+ scmRelay = undefined;
218
+ }
186
219
  },
187
220
  };
188
221
  }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Local control-plane ingest for v2 continuation.
3
+ *
4
+ * Twin of backend `routeVerifiedIngressContinuation`. Hosted Cursor runs
5
+ * that copy after the webhook is verified. Self-hosted / `july serve`
6
+ * runs this copy on the in-process local CP before the channel adapter
7
+ * `send()`s. Channel adapters do not extract.
8
+ */
9
+
10
+ import type { ChannelHandlerArgs } from "../types.js";
11
+ import {
12
+ bindMailboxContinuationSend,
13
+ type ContinuationIdentity,
14
+ continuationSource,
15
+ type MailboxContinuation,
16
+ normalizeIdentity,
17
+ stampMailboxContinuation,
18
+ } from "./continuation-identity.js";
19
+
20
+ export type LocalIngressContinuation = {
21
+ explicitContinuationKey?: string;
22
+ mailboxContinuation?: MailboxContinuation;
23
+ };
24
+
25
+ export type RouteLocalIngressContinuation = (
26
+ input: RouteLocalIngressContinuationInput
27
+ ) => LocalIngressContinuation;
28
+
29
+ export type RouteLocalIngressContinuationInput = {
30
+ identity?: ContinuationIdentity;
31
+ kind?: string;
32
+ eventType: string;
33
+ payload: unknown;
34
+ deterministicContinuationKey?: string;
35
+ };
36
+
37
+ /**
38
+ * Selector + live payload → mailbox stamp. `/v1` and omitted identity
39
+ * keep the caller-supplied deterministic key and do not stamp a mailbox.
40
+ */
41
+ export function routeLocalIngressContinuation(
42
+ input: RouteLocalIngressContinuationInput
43
+ ): LocalIngressContinuation {
44
+ if (input.identity === undefined) {
45
+ return {
46
+ ...(input.deterministicContinuationKey === undefined
47
+ ? {}
48
+ : { explicitContinuationKey: input.deterministicContinuationKey }),
49
+ };
50
+ }
51
+ const identity = normalizeIdentity(input.identity, input.kind);
52
+ if (continuationSource(identity, input.kind) !== "control-plane") {
53
+ return {
54
+ ...(input.deterministicContinuationKey === undefined
55
+ ? {}
56
+ : { explicitContinuationKey: input.deterministicContinuationKey }),
57
+ };
58
+ }
59
+ const mailboxContinuation = stampMailboxContinuation(
60
+ identity,
61
+ input.eventType,
62
+ input.payload,
63
+ input.kind
64
+ );
65
+ if (mailboxContinuation.key.startsWith("cont:")) {
66
+ return {
67
+ explicitContinuationKey: mailboxContinuation.key,
68
+ mailboxContinuation,
69
+ };
70
+ }
71
+ return { mailboxContinuation };
72
+ }
73
+
74
+ /**
75
+ * Bind a CP mailbox stamp onto `args.send` so the channel adapter only
76
+ * calls `send()` — it does not extract. No-op for `/v1`.
77
+ */
78
+ export function bindLocalIngressSend(
79
+ args: ChannelHandlerArgs,
80
+ input: RouteLocalIngressContinuationInput & {
81
+ routeContinuation?: RouteLocalIngressContinuation;
82
+ }
83
+ ): ChannelHandlerArgs {
84
+ const route = input.routeContinuation ?? routeLocalIngressContinuation;
85
+ const routed = route(input);
86
+ return {
87
+ ...args,
88
+ send: bindMailboxContinuationSend(args.send, routed.mailboxContinuation),
89
+ };
90
+ }
@@ -129,6 +129,7 @@ import {
129
129
  createLocalControlPlane,
130
130
  type LocalControlPlane,
131
131
  } from "./local-control-plane.js";
132
+ import type { RouteLocalIngressContinuation } from "./local-ingress.js";
132
133
  import { createLogRing, type LogRing } from "./log-ring.js";
133
134
  import {
134
135
  createAgentMcpEndpoint,
@@ -216,6 +217,28 @@ interface MountedRuntime {
216
217
  evalRuns: EvalRunStore;
217
218
  }
218
219
 
220
+ function localControlPlaneRoute(
221
+ runtimes: readonly MountedRuntime[]
222
+ ): RouteLocalIngressContinuation | undefined {
223
+ for (const runtime of runtimes) {
224
+ if (runtime.controlPlane !== undefined) {
225
+ return (input) => runtime.controlPlane!.routeContinuation(input);
226
+ }
227
+ }
228
+ return undefined;
229
+ }
230
+
231
+ function firstLocalControlPlane(
232
+ runtimes: readonly MountedRuntime[]
233
+ ): LocalControlPlane | undefined {
234
+ return runtimes.find((runtime) => runtime.controlPlane !== undefined)
235
+ ?.controlPlane;
236
+ }
237
+
238
+ function isV2Project(project: { agent: { architecture?: string } }): boolean {
239
+ return project.agent.architecture === "v2";
240
+ }
241
+
219
242
  /** Internal-only options for {@link startServer} (not on public `serve`). */
220
243
  export interface StartServerInternalOptions {
221
244
  /** Test seam: replace the Cursor SDK runner (integration tests). */
@@ -317,6 +340,20 @@ export async function startServer(
317
340
  ? undefined
318
341
  : requireCursorEventsAuth(effectiveCursorEvents, accountKey);
319
342
 
343
+ // Two architectures + local CP + cursor-events would register two
344
+ // /v0/scm-events consumers (engine offset vs control-plane offset).
345
+ // Fail before engines or relays start. `--no-control-plane` mixed is one
346
+ // July consumer (v1 engine); hosted CP owns v2.
347
+ assertSingleCursorEventsLane({
348
+ noControlPlane,
349
+ config: effectiveCursorEvents,
350
+ auth: cursorEventsAuth,
351
+ v1Mounts: mounts.filter((mount) => !isV2Project(mount.project)),
352
+ v2Mounts: mounts.filter((mount) => isV2Project(mount.project)),
353
+ githubRepos: cursorGithubConfig.cursorEvents?.repos ?? [],
354
+ originRepos: cursorOriginScope?.repos ?? [],
355
+ });
356
+
320
357
  let cursorGithubCredentials: CursorGithubCredentialProvider | undefined;
321
358
  // Proxy-wired GitHub token resolver for `ctx.host.github` when no startup
322
359
  // token mint runs (empty repo scope): host Octokit calls must route
@@ -755,36 +792,46 @@ export async function startServer(
755
792
  // job (backend `/github_webhook` already verified); we only admit loopback
756
793
  // so this route is not a public unsigned endpoint. Agents decide whether
757
794
  // to run via their onPullRequest / onComment / … hooks.
758
- const collectGitHubTargets = () =>
795
+ const collectGitHubTargetsFrom = (selected: readonly MountedRuntime[]) =>
759
796
  collectGitHubTriggerTargets({
760
- mounts: runtimes.map((runtime) => ({
797
+ mounts: selected.map((runtime) => ({
761
798
  slug: runtime.slug,
762
799
  channels: runtime.project.channels,
763
800
  buildHandlerArgs: (channelId) =>
764
801
  buildChannelHandlerArgs({ runtime, channelId }),
765
802
  })),
766
803
  });
767
- const collectOriginTargets = () =>
804
+ const collectOriginTargetsFrom = (selected: readonly MountedRuntime[]) =>
768
805
  collectOriginTriggerTargets({
769
- mounts: runtimes.map((runtime) => ({
806
+ mounts: selected.map((runtime) => ({
770
807
  slug: runtime.slug,
771
808
  channels: runtime.project.channels,
772
809
  buildHandlerArgs: (channelId) =>
773
810
  buildChannelHandlerArgs({ runtime, channelId }),
774
811
  })),
775
812
  });
813
+ const collectGitHubTargets = () => collectGitHubTargetsFrom(runtimes);
814
+ const collectOriginTargets = () => collectOriginTargetsFrom(runtimes);
815
+ const v1Runtimes = runtimes.filter(
816
+ (runtime) => !isV2Project(runtime.project)
817
+ );
818
+ const v2Runtimes = runtimes.filter((runtime) => isV2Project(runtime.project));
819
+ const v1Mounts = mounts.filter((mount) => !isV2Project(mount.project));
820
+ const v2Mounts = mounts.filter((mount) => isV2Project(mount.project));
776
821
  router.add({
777
822
  method: "POST",
778
823
  pattern: "/v1/webhooks/github",
779
824
  auth: [localDevStrict()],
780
825
  handler: async (request) => {
781
826
  const bodyText = await request.text();
827
+ const routeContinuation = localControlPlaneRoute(runtimes);
782
828
  const result = await handleAgentServeTrigger({
783
829
  type: "github",
784
830
  bodyText,
785
831
  eventName: request.headers.get("x-github-event"),
786
832
  deliveryId: request.headers.get("x-github-delivery") ?? undefined,
787
833
  targets: collectGitHubTargets(),
834
+ ...(routeContinuation === undefined ? {} : { routeContinuation }),
788
835
  });
789
836
  const status = result.ok ? 202 : (result.targets[0]?.status ?? 500);
790
837
  return jsonResponse(result, { status });
@@ -820,21 +867,48 @@ export async function startServer(
820
867
  throw error;
821
868
  }
822
869
 
823
- // Cursor SCM events relay (opt-in: serve({ cursorEvents }) / --cursor-events).
824
- const cursorEventRelay = startCursorEventRelay({
870
+ // Cursor SCM events: v1 stays on the engine poll. v2 ingest is local-CP
871
+ // only hosted `--no-control-plane` skips the July poll (hosted CP wakes).
872
+ const routeContinuation = localControlPlaneRoute(runtimes);
873
+ const cursorEventsCommon = {
825
874
  config: effectiveCursorEvents,
826
875
  githubRepos: cursorGithubConfig.cursorEvents?.repos ?? [],
827
876
  originRepos: cursorOriginScope?.repos ?? [],
828
877
  stateRoot: options.stateRoot,
829
- mounts,
830
878
  auth: cursorEventsAuth,
831
879
  logger,
832
- collectGitHubTargets,
833
- collectOriginTargets,
834
880
  cursorAccountOnly:
835
881
  options.cursorEvents === undefined &&
836
882
  (cursorGithubTargets.length > 0 || cursorOriginTargets.length > 0),
883
+ };
884
+ const cursorEventRelay = startCursorEventRelay({
885
+ ...cursorEventsCommon,
886
+ mounts: v1Mounts,
887
+ collectGitHubTargets: () => collectGitHubTargetsFrom(v1Runtimes),
888
+ collectOriginTargets: () => collectOriginTargetsFrom(v1Runtimes),
889
+ lane: "engine",
890
+ skippedV2: v2Mounts.length > 0,
891
+ });
892
+ const controlPlaneScmRelay = startCursorEventRelay({
893
+ ...cursorEventsCommon,
894
+ mounts: v2Mounts,
895
+ collectGitHubTargets: () => collectGitHubTargetsFrom(v2Runtimes),
896
+ collectOriginTargets: () => collectOriginTargetsFrom(v2Runtimes),
897
+ ...(routeContinuation === undefined ? {} : { routeContinuation }),
898
+ lane: "control-plane",
899
+ start: false,
900
+ stateDirName: join("control-plane", "cursor-events"),
837
901
  });
902
+ const localCp = firstLocalControlPlane(runtimes);
903
+ if (controlPlaneScmRelay !== undefined) {
904
+ if (localCp !== undefined) {
905
+ localCp.attachScmEvents(controlPlaneScmRelay);
906
+ } else {
907
+ logger(
908
+ "[agent-sdk] cursor-events: skipping v2 scm-events poll (--no-control-plane); hosted CP owns ingest"
909
+ );
910
+ }
911
+ }
838
912
 
839
913
  // Cursor deployment-events relay: automatic when a mounted agent authored
840
914
  // `deploymentsChannel({ ... })`.
@@ -866,6 +940,7 @@ export async function startServer(
866
940
  } catch (error) {
867
941
  await cursorDeploymentRelay?.stop();
868
942
  await cursorEventRelay?.stop();
943
+ await controlPlaneScmRelay?.stop();
869
944
  await cursorSlackRelay?.stop();
870
945
  cursorGithubCredentials?.stop();
871
946
  await shutdownOtelRuntime(otelRuntime);
@@ -1027,6 +1102,7 @@ export async function startServer(
1027
1102
  playgroundDev?.stop();
1028
1103
  await cursorDeploymentRelay?.stop();
1029
1104
  await cursorEventRelay?.stop();
1105
+ await controlPlaneScmRelay?.stop();
1030
1106
  await cursorSlackRelay?.stop();
1031
1107
  for (const runtime of runtimes) {
1032
1108
  runtime.controlPlane?.stop();
@@ -1878,24 +1954,13 @@ function requireCursorEventsAuth(
1878
1954
  * channels; Origin is subscribed only when an `originChannel({ cursorAccount })`
1879
1955
  * opted in.
1880
1956
  */
1881
- function startCursorEventRelay(input: {
1882
- config: ServeOptions["cursorEvents"];
1957
+ function collectCursorEventSubscriptions(input: {
1958
+ mounts: readonly AgentMount[];
1883
1959
  githubRepos: readonly string[];
1884
1960
  originRepos: readonly string[];
1885
- stateRoot: string | undefined;
1886
- mounts: AgentMount[];
1887
- auth: CursorEventsAuth | undefined;
1888
- logger: (line: string) => void;
1889
- collectGitHubTargets: () => ReturnType<typeof collectGitHubTriggerTargets>;
1890
- collectOriginTargets: () => ReturnType<typeof collectOriginTriggerTargets>;
1891
- cursorAccountOnly: boolean;
1892
- }): CursorEventRelay | undefined {
1893
- const { config, stateRoot, mounts, auth, logger } = input;
1894
- if (config === undefined || auth === undefined) {
1895
- return undefined;
1896
- }
1897
- const githubTargets = findGitHubChannelTargets(mounts);
1898
- const originTargets = findOriginChannelTargets(mounts);
1961
+ }): CursorScmSubscription[] {
1962
+ const githubTargets = findGitHubChannelTargets(input.mounts);
1963
+ const originTargets = findOriginChannelTargets(input.mounts);
1899
1964
  const githubEvents = [
1900
1965
  ...new Set(githubTargets.flatMap((target) => target.events)),
1901
1966
  ];
@@ -1928,8 +1993,88 @@ function startCursorEventRelay(input: {
1928
1993
  });
1929
1994
  }
1930
1995
  }
1996
+ return subscriptions;
1997
+ }
1998
+
1999
+ /**
2000
+ * Local CP + `--cursor-events` + both architectures would start two
2001
+ * consumers (different offset dirs). Refuse that at serve start.
2002
+ * `--no-control-plane` is one July consumer — hosted CP owns v2.
2003
+ */
2004
+ function assertSingleCursorEventsLane(input: {
2005
+ noControlPlane: boolean;
2006
+ config: ServeOptions["cursorEvents"];
2007
+ auth: CursorEventsAuth | undefined;
2008
+ v1Mounts: readonly AgentMount[];
2009
+ v2Mounts: readonly AgentMount[];
2010
+ githubRepos: readonly string[];
2011
+ originRepos: readonly string[];
2012
+ }): void {
2013
+ if (
2014
+ input.noControlPlane ||
2015
+ input.config === undefined ||
2016
+ input.auth === undefined
2017
+ ) {
2018
+ return;
2019
+ }
2020
+ const shared = {
2021
+ githubRepos: input.githubRepos,
2022
+ originRepos: input.originRepos,
2023
+ };
2024
+ if (
2025
+ collectCursorEventSubscriptions({ mounts: input.v1Mounts, ...shared })
2026
+ .length === 0 ||
2027
+ collectCursorEventSubscriptions({ mounts: input.v2Mounts, ...shared })
2028
+ .length === 0
2029
+ ) {
2030
+ return;
2031
+ }
2032
+ throw new Error(
2033
+ "[agent-sdk] cursor-events: refusing to serve v1 and v2 github/origin agents in one process — they would open two /v0/scm-events consumers. Serve one architecture per process."
2034
+ );
2035
+ }
2036
+
2037
+ function startCursorEventRelay(input: {
2038
+ config: ServeOptions["cursorEvents"];
2039
+ githubRepos: readonly string[];
2040
+ originRepos: readonly string[];
2041
+ stateRoot: string | undefined;
2042
+ mounts: AgentMount[];
2043
+ auth: CursorEventsAuth | undefined;
2044
+ logger: (line: string) => void;
2045
+ collectGitHubTargets: () => ReturnType<typeof collectGitHubTriggerTargets>;
2046
+ collectOriginTargets: () => ReturnType<typeof collectOriginTriggerTargets>;
2047
+ routeContinuation?: RouteLocalIngressContinuation;
2048
+ cursorAccountOnly: boolean;
2049
+ /** Engine = v1 poll. Control-plane = v2 poll (started by local CP). */
2050
+ lane: "engine" | "control-plane";
2051
+ /** When the engine lane has no v1 targets, mention skipped v2 mounts. */
2052
+ skippedV2?: boolean;
2053
+ /** When false, return an unstarted relay (local CP calls start). */
2054
+ start?: boolean;
2055
+ /** Offset dir under stateRoot. Defaults to `cursor-events`. */
2056
+ stateDirName?: string;
2057
+ }): CursorEventRelay | undefined {
2058
+ const { config, stateRoot, mounts, auth, logger } = input;
2059
+ if (config === undefined || auth === undefined) {
2060
+ return undefined;
2061
+ }
2062
+ const subscriptions = collectCursorEventSubscriptions({
2063
+ mounts,
2064
+ githubRepos: input.githubRepos,
2065
+ originRepos: input.originRepos,
2066
+ });
1931
2067
  if (subscriptions.length === 0) {
1932
- if (githubRepos.length === 0 && originRepos.length === 0) {
2068
+ if (input.lane === "engine" && input.skippedV2 === true) {
2069
+ logger(
2070
+ "[agent-sdk] cursor-events: no v1 github/origin targets; v2 ingest is control-plane only"
2071
+ );
2072
+ } else if (input.lane === "control-plane") {
2073
+ return undefined;
2074
+ } else if (
2075
+ input.githubRepos.every((repo) => repo.trim() === "") &&
2076
+ input.originRepos.every((repo) => repo.trim() === "")
2077
+ ) {
1933
2078
  logger(
1934
2079
  "[agent-sdk] cursor-events: no repos configured (pass --repo owner/name); relay disabled"
1935
2080
  );
@@ -1947,7 +2092,7 @@ function startCursorEventRelay(input: {
1947
2092
  baseUrl: auth.baseUrl,
1948
2093
  apiKey: auth.apiKey,
1949
2094
  subscriptions,
1950
- stateDir: join(baseStateRoot, "cursor-events"),
2095
+ stateDir: join(baseStateRoot, input.stateDirName ?? "cursor-events"),
1951
2096
  logger,
1952
2097
  dispatch: async (event) => {
1953
2098
  if (event.provider === "origin") {
@@ -1970,6 +2115,9 @@ function startCursorEventRelay(input: {
1970
2115
  type: "origin",
1971
2116
  bodyText: JSON.stringify(scmMetadataToOriginPayload(event)),
1972
2117
  targets,
2118
+ ...(input.routeContinuation === undefined
2119
+ ? {}
2120
+ : { routeContinuation: input.routeContinuation }),
1973
2121
  });
1974
2122
  logCursorScmDispatch(logger, event, result);
1975
2123
  return;
@@ -1996,10 +2144,16 @@ function startCursorEventRelay(input: {
1996
2144
  eventName: event.eventName,
1997
2145
  deliveryId: event.deliveryId,
1998
2146
  targets,
2147
+ ...(input.routeContinuation === undefined
2148
+ ? {}
2149
+ : { routeContinuation: input.routeContinuation }),
1999
2150
  });
2000
2151
  logCursorScmDispatch(logger, event, result);
2001
2152
  },
2002
2153
  });
2154
+ if (input.start === false) {
2155
+ return relay;
2156
+ }
2003
2157
  relay.start();
2004
2158
  logger(
2005
2159
  `[agent-sdk] cursor-events: relaying ${subscriptions.length} subscription(s) from ${auth.baseUrl}`
@@ -2291,7 +2445,7 @@ function collectCursorDeploymentTargets(
2291
2445
  }
2292
2446
  const subscriptions = resolveDeploymentSubscriptions(options);
2293
2447
  if (subscriptions.length === 0) {
2294
- // Declared the channel but named no repositories — usually an unset
2448
+ // Declared the channel but named no deploy sources — usually an unset
2295
2449
  // env var. Say so rather than starting a relay that receives nothing.
2296
2450
  skipped.push(`${runtime.slug || runtime.project.name}/${channel.id}`);
2297
2451
  continue;
@@ -2305,7 +2459,7 @@ function collectCursorDeploymentTargets(
2305
2459
  /**
2306
2460
  * Cursor deployment-events relay: automatic when a mounted agent authored
2307
2461
  * `deploymentsChannel({ ... })`. One relay per process reads
2308
- * `/v0/deployment-events` for the union of every channel's repositories, and
2462
+ * `/v0/deployment-events` for the union of every channel's deploy sources, and
2309
2463
  * each event is routed to the channels that asked for it — the same ownership
2310
2464
  * `startCursorSlackRelay` and `startCursorEventRelay` have.
2311
2465
  */
@@ -2320,7 +2474,7 @@ function startCursorDeploymentRelay(input: {
2320
2474
  const targets = collectCursorDeploymentTargets(runtimes, skipped);
2321
2475
  if (skipped.length > 0) {
2322
2476
  logger(
2323
- `[agent-sdk] deployment-events: no repositories declared on ${skipped.join(", ")}; those channels receive nothing`
2477
+ `[agent-sdk] deployment-events: no deploy sources declared on ${skipped.join(", ")}; those channels receive nothing`
2324
2478
  );
2325
2479
  }
2326
2480
  if (targets.length === 0) {
@@ -2328,19 +2482,19 @@ function startCursorDeploymentRelay(input: {
2328
2482
  }
2329
2483
  if (accountKey === undefined) {
2330
2484
  throw new Error(
2331
- `This project has channels subscribed to deployment events (deploymentsChannel({ repos })) but the host is not signed in to Cursor. ${SIGN_IN_HINT}`
2485
+ `This project has channels subscribed to deployment events (deploymentsChannel({ deploySourceUris })) but the host is not signed in to Cursor. ${SIGN_IN_HINT}`
2332
2486
  );
2333
2487
  }
2334
- // Union across channels: one registration per repo for the whole process,
2335
- // however many channels named it. Per-channel environment and event-type
2336
- // filtering happens at dispatch.
2337
- const byRepo = new Map<string, ResolvedSubscription>();
2488
+ // Union across channels: one registration per deploy source for the whole
2489
+ // process, however many channels named it. Per-channel environment and
2490
+ // event-type filtering happens at dispatch.
2491
+ const bySource = new Map<string, ResolvedSubscription>();
2338
2492
  for (const target of targets) {
2339
2493
  for (const subscription of target.subscriptions) {
2340
- const existing = byRepo.get(subscription.repo);
2494
+ const existing = bySource.get(subscription.deploySourceUri);
2341
2495
  if (existing === undefined) {
2342
- byRepo.set(subscription.repo, {
2343
- repo: subscription.repo,
2496
+ bySource.set(subscription.deploySourceUri, {
2497
+ deploySourceUri: subscription.deploySourceUri,
2344
2498
  environments: [...subscription.environments],
2345
2499
  events: new Set(subscription.events),
2346
2500
  });
@@ -2369,7 +2523,7 @@ function startCursorDeploymentRelay(input: {
2369
2523
  baseUrl: cursorExternalApiUrl(),
2370
2524
  apiKey: accountKey.apiKey,
2371
2525
  stateDir: join(baseStateRoot, "deployment-events"),
2372
- subscriptions: [...byRepo.values()],
2526
+ subscriptions: [...bySource.values()],
2373
2527
  logger,
2374
2528
  dispatch: async (event) =>
2375
2529
  fanOutDeploymentEvent({
@@ -2392,7 +2546,7 @@ function startCursorDeploymentRelay(input: {
2392
2546
  });
2393
2547
  relay.start();
2394
2548
  logger(
2395
- `[agent-sdk] deployment-events: relaying ${byRepo.size} repository subscription(s) to ${targets.length} channel(s)`
2549
+ `[agent-sdk] deployment-events: relaying ${bySource.size} deploy-source subscription(s) to ${targets.length} channel(s)`
2396
2550
  );
2397
2551
  return relay;
2398
2552
  }
package/src/types.ts CHANGED
@@ -1689,8 +1689,8 @@ export interface SendMessageOptions {
1689
1689
  */
1690
1690
  continuationToken?: string;
1691
1691
  /**
1692
- * v2 mailbox stamp from the control plane (or the pack's builtin extract
1693
- * on local serve). Ignored on v1.
1692
+ * v2 mailbox stamp from the control plane (hosted verified ingress or
1693
+ * the local CP). Channel adapters do not extract. Ignored on v1.
1694
1694
  */
1695
1695
  mailboxContinuation?: MailboxContinuation;
1696
1696
  /**