@cursor/july 0.1.1 → 0.1.2
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/AGENTS.md +24 -2
- package/README.md +25 -15
- package/dist/bin/agent-serve.js +101 -12
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +31 -14
- package/dist/channels/github/cursor-account.d.ts +43 -0
- package/dist/channels/github/cursor-account.d.ts.map +1 -0
- package/dist/channels/github/cursor-account.js +95 -0
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +46 -9
- package/dist/channels/github/index.d.ts +2 -2
- package/dist/channels/github/index.js +2 -2
- package/dist/channels/github/types.d.ts +17 -0
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.d.ts +8 -2
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +8 -0
- package/dist/channels/slack/types.d.ts +24 -3
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/channels/slack/types.js +15 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +8 -8
- package/dist/docs/assets/{ab.md.COdXkces.js → ab.md.BMCZ6Hd7.js} +3 -3
- package/dist/docs/assets/{ab.md.COdXkces.lean.js → ab.md.BMCZ6Hd7.lean.js} +1 -1
- package/dist/docs/assets/{app.DqfFEmJd.js → app.Oje4vhlk.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.zwQ9RCQ7.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BaLEdS15.js → VPLocalSearchBox.H5XZ2zCB.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CZRvu_0q.js → theme.CTR_TuaE.js} +2 -2
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.js → deployment.md.DTKwE15Z.js} +3 -3
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.lean.js → deployment.md.DTKwE15Z.lean.js} +1 -1
- package/dist/docs/assets/{evals.md.DPZ_MAnI.js → evals.md.DAgEc_hL.js} +3 -3
- package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.js → guides_agent-to-agent.md.Bpzgq2Pq.js} +1 -1
- package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.js → guides_cloud-runtime.md.gVzabdQL.js} +1 -1
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.js → guides_github.md.DOOCpqsW.js} +11 -4
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.lean.js → guides_github.md.DOOCpqsW.lean.js} +1 -1
- package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.js → guides_human-in-the-loop.md.DlUqsp1S.js} +2 -2
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.js → guides_slack.md.CCwqHvSV.js} +4 -4
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.lean.js → guides_slack.md.CCwqHvSV.lean.js} +1 -1
- package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.js → guides_webhooks.md.B1EswtUu.js} +2 -2
- package/dist/docs/assets/index.md.m81y7TY7.js +20 -0
- package/dist/docs/assets/{index.md.BPKcj5AI.lean.js → index.md.m81y7TY7.lean.js} +1 -1
- package/dist/docs/assets/quickstart.md.CfU8_uTC.js +192 -0
- package/dist/docs/assets/quickstart.md.CfU8_uTC.lean.js +1 -0
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.js → reference_agent-config.md.DrW2JUM8.js} +4 -4
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.lean.js → reference_agent-config.md.DrW2JUM8.lean.js} +1 -1
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.js → reference_channels.md.DdmiKgqf.js} +4 -4
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.lean.js → reference_channels.md.DdmiKgqf.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.C3vNH_DE.js → reference_connections.md.zaEYCLHT.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.js → reference_hooks.md.DyLVfE1O.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.lean.js → reference_hooks.md.DyLVfE1O.lean.js} +1 -1
- package/dist/docs/assets/{reference_http-api.md.DBAahtdz.js → reference_http-api.md.Dx_nmDG6.js} +1 -1
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.js → reference_instructions.md.CgoV-YEb.js} +9 -7
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.lean.js → reference_instructions.md.CgoV-YEb.lean.js} +1 -1
- package/dist/docs/assets/{reference_schedules.md.D7qijxLk.js → reference_schedules.md.w_F2mXB6.js} +2 -2
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.js → reference_skills.md.B_jHN7JL.js} +3 -3
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.lean.js → reference_skills.md.B_jHN7JL.lean.js} +1 -1
- package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.js → reference_subagents.md.zWAMNfi1.js} +1 -1
- package/dist/docs/assets/{reference_tools.md.DF5kwlt0.js → reference_tools.md.CqgJroI0.js} +2 -2
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.js +1 -0
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.lean.js +1 -0
- package/dist/docs/assets/storage.md.CVnInNiN.js +17 -0
- package/dist/docs/assets/storage.md.CVnInNiN.lean.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +7 -7
- package/dist/docs/evals.html +7 -7
- package/dist/docs/guides/agent-to-agent.html +6 -6
- package/dist/docs/guides/cloud-runtime.html +5 -5
- package/dist/docs/guides/github.html +14 -7
- package/dist/docs/guides/human-in-the-loop.html +6 -6
- package/dist/docs/guides/slack.html +8 -8
- package/dist/docs/guides/webhooks.html +6 -6
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +5 -5
- package/dist/docs/index.html +7 -7
- package/dist/docs/quickstart.html +180 -23
- package/dist/docs/reference/agent-config.html +7 -7
- package/dist/docs/reference/channels.html +8 -8
- package/dist/docs/reference/cli.html +4 -4
- package/dist/docs/reference/connections.html +5 -5
- package/dist/docs/reference/hooks.html +5 -5
- package/dist/docs/reference/http-api.html +6 -6
- package/dist/docs/reference/instructions.html +13 -11
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/schedules.html +7 -7
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +6 -6
- package/dist/docs/reference/subagents.html +6 -6
- package/dist/docs/reference/tools.html +7 -7
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +41 -0
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/internal/cli-deploy.d.ts +52 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -0
- package/dist/internal/cli-deploy.js +731 -0
- package/dist/internal/cursor/backend-client.d.ts +10 -1
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +79 -1
- package/dist/internal/cursor/github-credentials.d.ts +44 -0
- package/dist/internal/cursor/github-credentials.d.ts.map +1 -0
- package/dist/internal/cursor/github-credentials.js +195 -0
- package/dist/internal/deploy-client.d.ts +176 -0
- package/dist/internal/deploy-client.d.ts.map +1 -0
- package/dist/internal/deploy-client.js +375 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +73 -5
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/eval-run-store.d.ts +22 -3
- package/dist/internal/eval-run-store.d.ts.map +1 -1
- package/dist/internal/eval-run-store.js +37 -19
- package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
- package/dist/internal/handleAgentServeTrigger.js +10 -12
- package/dist/internal/host-platforms.d.ts +7 -2
- package/dist/internal/host-platforms.d.ts.map +1 -1
- package/dist/internal/host-platforms.js +15 -10
- package/dist/internal/hosting.d.ts +37 -0
- package/dist/internal/hosting.d.ts.map +1 -0
- package/dist/internal/hosting.js +67 -0
- package/dist/internal/reminder-runner.d.ts +7 -0
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +50 -6
- package/dist/internal/reminder-store.d.ts +2 -0
- package/dist/internal/reminder-store.d.ts.map +1 -1
- package/dist/internal/reminder-store.js +18 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +173 -42
- package/dist/internal/session-engine.d.ts +49 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +246 -17
- package/dist/internal/storage-coordinator.d.ts +139 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -0
- package/dist/internal/storage-coordinator.js +499 -0
- package/dist/playground/assets/cursor-icons-outline-BxTT_FVJ.woff2 +0 -0
- package/dist/playground/assets/index-B1Qc9h2u.css +1 -0
- package/dist/playground/assets/{index-FlWjhg3x.js → index-CpDYCj8W.js} +42 -42
- package/dist/playground/index.html +2 -2
- package/dist/storage.d.ts +204 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +153 -0
- package/dist/types.d.ts +44 -3
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +3 -2
- package/docs/guides/github.md +43 -8
- package/docs/guides/slack.md +1 -1
- package/docs/quickstart.md +329 -51
- package/docs/reference/instructions.md +8 -6
- package/docs/scaffolding-agents.md +1 -1
- package/docs/storage.md +98 -0
- package/package.json +8 -1
- package/skills/github/SKILL.md +9 -1
- package/src/bin/agent-serve.ts +139 -0
- package/src/channels/github/api.ts +42 -23
- package/src/channels/github/cursor-account.ts +165 -0
- package/src/channels/github/github-channel.ts +66 -6
- package/src/channels/github/index.ts +2 -2
- package/src/channels/github/types.ts +19 -0
- package/src/channels/slack/slack-channel.ts +17 -3
- package/src/channels/slack/types.ts +44 -3
- package/src/index.ts +13 -0
- package/src/internal/cli-deploy.ts +940 -0
- package/src/internal/cursor/backend-client.ts +103 -1
- package/src/internal/cursor/github-credentials.ts +248 -0
- package/src/internal/deploy-client.ts +591 -0
- package/src/internal/discovery.ts +88 -1
- package/src/internal/distribution.ts +1 -0
- package/src/internal/eval-run-store.ts +48 -19
- package/src/internal/handleAgentServeTrigger.ts +10 -12
- package/src/internal/host-platforms.ts +28 -11
- package/src/internal/hosting.ts +77 -0
- package/src/internal/reminder-runner.ts +50 -6
- package/src/internal/reminder-store.ts +21 -0
- package/src/internal/server.ts +213 -27
- package/src/internal/session-engine.ts +285 -7
- package/src/internal/storage-coordinator.ts +615 -0
- package/src/storage.ts +325 -0
- package/src/types.ts +40 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.CcVk1uKq.js +0 -1
- package/dist/docs/assets/index.md.BPKcj5AI.js +0 -20
- package/dist/docs/assets/quickstart.md.tVPiGK_L.js +0 -35
- package/dist/docs/assets/quickstart.md.tVPiGK_L.lean.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.lean.js +0 -1
- package/dist/playground/assets/cursor-icons-outline-oY2V_mvK.woff2 +0 -0
- package/dist/playground/assets/index-1K-hG-7p.css +0 -1
- /package/dist/docs/assets/{evals.md.DPZ_MAnI.lean.js → evals.md.DAgEc_hL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.lean.js → guides_agent-to-agent.md.Bpzgq2Pq.lean.js} +0 -0
- /package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.lean.js → guides_cloud-runtime.md.gVzabdQL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.lean.js → guides_human-in-the-loop.md.DlUqsp1S.lean.js} +0 -0
- /package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.lean.js → guides_webhooks.md.B1EswtUu.lean.js} +0 -0
- /package/dist/docs/assets/{reference_connections.md.C3vNH_DE.lean.js → reference_connections.md.zaEYCLHT.lean.js} +0 -0
- /package/dist/docs/assets/{reference_http-api.md.DBAahtdz.lean.js → reference_http-api.md.Dx_nmDG6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_schedules.md.D7qijxLk.lean.js → reference_schedules.md.w_F2mXB6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.lean.js → reference_subagents.md.zWAMNfi1.lean.js} +0 -0
- /package/dist/docs/assets/{reference_tools.md.DF5kwlt0.lean.js → reference_tools.md.CqgJroI0.lean.js} +0 -0
|
@@ -30,7 +30,8 @@ const EXECUTE_TIMEOUT_MS = 120_000;
|
|
|
30
30
|
export class CursorBackendError extends Error {
|
|
31
31
|
constructor(
|
|
32
32
|
message: string,
|
|
33
|
-
readonly status?: number
|
|
33
|
+
readonly status?: number,
|
|
34
|
+
readonly retryAfterMs?: number
|
|
34
35
|
) {
|
|
35
36
|
super(message);
|
|
36
37
|
this.name = "CursorBackendError";
|
|
@@ -65,6 +66,16 @@ const createUserApiKeyResponseSchema = z.object({
|
|
|
65
66
|
apiKey: z.string().min(1),
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
export interface CursorGithubToken {
|
|
70
|
+
token: string;
|
|
71
|
+
expiresAt: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const githubTokenResponseSchema: z.ZodType<CursorGithubToken> = z.object({
|
|
75
|
+
token: z.string().min(1),
|
|
76
|
+
expiresAt: z.string().datetime(),
|
|
77
|
+
});
|
|
78
|
+
|
|
68
79
|
const mcpToolSchema = z.object({
|
|
69
80
|
name: z.string(),
|
|
70
81
|
providerIdentifier: z.string().optional(),
|
|
@@ -298,6 +309,50 @@ export class CursorBackendClient {
|
|
|
298
309
|
return toCallResult(parsed.result ?? {});
|
|
299
310
|
}
|
|
300
311
|
|
|
312
|
+
/** Mint a short-lived GitHub token scoped to the requested repositories. */
|
|
313
|
+
async getGithubToken(
|
|
314
|
+
repos: readonly string[],
|
|
315
|
+
options?: { permissions?: "read" | "pr-write" | "contents-write" }
|
|
316
|
+
): Promise<CursorGithubToken> {
|
|
317
|
+
const permissions = options?.permissions ?? "pr-write";
|
|
318
|
+
let response: Response;
|
|
319
|
+
try {
|
|
320
|
+
response = await this.fetchImpl(
|
|
321
|
+
`${this.backendUrl}/v0/scm-events/github-token`,
|
|
322
|
+
{
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers: {
|
|
325
|
+
"content-type": "application/json",
|
|
326
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
327
|
+
},
|
|
328
|
+
body: JSON.stringify({ repos: [...repos], permissions }),
|
|
329
|
+
signal: AbortSignal.timeout(30_000),
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
throw new CursorBackendError(
|
|
334
|
+
`Cursor GitHub credential request failed: ${describeError(error)}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const text = await response.text();
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
throw new CursorBackendError(
|
|
340
|
+
describeGithubTokenHttpFailure(response.status, text),
|
|
341
|
+
response.status,
|
|
342
|
+
retryAfterMsFromResponse(response)
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
let parsed: unknown;
|
|
346
|
+
try {
|
|
347
|
+
parsed = JSON.parse(text) as unknown;
|
|
348
|
+
} catch {
|
|
349
|
+
throw new CursorBackendError(
|
|
350
|
+
"Cursor backend returned a non-JSON GitHub credential response."
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return githubTokenResponseSchema.parse(parsed);
|
|
354
|
+
}
|
|
355
|
+
|
|
301
356
|
private async rpc(
|
|
302
357
|
method: string,
|
|
303
358
|
body: unknown,
|
|
@@ -582,6 +637,53 @@ function connectErrorMessage(bodyText: string): string {
|
|
|
582
637
|
return bodyText.slice(0, 300) || "(empty response body)";
|
|
583
638
|
}
|
|
584
639
|
|
|
640
|
+
const MAX_GITHUB_TOKEN_RETRY_AFTER_MS = 60 * 60_000;
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Prefer the backend's actionable denial message. Only a true 401 on this
|
|
644
|
+
* route means the Cursor API key itself was rejected — GitHub-connection
|
|
645
|
+
* problems arrive as 403 with a server `error` string.
|
|
646
|
+
*/
|
|
647
|
+
function describeGithubTokenHttpFailure(
|
|
648
|
+
status: number,
|
|
649
|
+
bodyText: string
|
|
650
|
+
): string {
|
|
651
|
+
const detail = githubTokenErrorFromBody(bodyText);
|
|
652
|
+
if (status === 401) {
|
|
653
|
+
return `The Cursor API key was rejected${
|
|
654
|
+
detail === undefined ? "" : `: ${detail}`
|
|
655
|
+
}. Run \`agent-serve login\` again or update CURSOR_API_KEY.`;
|
|
656
|
+
}
|
|
657
|
+
if (detail !== undefined) {
|
|
658
|
+
return detail;
|
|
659
|
+
}
|
|
660
|
+
return `Cursor GitHub credential request failed (${status}).`;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function githubTokenErrorFromBody(bodyText: string): string | undefined {
|
|
664
|
+
const parsed = safeJsonParse(bodyText);
|
|
665
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
const error = (parsed as { error?: unknown }).error;
|
|
669
|
+
if (typeof error === "string" && error !== "") {
|
|
670
|
+
return error;
|
|
671
|
+
}
|
|
672
|
+
const message = (parsed as { message?: unknown }).message;
|
|
673
|
+
if (typeof message === "string" && message !== "") {
|
|
674
|
+
return message;
|
|
675
|
+
}
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function retryAfterMsFromResponse(response: Response): number | undefined {
|
|
680
|
+
const retryAfter = Number(response.headers.get("retry-after") ?? "");
|
|
681
|
+
if (!Number.isFinite(retryAfter) || retryAfter <= 0) {
|
|
682
|
+
return undefined;
|
|
683
|
+
}
|
|
684
|
+
return Math.min(retryAfter * 1000, MAX_GITHUB_TOKEN_RETRY_AFTER_MS);
|
|
685
|
+
}
|
|
686
|
+
|
|
585
687
|
function jwtExpiryMs(token: string): number {
|
|
586
688
|
try {
|
|
587
689
|
const payload = token.split(".")[1];
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Short-lived GitHub credentials minted from the host's Cursor account.
|
|
3
|
+
*
|
|
4
|
+
* The token stays in memory. `GH_TOKEN` is updated for child `gh` processes,
|
|
5
|
+
* while Octokit callers use {@link getToken} so long-lived clients refresh
|
|
6
|
+
* without being rebuilt.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CursorGithubRepoScope } from "../../channels/github/cursor-account.js";
|
|
10
|
+
import type { CursorBackendClient } from "./backend-client.js";
|
|
11
|
+
import { CursorBackendError } from "./backend-client.js";
|
|
12
|
+
|
|
13
|
+
const REFRESH_LEEWAY_MS = 5 * 60_000;
|
|
14
|
+
const REFRESH_RETRY_MS = 60_000;
|
|
15
|
+
const FAILURE_CACHE_TTL_MS = 60_000;
|
|
16
|
+
const MAX_RETRY_AFTER_MS = 60 * 60_000;
|
|
17
|
+
const MIN_TIMER_DELAY_MS = 1_000;
|
|
18
|
+
|
|
19
|
+
type GithubTokenClient = Pick<CursorBackendClient, "getGithubToken">;
|
|
20
|
+
|
|
21
|
+
export interface CursorGithubCredentialProviderOptions {
|
|
22
|
+
client: GithubTokenClient;
|
|
23
|
+
scope: CursorGithubRepoScope;
|
|
24
|
+
logger?: (line: string) => void;
|
|
25
|
+
/** Test seam. */
|
|
26
|
+
now?: () => number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CachedGithubToken {
|
|
30
|
+
token: string;
|
|
31
|
+
expiresAtMs: number;
|
|
32
|
+
refreshAtMs: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface CachedFailure {
|
|
36
|
+
message: string;
|
|
37
|
+
retryAtMs: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class CursorGithubCredentialProvider {
|
|
41
|
+
private readonly client: GithubTokenClient;
|
|
42
|
+
private readonly repos: string[];
|
|
43
|
+
private readonly permissions: CursorGithubRepoScope["permissions"];
|
|
44
|
+
private readonly logger: (line: string) => void;
|
|
45
|
+
private readonly now: () => number;
|
|
46
|
+
private cached: CachedGithubToken | undefined;
|
|
47
|
+
private failure: CachedFailure | undefined;
|
|
48
|
+
private refreshPromise: Promise<string> | undefined;
|
|
49
|
+
private refreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
50
|
+
private started = false;
|
|
51
|
+
private stopped = false;
|
|
52
|
+
private previousGhToken: string | undefined;
|
|
53
|
+
private installedGhToken: string | undefined;
|
|
54
|
+
|
|
55
|
+
constructor(options: CursorGithubCredentialProviderOptions) {
|
|
56
|
+
this.client = options.client;
|
|
57
|
+
this.repos = [...options.scope.repos];
|
|
58
|
+
this.permissions = options.scope.permissions;
|
|
59
|
+
this.logger = options.logger ?? (() => {});
|
|
60
|
+
this.now = options.now ?? (() => Date.now());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async start(): Promise<void> {
|
|
64
|
+
if (this.stopped) {
|
|
65
|
+
throw new Error("Cursor account GitHub credentials have been stopped.");
|
|
66
|
+
}
|
|
67
|
+
if (this.started) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.previousGhToken = process.env.GH_TOKEN;
|
|
71
|
+
this.started = true;
|
|
72
|
+
try {
|
|
73
|
+
const token = await this.getToken();
|
|
74
|
+
this.installGhToken(token);
|
|
75
|
+
this.scheduleRefresh();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
this.started = false;
|
|
78
|
+
this.restoreGhToken();
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getToken(): Promise<string> {
|
|
84
|
+
if (this.stopped) {
|
|
85
|
+
throw new Error("Cursor account GitHub credentials have been stopped.");
|
|
86
|
+
}
|
|
87
|
+
const now = this.now();
|
|
88
|
+
if (this.cached !== undefined && now >= this.cached.expiresAtMs) {
|
|
89
|
+
// Expired leases must not be served; drop them so the next mint is clean.
|
|
90
|
+
this.cached = undefined;
|
|
91
|
+
this.restoreGhToken();
|
|
92
|
+
}
|
|
93
|
+
if (this.cached !== undefined && now < this.cached.refreshAtMs) {
|
|
94
|
+
return this.cached.token;
|
|
95
|
+
}
|
|
96
|
+
if (
|
|
97
|
+
this.failure !== undefined &&
|
|
98
|
+
now < this.failure.retryAtMs &&
|
|
99
|
+
this.cached === undefined
|
|
100
|
+
) {
|
|
101
|
+
throw new Error(this.failure.message);
|
|
102
|
+
}
|
|
103
|
+
if (this.refreshPromise === undefined) {
|
|
104
|
+
this.refreshPromise = this.refresh().finally(() => {
|
|
105
|
+
this.refreshPromise = undefined;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return this.refreshPromise;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
stop(): void {
|
|
112
|
+
this.started = false;
|
|
113
|
+
this.stopped = true;
|
|
114
|
+
if (this.refreshTimer !== undefined) {
|
|
115
|
+
clearTimeout(this.refreshTimer);
|
|
116
|
+
this.refreshTimer = undefined;
|
|
117
|
+
}
|
|
118
|
+
this.cached = undefined;
|
|
119
|
+
this.failure = undefined;
|
|
120
|
+
this.restoreGhToken();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async refresh(): Promise<string> {
|
|
124
|
+
try {
|
|
125
|
+
const result = await this.client.getGithubToken(this.repos, {
|
|
126
|
+
permissions: this.permissions,
|
|
127
|
+
});
|
|
128
|
+
if (this.stopped) {
|
|
129
|
+
throw new Error("Cursor account GitHub credentials have been stopped.");
|
|
130
|
+
}
|
|
131
|
+
const expiresAtMs = Date.parse(result.expiresAt);
|
|
132
|
+
if (
|
|
133
|
+
!Number.isFinite(expiresAtMs) ||
|
|
134
|
+
expiresAtMs <= this.now() + REFRESH_LEEWAY_MS
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
"Cursor backend returned an invalid GitHub credential expiry."
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
this.failure = undefined;
|
|
141
|
+
this.cached = {
|
|
142
|
+
token: result.token,
|
|
143
|
+
expiresAtMs,
|
|
144
|
+
refreshAtMs: expiresAtMs - REFRESH_LEEWAY_MS,
|
|
145
|
+
};
|
|
146
|
+
if (this.started) {
|
|
147
|
+
this.installGhToken(result.token);
|
|
148
|
+
this.scheduleRefresh();
|
|
149
|
+
}
|
|
150
|
+
this.logger(
|
|
151
|
+
`[agent-serve] GitHub auth: refreshed Cursor account credential for ${this.repos.length} repo(s)`
|
|
152
|
+
);
|
|
153
|
+
return result.token;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
156
|
+
if (this.cached !== undefined && this.now() < this.cached.expiresAtMs) {
|
|
157
|
+
this.logger(
|
|
158
|
+
`[agent-serve] GitHub auth: Cursor account credential refresh failed; using the current lease: ${message}`
|
|
159
|
+
);
|
|
160
|
+
this.cached.refreshAtMs = Math.min(
|
|
161
|
+
this.cached.expiresAtMs,
|
|
162
|
+
this.now() + REFRESH_RETRY_MS
|
|
163
|
+
);
|
|
164
|
+
if (this.started) {
|
|
165
|
+
this.scheduleRetry();
|
|
166
|
+
}
|
|
167
|
+
return this.cached.token;
|
|
168
|
+
}
|
|
169
|
+
// Hard failure with no usable lease: evict any stale cache and back off.
|
|
170
|
+
this.cached = undefined;
|
|
171
|
+
this.restoreGhToken();
|
|
172
|
+
this.failure = {
|
|
173
|
+
message,
|
|
174
|
+
retryAtMs: this.now() + failureBackoffMs(error),
|
|
175
|
+
};
|
|
176
|
+
if (this.started) {
|
|
177
|
+
this.scheduleRetry();
|
|
178
|
+
}
|
|
179
|
+
throw error instanceof Error ? error : new Error(message);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private scheduleRefresh(): void {
|
|
184
|
+
if (!this.started || this.cached === undefined) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.replaceTimer(
|
|
188
|
+
Math.max(MIN_TIMER_DELAY_MS, this.cached.refreshAtMs - this.now())
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private scheduleRetry(): void {
|
|
193
|
+
if (!this.started) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const delayMs =
|
|
197
|
+
this.failure !== undefined
|
|
198
|
+
? Math.max(MIN_TIMER_DELAY_MS, this.failure.retryAtMs - this.now())
|
|
199
|
+
: REFRESH_RETRY_MS;
|
|
200
|
+
this.replaceTimer(delayMs);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private replaceTimer(delayMs: number): void {
|
|
204
|
+
if (this.refreshTimer !== undefined) {
|
|
205
|
+
clearTimeout(this.refreshTimer);
|
|
206
|
+
}
|
|
207
|
+
this.refreshTimer = setTimeout(() => {
|
|
208
|
+
void this.getToken().catch((error: unknown) => {
|
|
209
|
+
this.logger(
|
|
210
|
+
`[agent-serve] GitHub auth: Cursor account credential refresh failed: ${
|
|
211
|
+
error instanceof Error ? error.message : String(error)
|
|
212
|
+
}`
|
|
213
|
+
);
|
|
214
|
+
this.scheduleRetry();
|
|
215
|
+
});
|
|
216
|
+
}, delayMs);
|
|
217
|
+
this.refreshTimer.unref?.();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private installGhToken(token: string): void {
|
|
221
|
+
process.env.GH_TOKEN = token;
|
|
222
|
+
this.installedGhToken = token;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private restoreGhToken(): void {
|
|
226
|
+
if (
|
|
227
|
+
this.installedGhToken !== undefined &&
|
|
228
|
+
process.env.GH_TOKEN === this.installedGhToken
|
|
229
|
+
) {
|
|
230
|
+
if (this.previousGhToken === undefined) {
|
|
231
|
+
delete process.env.GH_TOKEN;
|
|
232
|
+
} else {
|
|
233
|
+
process.env.GH_TOKEN = this.previousGhToken;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
this.installedGhToken = undefined;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function failureBackoffMs(error: unknown): number {
|
|
241
|
+
if (error instanceof CursorBackendError && error.retryAfterMs !== undefined) {
|
|
242
|
+
return Math.min(
|
|
243
|
+
Math.max(error.retryAfterMs, FAILURE_CACHE_TTL_MS),
|
|
244
|
+
MAX_RETRY_AFTER_MS
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return FAILURE_CACHE_TTL_MS;
|
|
248
|
+
}
|