@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.
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +18 -8
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +26 -17
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +16 -4
- package/dist/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.BQ8Hihdf.js → app.DpGLSiym.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.D82_RVtF.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BBCr8Yuy.js → VPLocalSearchBox.B11XEMST.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CK_NiGC-.js → theme.iTidHsAx.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/example-agents/approval-buddy.html +2 -2
- package/dist/docs/example-agents/benny.html +2 -2
- package/dist/docs/example-agents/bugbot.html +2 -2
- package/dist/docs/example-agents/codebase-wiki.html +2 -2
- package/dist/docs/example-agents/codeowners-review.html +2 -2
- package/dist/docs/example-agents/concierge.html +2 -2
- package/dist/docs/example-agents/fsd.html +2 -2
- package/dist/docs/example-agents/index.html +2 -2
- package/dist/docs/example-agents/knowledge-base.html +2 -2
- package/dist/docs/example-agents/oncall.html +2 -2
- package/dist/docs/example-agents/security-reviewer.html +2 -2
- package/dist/docs/example-agents/slack-agent.html +2 -2
- package/dist/docs/example-agents/weather-agent.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/mcp-oauth.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/artifacts.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/prompt.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
- package/dist/internal/cli-mcp-oauth.js +3 -1
- package/dist/internal/cursor-event-relay.d.ts +45 -0
- package/dist/internal/cursor-event-relay.d.ts.map +1 -1
- package/dist/internal/cursor-event-relay.js +96 -2
- package/dist/internal/mcp-oauth.d.ts +12 -1
- package/dist/internal/mcp-oauth.d.ts.map +1 -1
- package/dist/internal/mcp-oauth.js +25 -3
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +6 -2
- package/dist/playground/assets/{index-_2WKw9a-.js → index-Bc1oFgFG.js} +49 -49
- package/dist/playground/index.html +1 -1
- package/package.json +1 -1
- package/src/channels/slack/api.ts +19 -14
- package/src/channels/slack/defaults.ts +34 -18
- package/src/channels/slack/live-delivery.ts +17 -3
- package/src/internal/cli-mcp-oauth.ts +3 -1
- package/src/internal/cursor-event-relay.ts +168 -0
- package/src/internal/mcp-oauth.ts +29 -4
- package/src/internal/server.ts +8 -1
- 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-
|
|
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.
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
188
|
-
//
|
|
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
|
-
|
|
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
|
|
394
|
-
const path = args
|
|
395
|
-
const timeoutMs = args
|
|
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" })
|
package/src/internal/server.ts
CHANGED
|
@@ -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) =>
|
|
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
|
);
|