@cursor/july 0.1.85 → 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 (36) hide show
  1. package/dist/channels/github/github-channel.d.ts +4 -0
  2. package/dist/channels/github/github-channel.d.ts.map +1 -1
  3. package/dist/channels/github/github-channel.js +18 -11
  4. package/dist/channels/origin/origin-channel.d.ts +6 -0
  5. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  6. package/dist/channels/origin/origin-channel.js +24 -11
  7. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  8. package/dist/channels/slack/dispatch.js +12 -2
  9. package/dist/internal/continuation-identity.d.ts +10 -0
  10. package/dist/internal/continuation-identity.d.ts.map +1 -1
  11. package/dist/internal/continuation-identity.js +10 -0
  12. package/dist/internal/handleAgentServeTrigger.d.ts +5 -0
  13. package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
  14. package/dist/internal/handleAgentServeTrigger.js +11 -3
  15. package/dist/internal/local-control-plane.d.ts +19 -0
  16. package/dist/internal/local-control-plane.d.ts.map +1 -1
  17. package/dist/internal/local-control-plane.js +17 -0
  18. package/dist/internal/local-ingress.d.ts +35 -0
  19. package/dist/internal/local-ingress.d.ts.map +1 -0
  20. package/dist/internal/local-ingress.js +44 -0
  21. package/dist/internal/server.d.ts.map +1 -1
  22. package/dist/internal/server.js +138 -62
  23. package/dist/playground/assets/{index-CMe7imuc.css → index-C0_5hOsf.css} +1 -1
  24. package/dist/playground/index.html +2 -2
  25. package/dist/types.d.ts +2 -2
  26. package/package.json +1 -1
  27. package/src/channels/github/github-channel.ts +18 -21
  28. package/src/channels/origin/origin-channel.ts +26 -15
  29. package/src/channels/slack/dispatch.ts +11 -7
  30. package/src/internal/continuation-identity.ts +10 -0
  31. package/src/internal/handleAgentServeTrigger.ts +31 -2
  32. package/src/internal/local-control-plane.ts +33 -0
  33. package/src/internal/local-ingress.ts +90 -0
  34. package/src/internal/server.ts +181 -27
  35. package/src/types.ts +2 -2
  36. /package/dist/playground/assets/{index-JaFLlugp.js → index-Bqn91tW4.js} +0 -0
@@ -8,8 +8,8 @@
8
8
  />
9
9
  <meta name="viewport" content="width=device-width, initial-scale=1" />
10
10
  <title>agent-serve playground</title>
