@cursor/july 0.1.39 → 0.1.40

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 (79) hide show
  1. package/dist/channels/slack/api.d.ts.map +1 -1
  2. package/dist/channels/slack/api.js +18 -8
  3. package/dist/channels/slack/defaults.d.ts.map +1 -1
  4. package/dist/channels/slack/defaults.js +26 -17
  5. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  6. package/dist/channels/slack/live-delivery.js +16 -4
  7. package/dist/docs/404.html +1 -1
  8. package/dist/docs/ab.html +2 -2
  9. package/dist/docs/assets/{app.BQ8Hihdf.js → app.DpGLSiym.js} +1 -1
  10. package/dist/docs/assets/chunks/@localSearchIndexroot.D82_RVtF.js +1 -0
  11. package/dist/docs/assets/chunks/{VPLocalSearchBox.BBCr8Yuy.js → VPLocalSearchBox.B11XEMST.js} +1 -1
  12. package/dist/docs/assets/chunks/{theme.CK_NiGC-.js → theme.iTidHsAx.js} +2 -2
  13. package/dist/docs/building-with-agents.html +2 -2
  14. package/dist/docs/concepts.html +2 -2
  15. package/dist/docs/deployment.html +2 -2
  16. package/dist/docs/evals.html +2 -2
  17. package/dist/docs/example-agents/approval-buddy.html +2 -2
  18. package/dist/docs/example-agents/benny.html +2 -2
  19. package/dist/docs/example-agents/bugbot.html +2 -2
  20. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  21. package/dist/docs/example-agents/codeowners-review.html +2 -2
  22. package/dist/docs/example-agents/concierge.html +2 -2
  23. package/dist/docs/example-agents/fsd.html +2 -2
  24. package/dist/docs/example-agents/index.html +2 -2
  25. package/dist/docs/example-agents/knowledge-base.html +2 -2
  26. package/dist/docs/example-agents/oncall.html +2 -2
  27. package/dist/docs/example-agents/security-reviewer.html +2 -2
  28. package/dist/docs/example-agents/slack-agent.html +2 -2
  29. package/dist/docs/example-agents/weather-agent.html +2 -2
  30. package/dist/docs/guides/agent-to-agent.html +2 -2
  31. package/dist/docs/guides/cloud-runtime.html +2 -2
  32. package/dist/docs/guides/github.html +2 -2
  33. package/dist/docs/guides/human-in-the-loop.html +2 -2
  34. package/dist/docs/guides/mcp-oauth.html +2 -2
  35. package/dist/docs/guides/slack.html +2 -2
  36. package/dist/docs/guides/webhooks.html +2 -2
  37. package/dist/docs/hillclimbing.html +2 -2
  38. package/dist/docs/index.html +2 -2
  39. package/dist/docs/quickstart.html +2 -2
  40. package/dist/docs/reference/agent-config.html +2 -2
  41. package/dist/docs/reference/artifacts.html +2 -2
  42. package/dist/docs/reference/channels.html +2 -2
  43. package/dist/docs/reference/cli.html +2 -2
  44. package/dist/docs/reference/connections.html +2 -2
  45. package/dist/docs/reference/hooks.html +2 -2
  46. package/dist/docs/reference/http-api.html +2 -2
  47. package/dist/docs/reference/instructions.html +2 -2
  48. package/dist/docs/reference/playground.html +2 -2
  49. package/dist/docs/reference/project-layout.html +2 -2
  50. package/dist/docs/reference/prompt.html +2 -2
  51. package/dist/docs/reference/schedules.html +2 -2
  52. package/dist/docs/reference/sessions.html +2 -2
  53. package/dist/docs/reference/skills.html +2 -2
  54. package/dist/docs/reference/subagents.html +2 -2
  55. package/dist/docs/reference/tools.html +2 -2
  56. package/dist/docs/scaffolding-agents.html +2 -2
  57. package/dist/docs/storage.html +2 -2
  58. package/dist/docs/troubleshooting.html +2 -2
  59. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
  60. package/dist/internal/cli-mcp-oauth.js +3 -1
  61. package/dist/internal/cursor-event-relay.d.ts +45 -0
  62. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  63. package/dist/internal/cursor-event-relay.js +96 -2
  64. package/dist/internal/mcp-oauth.d.ts +12 -1
  65. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  66. package/dist/internal/mcp-oauth.js +25 -3
  67. package/dist/internal/server.d.ts.map +1 -1
  68. package/dist/internal/server.js +6 -2
  69. package/dist/playground/assets/{index-_2WKw9a-.js → index-Bc1oFgFG.js} +49 -49
  70. package/dist/playground/index.html +1 -1
  71. package/package.json +1 -1
  72. package/src/channels/slack/api.ts +19 -14
  73. package/src/channels/slack/defaults.ts +34 -18
  74. package/src/channels/slack/live-delivery.ts +17 -3
  75. package/src/internal/cli-mcp-oauth.ts +3 -1
  76. package/src/internal/cursor-event-relay.ts +168 -0
  77. package/src/internal/mcp-oauth.ts +29 -4
  78. package/src/internal/server.ts +8 -1
  79. package/dist/docs/assets/chunks/@localSearchIndexroot.B-VTH4As.js +0 -1