11
- <script type="module" crossorigin src="./assets/index-JaFLlugp.js"></script>
12
- <link rel="stylesheet" crossorigin href="./assets/index-CMe7imuc.css">
11
+ <script type="module" crossorigin src="./assets/index-Bqn91tW4.js"></script>
12
+ <link rel="stylesheet" crossorigin href="./assets/index-C0_5hOsf.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
package/dist/types.d.ts CHANGED
@@ -1463,8 +1463,8 @@ export interface SendMessageOptions {
1463
1463
  */
1464
1464
  continuationToken?: string;
1465
1465
  /**
1466
- * v2 mailbox stamp from the control plane (or the pack's builtin extract
1467
- * on local serve). Ignored on v1.
1466
+ * v2 mailbox stamp from the control plane (hosted verified ingress or
1467
+ * the local CP). Channel adapters do not extract. Ignored on v1.
1468
1468
  */
1469
1469
  mailboxContinuation?: MailboxContinuation;
1470
1470
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cursor/july",
3
- "version": "0.1.85",
3
+ "version": "0.1.86",
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": {
@@ -8,12 +8,11 @@ import {
8
8
  POST,
9
9
  } from "../../channels.js";
10
10
  import {
11
- bindMailboxContinuationSend,
12
11
  continuationSource,
13
12
  normalizeIdentity,
14
- stampMailboxContinuation,
15
13
  } from "../../internal/continuation-identity.js";
16
14
  import { isDevMode } from "../../internal/dev-mode.js";
15
+ import { bindLocalIngressSend } from "../../internal/local-ingress.js";
17
16
  import type {
18
17
  AgentCloudOptions,
19
18
  ArtifactsApi,
@@ -477,6 +476,10 @@ function verifyWebhookSha256Signature(args: {
477
476
  * {@link handleAgentServeTrigger} fan-out. Signature verification is the
478
477
  * caller's responsibility (HTTP route verifies; internal fan-out skips).
479
478
  */
479
+ /**
480
+ * Run authored GitHub hooks and `send()`. Does not extract a v2 mailbox —
481
+ * the local CP (or hosted verified ingress) stamps before this runs.
482
+ */
480
483
  export async function dispatchGitHubWebhook(input: {
481
484
  options: GitHubChannelOptions;
482
485
  eventName: string | null;
@@ -484,23 +487,7 @@ export async function dispatchGitHubWebhook(input: {
484
487
  deliveryId?: string;
485
488
  args: ChannelHandlerArgs;
486
489
  }): Promise<Response> {
487
- const identity = normalizeIdentity(input.options.continuation, "github");
488
- let args = input.args;
489
- if (
490
- continuationSource(identity, "github") === "control-plane" &&
491
- input.eventName !== null
492
- ) {
493
- const mailbox = stampMailboxContinuation(
494
- identity,
495
- input.eventName,
496
- input.payload
497
- );
498
- args = {
499
- ...input.args,
500
- send: bindMailboxContinuationSend(input.args.send, mailbox),
501
- };
502
- }
503
- return dispatchGitHubWebhookInner({ ...input, args });
490
+ return dispatchGitHubWebhookInner(input);
504
491
  }
505
492
 
506
493
  async function dispatchGitHubWebhookInner(input: {
@@ -644,12 +631,22 @@ export async function handleGitHubWebhookRequest(input: {
644
631
  return Response.json({ ok: false, error: "invalid_json" }, { status: 400 });
645
632
  }
646
633
 
634
+ const eventName = input.request.headers.get("x-github-event");
635
+ const args =
636
+ eventName === null
637
+ ? input.args
638
+ : bindLocalIngressSend(input.args, {
639
+ identity: input.options.continuation,
640
+ kind: "github",
641
+ eventType: eventName,
642
+ payload,
643
+ });
647
644
  return dispatchGitHubWebhook({
648
645
  options: input.options,
649
- eventName: input.request.headers.get("x-github-event"),
646
+ eventName,
650
647
  payload,
651
648
  deliveryId: input.request.headers.get("x-github-delivery") ?? undefined,
652
- args: input.args,
649
+ args,
653
650
  });
654
651
  }
655
652
 
@@ -7,12 +7,11 @@ import {
7
7
  POST,
8
8
  } from "../../channels.js";
9
9
  import {
10
- bindMailboxContinuationSend,
11
10
  continuationSource,
12
11
  normalizeIdentity,
13
- stampMailboxContinuation,
14
12
  } from "../../internal/continuation-identity.js";
15
13
  import { isDevMode } from "../../internal/dev-mode.js";
14
+ import { bindLocalIngressSend } from "../../internal/local-ingress.js";
16
15
  import type {
17
16
  AgentCloudOptions,
18
17
  AuthContext,
@@ -364,6 +363,20 @@ export function originContinuationKey(
364
363
  return `origin-pr:${ctx.repository.fullName}#${ctx.pullRequestNumber}`;
365
364
  }
366
365
 
366
+ /** Payload shape local/hosted CP extract expects (`fullName` + number). */
367
+ export function originIngressExtractPayload(
368
+ envelope: OriginWebhookEnvelope
369
+ ): unknown {
370
+ const parsed = parseOriginPullRequestEvent({ envelope });
371
+ if (parsed === null) {
372
+ return envelope.event.payload;
373
+ }
374
+ return {
375
+ pullRequest: { number: parsed.ctx.pullRequestNumber },
376
+ repository: { fullName: parsed.ctx.repository.fullName },
377
+ };
378
+ }
379
+
367
380
  export function defaultOriginTitle(
368
381
  ctx: Pick<OriginEventContext, "repository" | "pullRequestNumber">
369
382
  ): string {
@@ -532,6 +545,10 @@ async function applyDispatch(input: {
532
545
  * {@link handleAgentServeTrigger} fan-out. Signature verification is the
533
546
  * caller's responsibility (HTTP route verifies; internal fan-out skips).
534
547
  */
548
+ /**
549
+ * Run authored Origin hooks and `send()`. Does not extract a v2 mailbox —
550
+ * the local CP (or hosted verified ingress) stamps before this runs.
551
+ */
535
552
  export async function dispatchOriginWebhook(input: {
536
553
  options: OriginChannelOptions;
537
554
  envelope: OriginWebhookEnvelope;
@@ -545,18 +562,7 @@ export async function dispatchOriginWebhook(input: {
545
562
  { status: 400 }
546
563
  );
547
564
  }
548
- const identity = normalizeIdentity(input.options.continuation, "origin");
549
- let args = input.args;
550
- if (continuationSource(identity, "origin") === "control-plane") {
551
- const mailbox = stampMailboxContinuation(identity, eventType, {
552
- pullRequest: { number: parsed.ctx.pullRequestNumber },
553
- repository: { fullName: parsed.ctx.repository.fullName },
554
- });
555
- args = {
556
- ...input.args,
557
- send: bindMailboxContinuationSend(input.args.send, mailbox),
558
- };
559
- }
565
+ const args = input.args;
560
566
  const ctx = enrichEventContext({
561
567
  ctx: parsed.ctx,
562
568
  args,
@@ -711,7 +717,12 @@ export async function handleOriginWebhookRequest(input: {
711
717
  return dispatchOriginWebhook({
712
718
  options: input.options,
713
719
  envelope,
714
- args: input.args,
720
+ args: bindLocalIngressSend(input.args, {
721
+ identity: input.options.continuation,
722
+ kind: "origin",
723
+ eventType: envelope.event.type,
724
+ payload: originIngressExtractPayload(envelope),
725
+ }),
715
726
  });
716
727
  }
717
728
 
@@ -7,8 +7,8 @@ import {
7
7
  continuationSource,
8
8
  type MailboxContinuation,
9
9
  normalizeIdentity,
10
- stampMailboxContinuation,
11
10
  } from "../../internal/continuation-identity.js";
11
+ import { routeLocalIngressContinuation } from "../../internal/local-ingress.js";
12
12
  import type {
13
13
  ArtifactsApi,
14
14
  HostContext,
@@ -208,12 +208,16 @@ function slackSessionContinuation(
208
208
  const key = slackContinuationKey(channelId, threadTs);
209
209
  return { probeKey: key, sendPatch: { continuationToken: key } };
210
210
  }
211
- const mailbox = stampMailboxContinuation(
212
- normalizeIdentity(identity, "slack"),
213
- "message",
214
- { channelId, threadTs },
215
- "slack"
216
- );
211
+ const routed = routeLocalIngressContinuation({
212
+ identity: normalizeIdentity(identity, "slack"),
213
+ kind: "slack",
214
+ eventType: "message",
215
+ payload: { channelId, threadTs },
216
+ });
217
+ const mailbox = routed.mailboxContinuation;
218
+ if (mailbox === undefined) {
219
+ throw new Error("v2 slack ingress did not stamp a mailbox");
220
+ }
217
221
  return { probeKey: mailbox.key, sendPatch: { mailboxContinuation: mailbox } };
218
222
  }
219
223
 
@@ -361,6 +361,16 @@ export function extractContinuation(
361
361
  }
362
362
  }
363
363
 
364
+ /**
365
+ * Extract primitive used by the control plane (hosted
366
+ * `routeVerifiedIngressContinuation` and local
367
+ * `routeLocalIngressContinuation`). Channel adapters must not call this.
368
+ */
369
+ /**
370
+ * Extract primitive used by the control plane (hosted
371
+ * `routeVerifiedIngressContinuation` and local
372
+ * `routeLocalIngressContinuation`). Channel adapters must not call this.
373
+ */
364
374
  export function stampMailboxContinuation(
365
375
  identity: ContinuationIdentity,
366
376
  eventName: string,
@@ -19,10 +19,15 @@ import type { GitHubChannelOptions } from "../channels/github/types.js";
19
19
  import {
20
20
  dispatchOriginWebhook,
21
21
  getOriginChannelOptions,
22
+ originIngressExtractPayload,
22
23
  parseOriginWebhookEnvelope,
23
24
  } from "../channels/origin/origin-channel.js";
24
25
  import type { OriginChannelOptions } from "../channels/origin/types.js";
25
26
  import type { ChannelHandlerArgs } from "../types.js";
27
+ import {
28
+ bindLocalIngressSend,
29
+ type RouteLocalIngressContinuation,
30
+ } from "./local-ingress.js";
26
31
 
27
32
  export interface AgentServeGitHubTriggerTarget {
28
33
  /** Agent mount slug (empty string in single-agent mode). */
@@ -40,6 +45,8 @@ export interface AgentServeGitHubTriggerEvent {
40
45
  eventName: string | null;
41
46
  deliveryId?: string;
42
47
  targets: AgentServeGitHubTriggerTarget[];
48
+ /** Local CP ingest. Defaults to {@link routeLocalIngressContinuation}. */
49
+ routeContinuation?: RouteLocalIngressContinuation;
43
50
  }
44
51
 
45
52
  export interface AgentServeOriginTriggerTarget {
@@ -53,6 +60,8 @@ export interface AgentServeOriginTriggerEvent {
53
60
  /** Origin envelope JSON (signature already verified by the caller). */
54
61
  bodyText: string;
55
62
  targets: AgentServeOriginTriggerTarget[];
63
+ /** Local CP ingest. Defaults to {@link routeLocalIngressContinuation}. */
64
+ routeContinuation?: RouteLocalIngressContinuation;
56
65
  }
57
66
 
58
67
  export type AgentServeTriggerEvent =
@@ -177,12 +186,24 @@ async function handleAgentServeGitHubTrigger(
177
186
 
178
187
  const targets: AgentServeTriggerTargetResult[] = [];
179
188
  for (const target of event.targets) {
189
+ const args =
190
+ event.eventName === null
191
+ ? target.args
192
+ : bindLocalIngressSend(target.args, {
193
+ identity: target.options.continuation,
194
+ kind: "github",
195
+ eventType: event.eventName,
196
+ payload,
197
+ ...(event.routeContinuation === undefined
198
+ ? {}
199
+ : { routeContinuation: event.routeContinuation }),
200
+ });
180
201
  const response = await dispatchGitHubWebhook({
181
202
  options: target.options,
182
203
  eventName: event.eventName,
183
204
  payload,
184
205
  deliveryId: event.deliveryId,
185
- args: target.args,
206
+ args,
186
207
  });
187
208
  targets.push(await triggerTargetResult(target.slug, response));
188
209
  }
@@ -234,7 +255,15 @@ async function handleAgentServeOriginTrigger(
234
255
  const response = await dispatchOriginWebhook({
235
256
  options: target.options,
236
257
  envelope,
237
- args: target.args,
258
+ args: bindLocalIngressSend(target.args, {
259
+ identity: target.options.continuation,
260
+ kind: "origin",
261
+ eventType: envelope.event.type,
262
+ payload: originIngressExtractPayload(envelope),
263
+ ...(event.routeContinuation === undefined
264
+ ? {}
265
+ : { routeContinuation: event.routeContinuation }),
266
+ }),
238
267
  });
239
268
  targets.push(await triggerTargetResult(target.slug, response));
240
269
  }
@@ -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
+ }