@@ -8,7 +8,7 @@
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-_2WKw9a-.js"></script>
11
+ <script type="module" crossorigin src="./assets/index-Bc1oFgFG.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="./assets/index-BRhBbnd4.css">
13
13
  </head>
14
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cursor/july",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
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": {
@@ -204,25 +204,30 @@ export async function setThreadStatus(args: {
204
204
  return { ok: false, error: "missing_channel_or_thread" };
205
205
  }
206
206
  const status = truncateTypingStatus(args.status);
207
- const loadingRaw =
208
- args.loadingMessages !== undefined && args.loadingMessages.length > 0
209
- ? args.loadingMessages
210
- : status.length > 0
211
- ? [
207
+ const body: Record<string, unknown> = {
208
+ channel_id: args.channelId,
209
+ thread_ts: args.threadTs,
210
+ status,
211
+ };
212
+ // Empty status clears Slack's assistant chip ("App is working…").
213
+ // Sending loading_messages with it keeps the chip rotating.
214
+ if (status.length > 0) {
215
+ const loadingRaw =
216
+ args.loadingMessages !== undefined && args.loadingMessages.length > 0
217
+ ? args.loadingMessages
218
+ : [
212
219
  status,
213
220
  ...SLACK_DEFAULT_LOADING_MESSAGES.filter((m) => m !== status),
214
- ]
215
- : [];
216
- const loading_messages = truncateLoadingMessages(loadingRaw);
221
+ ];
222
+ const loading_messages = truncateLoadingMessages(loadingRaw);
223
+ if (loading_messages.length > 0) {
224
+ body.loading_messages = loading_messages;
225
+ }
226
+ }
217
227
  return callSlackApiSoft({
218
228
  botToken: args.botToken,
219
229
  operation: "assistant.threads.setStatus",
220
- body: {
221
- channel_id: args.channelId,
222
- thread_ts: args.threadTs,
223
- status,
224
- ...(loading_messages.length > 0 ? { loading_messages } : {}),
225
- },
230
+ body,
226
231
  });
227
232
  }
228
233
 
@@ -155,6 +155,25 @@ export function buildDefaultEvents(
155
155
  const getLog = options.getLog ?? (() => defaultSlackLog);
156
156
  const strategy = createStreamDeliveryStrategy({ botToken, getLog });
157
157
 
158
+ const clearWorkingStatus = async (
159
+ channelId: string,
160
+ threadTs: string
161
+ ): Promise<void> => {
162
+ const result = await setThreadStatus({
163
+ botToken,
164
+ channelId,
165
+ threadTs,
166
+ status: "",
167
+ });
168
+ if (!result.ok) {
169
+ getLog().warn("assistant.threads.setStatus not-ok", {
170
+ error: result.error,
171
+ channel: channelId,
172
+ thread_ts: threadTs,
173
+ });
174
+ }
175
+ };
176
+
158
177
  return {
159
178
  async "turn.started"(event, channel) {
160
179
  const log = getLog();
@@ -366,15 +385,6 @@ export function buildDefaultEvents(
366
385
  return;
367
386
  }
368
387
 
369
- const clearStatus = async (): Promise<void> => {
370
- await setThreadStatus({
371
- botToken,
372
- channelId: state.channelId!,
373
- threadTs: state.threadTs!,
374
- status: "",
375
- });
376
- };
377
-
378
388
  // Empty final text: still close any open stream / clear the skeleton
379
389
  // and clear typing.
380
390
  if (event.data.text === "") {
@@ -386,7 +396,7 @@ export function buildDefaultEvents(
386
396
  });
387
397
  channel.state.streamTs = null;
388
398
  }
389
- await clearStatus();
399
+ await clearWorkingStatus(state.channelId, state.threadTs);
390
400
  return;
391
401
  }
392
402
 
@@ -405,7 +415,7 @@ export function buildDefaultEvents(
405
415
  sessionId: event.sessionId,
406
416
  });
407
417
  if (delivered) {
408
- await clearStatus();
418
+ await clearWorkingStatus(state.channelId, state.threadTs);
409
419
  return;
410
420
  }
411
421
  }
@@ -439,7 +449,7 @@ export function buildDefaultEvents(
439
449
  );
440
450
  }
441
451
 
442
- await clearStatus();
452
+ await clearWorkingStatus(state.channelId, state.threadTs);
443
453
  },
444
454
 
445
455
  async "turn.completed"(event, channel) {
@@ -453,6 +463,17 @@ export function buildDefaultEvents(
453
463
 
454
464
  await strategy.cleanupOrphan({ channel });
455
465
 
466
+ // Always drop the assistant chip. message.completed already clears
467
+ // after a normal reply, but a tool-call-only finish (or a missed
468
+ // final assistant message) never hits that path — leaving
469
+ // "Working…" stuck until Slack expires it.
470
+ if (channel.state.channelId !== null && channel.state.threadTs !== null) {
471
+ await clearWorkingStatus(
472
+ channel.state.channelId,
473
+ channel.state.threadTs
474
+ );
475
+ }
476
+
456
477
  if (
457
478
  suggestedPrompts.length === 0 ||
458
479
  channel.state.channelId === null ||
@@ -518,12 +539,7 @@ export function buildDefaultEvents(
518
539
  );
519
540
  }
520
541
  }
521
- await setThreadStatus({
522
- botToken,
523
- channelId: state.channelId,
524
- threadTs: state.threadTs,
525
- status: "",
526
- });
542
+ await clearWorkingStatus(state.channelId, state.threadTs);
527
543
  },
528
544
  };
529
545
  }
@@ -183,9 +183,23 @@ export function createStreamDeliveryStrategy(
183
183
  await stopChatStream({ botToken, channel: channelId, ts: streamTs });
184
184
  },
185
185
 
186
- async cleanupOrphan() {
187
- // The stream path closes its message in message.completed / turn.failed;
188
- // there is nothing to clean up after turn.completed.
186
+ async cleanupOrphan({ channel }) {
187
+ // message.completed / turn.failed already stop the stream. If the
188
+ // turn ended without a final assistant message, close it here so
189
+ // Slack does not leave the live reply in a streaming state.
190
+ const streamTs = channel.state.streamTs;
191
+ const channelId =
192
+ channel.state.streamChannelId ?? channel.state.channelId;
193
+ if (
194
+ streamTs === null ||
195
+ streamTs === "" ||
196
+ channelId === null ||
197
+ channelId === ""
198
+ ) {
199
+ return;
200
+ }
201
+ await stopChatStream({ botToken, channel: channelId, ts: streamTs });
202
+ channel.state.streamTs = null;
189
203
  },
190
204
 
191
205
  async fail({ channel, channelId, streamTs, errorText }) {
@@ -209,7 +209,9 @@ export async function authorizeHostMcpOauth(
209
209
 
210
210
  let callback: Awaited<ReturnType<typeof waitForMcpOAuthCallback>>;
211
211
  try {
212
- callback = await waitForMcpOAuthCallback();
212
+ callback = await waitForMcpOAuthCallback({
213
+ authorizationUrl: redirect.toString(),
214
+ });
213
215
  } catch (error) {
214
216
  err(`${LOG_PREFIX} OAuth callback failed: ${describe(error)}\n`);
215
217
  return { ok: false };
@@ -57,6 +57,40 @@ export type CursorScmPullRequestMeta = {
57
57
  baseSha?: string;
58
58
  };
59
59
 
60
+ /** PR ref on a Checks / Actions webhook object. */
61
+ export type CursorScmCiPullRequestMeta = {
62
+ number: number;
63
+ url?: string;
64
+ };
65
+
66
+ export type CursorScmCiAppMeta = {
67
+ slug?: string;
68
+ name?: string;
69
+ };
70
+
71
+ /** Shared Checks / Actions fields. `id` + `headSha` satisfy the channel Zod schemas. */
72
+ type CursorScmCiBaseMeta = {
73
+ id: number;
74
+ headSha: string;
75
+ status?: string;
76
+ conclusion?: string | null;
77
+ htmlUrl?: string;
78
+ app?: CursorScmCiAppMeta;
79
+ pullRequests: CursorScmCiPullRequestMeta[];
80
+ };
81
+
82
+ export type CursorScmCheckRunMeta = CursorScmCiBaseMeta & {
83
+ name?: string;
84
+ checkSuiteId?: number;
85
+ };
86
+
87
+ export type CursorScmCheckSuiteMeta = CursorScmCiBaseMeta;
88
+
89
+ export type CursorScmWorkflowRunMeta = CursorScmCiBaseMeta & {
90
+ name?: string;
91
+ workflowId?: number;
92
+ };
93
+
60
94
  /** One event delivered by `GET /v0/scm-events` (metadata only). */
61
95
  export interface CursorScmEvent {
62
96
  offset: string;
@@ -81,6 +115,12 @@ export interface CursorScmEvent {
81
115
  * Zod-valid `issue_comment` / `pull_request_review_comment` webhook body.
82
116
  */
83
117
  comment?: CursorScmCommentMeta;
118
+ /** `check_run` snapshot; enough to stub `onCheckRun`. */
119
+ checkRun?: CursorScmCheckRunMeta;
120
+ /** `check_suite` snapshot; enough to stub `onCheckSuite`. */
121
+ checkSuite?: CursorScmCheckSuiteMeta;
122
+ /** `workflow_run` snapshot; enough to stub `onWorkflowRun`. */
123
+ workflowRun?: CursorScmWorkflowRunMeta;
84
124
  /** GitHub-side event time from the payload, when present. */
85
125
  occurredAtMs?: number;
86
126
  }
@@ -138,12 +178,37 @@ export function formatCursorScmEventLog(event: {
138
178
  return `${event.eventName}${action} on ${pr} received=${received}${occurred} (${delivery})`;
139
179
  }
140
180
 
181
+ /** Relay log suffix for one fan-out target, including parse errors on 4xx. */
182
+ export function formatCursorScmDispatchOutcome(target: {
183
+ slug: string;
184
+ status: number;
185
+ body: unknown;
186
+ }): string {
187
+ const slug = target.slug === "" ? "default" : target.slug;
188
+ if (target.status < 400) {
189
+ return `${slug}:${target.status}`;
190
+ }
191
+ const error = readDispatchError(target.body);
192
+ return error === undefined
193
+ ? `${slug}:${target.status}`
194
+ : `${slug}:${target.status} ${error}`;
195
+ }
196
+
197
+ function readDispatchError(body: unknown): string | undefined {
198
+ if (typeof body !== "object" || body === null) return undefined;
199
+ if (!("error" in body)) return undefined;
200
+ const error = body.error;
201
+ return typeof error === "string" && error !== "" ? error : undefined;
202
+ }
203
+
141
204
  /**
142
205
  * Minimal GitHub webhook body from an SCM-events metadata envelope.
143
206
  * When `pullRequest` has title + head/base refs+shas, emits a Zod-valid
144
207
  * `pull_request` object for `onPullRequest`; otherwise identity-only stubs.
145
208
  * When `comment` is present, emits a Zod-valid `comment` object so relayed
146
209
  * `issue_comment` / `pull_request_review_comment` deliveries dispatch.
210
+ * When a CI snapshot is present, emits `check_run` / `check_suite` /
211
+ * `workflow_run` so those typed hooks parse on the `--cursor-events` path.
147
212
  */
148
213
  export function scmMetadataToGitHubPayload(event: {
149
214
  repo: string;
@@ -152,6 +217,9 @@ export function scmMetadataToGitHubPayload(event: {
152
217
  prUrl?: string;
153
218
  pullRequest?: CursorScmPullRequestMeta;
154
219
  comment?: CursorScmCommentMeta;
220
+ checkRun?: CursorScmCheckRunMeta;
221
+ checkSuite?: CursorScmCheckSuiteMeta;
222
+ workflowRun?: CursorScmWorkflowRunMeta;
155
223
  }): Record<string, unknown> {
156
224
  const slash = event.repo.indexOf("/");
157
225
  const owner = slash === -1 ? "" : event.repo.slice(0, slash);
@@ -210,6 +278,30 @@ export function scmMetadataToGitHubPayload(event: {
210
278
  payload.comment = comment;
211
279
  }
212
280
 
281
+ const checkPullRequests = stubGitHubCiPullRequests({
282
+ snapshot:
283
+ event.checkRun?.pullRequests ??
284
+ event.checkSuite?.pullRequests ??
285
+ event.workflowRun?.pullRequests,
286
+ prNumber,
287
+ prUrl,
288
+ });
289
+ if (event.checkRun !== undefined) {
290
+ payload.check_run = stubGitHubCheckRun(event.checkRun, checkPullRequests);
291
+ }
292
+ if (event.checkSuite !== undefined) {
293
+ payload.check_suite = stubGitHubCheckSuite(
294
+ event.checkSuite,
295
+ checkPullRequests
296
+ );
297
+ }
298
+ if (event.workflowRun !== undefined) {
299
+ payload.workflow_run = stubGitHubWorkflowRun(
300
+ event.workflowRun,
301
+ checkPullRequests
302
+ );
303
+ }
304
+
213
305
  const title = prMeta?.title;
214
306
  const headRef = prMeta?.headRef;
215
307
  const headSha = prMeta?.headSha;
@@ -261,6 +353,82 @@ export function scmMetadataToGitHubPayload(event: {
261
353
  return payload;
262
354
  }
263
355
 
356
+ function stubGitHubCiPullRequests(input: {
357
+ snapshot: CursorScmCiPullRequestMeta[] | undefined;
358
+ prNumber: number | undefined;
359
+ prUrl: string | undefined;
360
+ }): Array<{ number: number; html_url?: string }> {
361
+ if (input.snapshot !== undefined && input.snapshot.length > 0) {
362
+ return input.snapshot.map((pr) => ({
363
+ number: pr.number,
364
+ ...(pr.url === undefined ? {} : { html_url: pr.url }),
365
+ }));
366
+ }
367
+ if (input.prNumber === undefined) return [];
368
+ return [
369
+ {
370
+ number: input.prNumber,
371
+ ...(input.prUrl === undefined ? {} : { html_url: input.prUrl }),
372
+ },
373
+ ];
374
+ }
375
+
376
+ function stubGitHubCiApp(
377
+ app: CursorScmCiAppMeta | undefined
378
+ ): Record<string, unknown> | undefined {
379
+ if (app === undefined) return undefined;
380
+ const out: Record<string, unknown> = {};
381
+ if (app.slug !== undefined) out.slug = app.slug;
382
+ if (app.name !== undefined) out.name = app.name;
383
+ return out;
384
+ }
385
+
386
+ function stubGitHubCiBase(
387
+ meta: CursorScmCiBaseMeta,
388
+ pullRequests: Array<{ number: number; html_url?: string }>
389
+ ): Record<string, unknown> {
390
+ const object: Record<string, unknown> = {
391
+ id: meta.id,
392
+ head_sha: meta.headSha,
393
+ };
394
+ if (meta.status !== undefined) object.status = meta.status;
395
+ if (meta.conclusion !== undefined) object.conclusion = meta.conclusion;
396
+ if (meta.htmlUrl !== undefined) object.html_url = meta.htmlUrl;
397
+ const app = stubGitHubCiApp(meta.app);
398
+ if (app !== undefined) object.app = app;
399
+ if (pullRequests.length > 0) object.pull_requests = pullRequests;
400
+ return object;
401
+ }
402
+
403
+ function stubGitHubCheckRun(
404
+ meta: CursorScmCheckRunMeta,
405
+ pullRequests: Array<{ number: number; html_url?: string }>
406
+ ): Record<string, unknown> {
407
+ const object = stubGitHubCiBase(meta, pullRequests);
408
+ if (meta.name !== undefined) object.name = meta.name;
409
+ if (meta.checkSuiteId !== undefined) {
410
+ object.check_suite = { id: meta.checkSuiteId };
411
+ }
412
+ return object;
413
+ }
414
+
415
+ function stubGitHubCheckSuite(
416
+ meta: CursorScmCheckSuiteMeta,
417
+ pullRequests: Array<{ number: number; html_url?: string }>
418
+ ): Record<string, unknown> {
419
+ return stubGitHubCiBase(meta, pullRequests);
420
+ }
421
+
422
+ function stubGitHubWorkflowRun(
423
+ meta: CursorScmWorkflowRunMeta,
424
+ pullRequests: Array<{ number: number; html_url?: string }>
425
+ ): Record<string, unknown> {
426
+ const object = stubGitHubCiBase(meta, pullRequests);
427
+ if (meta.name !== undefined) object.name = meta.name;
428
+ if (meta.workflowId !== undefined) object.workflow_id = meta.workflowId;
429
+ return object;
430
+ }
431
+
264
432
  /**
265
433
  * GitHub SCM-events relay: a thin {@link CursorRelayCore} adapter. Consumers
266
434
  * keep the same `new CursorEventRelay(options).start()/.stop()` surface.
@@ -385,14 +385,28 @@ export type OAuthCallbackResult = {
385
385
  state?: string;
386
386
  };
387
387
 
388
- export async function waitForMcpOAuthCallback(args?: {
388
+ /**
389
+ * Deliberately not `@anysphere/mcp-core/oauth/loopback`, which is the canonical
390
+ * implementation every other Cursor surface shares. `@cursor/july` publishes to
391
+ * public npm with no private workspace dependencies, and its OAuth state is a
392
+ * bare UUID rather than the payload that module's validator expects.
393
+ *
394
+ * Nothing here reads Statsig, so this listener is outside
395
+ * `unified_mcp_loopback`: turning that gate off restores the other surfaces'
396
+ * original servers but leaves this one, and its state check, in place.
397
+ */
398
+ export async function waitForMcpOAuthCallback(args: {
399
+ authorizationUrl: string;
389
400
  port?: number;
390
401
  path?: string;
391
402
  timeoutMs?: number;
392
403
  }): Promise<OAuthCallbackResult> {
393
- const port = args?.port ?? MCP_OAUTH_CALLBACK_PORT;
394
- const path = args?.path ?? MCP_OAUTH_CALLBACK_PATH;
395
- const timeoutMs = args?.timeoutMs ?? 5 * 60 * 1000;
404
+ const port = args.port ?? MCP_OAUTH_CALLBACK_PORT;
405
+ const path = args.path ?? MCP_OAUTH_CALLBACK_PATH;
406
+ const timeoutMs = args.timeoutMs ?? 5 * 60 * 1000;
407
+ const expectedState = new URL(args.authorizationUrl).searchParams.get(
408
+ "state"
409
+ );
396
410
 
397
411
  return new Promise((resolve, reject) => {
398
412
  let done = false;
@@ -428,6 +442,17 @@ export async function waitForMcpOAuthCallback(args?: {
428
442
  const code = url.searchParams.get("code");
429
443
  const oauthError = url.searchParams.get("error");
430
444
  const state = url.searchParams.get("state") ?? undefined;
445
+ // Without this, any page that can reach the loopback port during a login
446
+ // can inject its own authorization code into the exchange. Ignore what is
447
+ // not ours rather than failing the login: treating a stray request as
448
+ // fatal would let any page cancel a login, and free the port for someone
449
+ // else to bind before the real callback arrives.
450
+ if (state !== (expectedState ?? undefined)) {
451
+ res
452
+ .writeHead(404, { "content-type": "text/html; charset=utf-8" })
453
+ .end("<!doctype html><html><body><h1>Not found</h1></body></html>");
454
+ return;
455
+ }
431
456
  if (code) {
432
457
  res
433
458
  .writeHead(200, { "content-type": "text/html; charset=utf-8" })
@@ -58,6 +58,7 @@ import {
58
58
  import { CursorGithubCredentialProvider } from "./cursor/github-credentials.js";
59
59
  import {
60
60
  CursorEventRelay,
61
+ formatCursorScmDispatchOutcome,
61
62
  formatCursorScmEventLog,
62
63
  scmMetadataToGitHubPayload,
63
64
  } from "./cursor-event-relay.js";
@@ -1796,8 +1797,14 @@ function startCursorEventRelay(input: {
1796
1797
  targets,
1797
1798
  });
1798
1799
  const outcomes = result.targets
1799
- .map((target) => `${target.slug || "default"}:${target.status}`)
1800
+ .map((target) => formatCursorScmDispatchOutcome(target))
1800
1801
  .join(", ");
1802
+ const rejected = result.targets.filter((target) => target.status >= 400);
1803
+ if (rejected.length > 0) {
1804
+ logger(
1805
+ `[agent-sdk] cursor-events: dropped ${formatCursorScmEventLog(event)} before hook: ${rejected.map(formatCursorScmDispatchOutcome).join(", ")}`
1806
+ );
1807
+ }
1801
1808
  logger(
1802
1809
  `[agent-sdk] cursor-events: dispatched ${formatCursorScmEventLog(event)}${outcomes === "" ? "" : ` → ${outcomes}`}`
1803
1810
  );