@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.
Files changed (199) hide show
  1. package/AGENTS.md +24 -2
  2. package/README.md +25 -15
  3. package/dist/bin/agent-serve.js +101 -12
  4. package/dist/channels/github/api.d.ts.map +1 -1
  5. package/dist/channels/github/api.js +31 -14
  6. package/dist/channels/github/cursor-account.d.ts +43 -0
  7. package/dist/channels/github/cursor-account.d.ts.map +1 -0
  8. package/dist/channels/github/cursor-account.js +95 -0
  9. package/dist/channels/github/github-channel.d.ts.map +1 -1
  10. package/dist/channels/github/github-channel.js +46 -9
  11. package/dist/channels/github/index.d.ts +2 -2
  12. package/dist/channels/github/index.js +2 -2
  13. package/dist/channels/github/types.d.ts +17 -0
  14. package/dist/channels/github/types.d.ts.map +1 -1
  15. package/dist/channels/slack/slack-channel.d.ts +8 -2
  16. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  17. package/dist/channels/slack/slack-channel.js +8 -0
  18. package/dist/channels/slack/types.d.ts +24 -3
  19. package/dist/channels/slack/types.d.ts.map +1 -1
  20. package/dist/channels/slack/types.js +15 -1
  21. package/dist/docs/404.html +2 -2
  22. package/dist/docs/ab.html +8 -8
  23. package/dist/docs/assets/{ab.md.COdXkces.js → ab.md.BMCZ6Hd7.js} +3 -3
  24. package/dist/docs/assets/{ab.md.COdXkces.lean.js → ab.md.BMCZ6Hd7.lean.js} +1 -1
  25. package/dist/docs/assets/{app.DqfFEmJd.js → app.Oje4vhlk.js} +1 -1
  26. package/dist/docs/assets/chunks/@localSearchIndexroot.zwQ9RCQ7.js +1 -0
  27. package/dist/docs/assets/chunks/{VPLocalSearchBox.BaLEdS15.js → VPLocalSearchBox.H5XZ2zCB.js} +1 -1
  28. package/dist/docs/assets/chunks/{theme.CZRvu_0q.js → theme.CTR_TuaE.js} +2 -2
  29. package/dist/docs/assets/{deployment.md.Dx1TYNk5.js → deployment.md.DTKwE15Z.js} +3 -3
  30. package/dist/docs/assets/{deployment.md.Dx1TYNk5.lean.js → deployment.md.DTKwE15Z.lean.js} +1 -1
  31. package/dist/docs/assets/{evals.md.DPZ_MAnI.js → evals.md.DAgEc_hL.js} +3 -3
  32. package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.js → guides_agent-to-agent.md.Bpzgq2Pq.js} +1 -1
  33. package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.js → guides_cloud-runtime.md.gVzabdQL.js} +1 -1
  34. package/dist/docs/assets/{guides_github.md.DwbKhCeS.js → guides_github.md.DOOCpqsW.js} +11 -4
  35. package/dist/docs/assets/{guides_github.md.DwbKhCeS.lean.js → guides_github.md.DOOCpqsW.lean.js} +1 -1
  36. package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.js → guides_human-in-the-loop.md.DlUqsp1S.js} +2 -2
  37. package/dist/docs/assets/{guides_slack.md.bv41fHfW.js → guides_slack.md.CCwqHvSV.js} +4 -4
  38. package/dist/docs/assets/{guides_slack.md.bv41fHfW.lean.js → guides_slack.md.CCwqHvSV.lean.js} +1 -1
  39. package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.js → guides_webhooks.md.B1EswtUu.js} +2 -2
  40. package/dist/docs/assets/index.md.m81y7TY7.js +20 -0
  41. package/dist/docs/assets/{index.md.BPKcj5AI.lean.js → index.md.m81y7TY7.lean.js} +1 -1
  42. package/dist/docs/assets/quickstart.md.CfU8_uTC.js +192 -0
  43. package/dist/docs/assets/quickstart.md.CfU8_uTC.lean.js +1 -0
  44. package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.js → reference_agent-config.md.DrW2JUM8.js} +4 -4
  45. package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.lean.js → reference_agent-config.md.DrW2JUM8.lean.js} +1 -1
  46. package/dist/docs/assets/{reference_channels.md.D7JTR03W.js → reference_channels.md.DdmiKgqf.js} +4 -4
  47. package/dist/docs/assets/{reference_channels.md.D7JTR03W.lean.js → reference_channels.md.DdmiKgqf.lean.js} +1 -1
  48. package/dist/docs/assets/{reference_connections.md.C3vNH_DE.js → reference_connections.md.zaEYCLHT.js} +1 -1
  49. package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.js → reference_hooks.md.DyLVfE1O.js} +1 -1
  50. package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.lean.js → reference_hooks.md.DyLVfE1O.lean.js} +1 -1
  51. package/dist/docs/assets/{reference_http-api.md.DBAahtdz.js → reference_http-api.md.Dx_nmDG6.js} +1 -1
  52. package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.js → reference_instructions.md.CgoV-YEb.js} +9 -7
  53. package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.lean.js → reference_instructions.md.CgoV-YEb.lean.js} +1 -1
  54. package/dist/docs/assets/{reference_schedules.md.D7qijxLk.js → reference_schedules.md.w_F2mXB6.js} +2 -2
  55. package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.js → reference_skills.md.B_jHN7JL.js} +3 -3
  56. package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.lean.js → reference_skills.md.B_jHN7JL.lean.js} +1 -1
  57. package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.js → reference_subagents.md.zWAMNfi1.js} +1 -1
  58. package/dist/docs/assets/{reference_tools.md.DF5kwlt0.js → reference_tools.md.CqgJroI0.js} +2 -2
  59. package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.js +1 -0
  60. package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.lean.js +1 -0
  61. package/dist/docs/assets/storage.md.CVnInNiN.js +17 -0
  62. package/dist/docs/assets/storage.md.CVnInNiN.lean.js +1 -0
  63. package/dist/docs/building-with-agents.html +4 -4
  64. package/dist/docs/concepts.html +4 -4
  65. package/dist/docs/deployment.html +7 -7
  66. package/dist/docs/evals.html +7 -7
  67. package/dist/docs/guides/agent-to-agent.html +6 -6
  68. package/dist/docs/guides/cloud-runtime.html +5 -5
  69. package/dist/docs/guides/github.html +14 -7
  70. package/dist/docs/guides/human-in-the-loop.html +6 -6
  71. package/dist/docs/guides/slack.html +8 -8
  72. package/dist/docs/guides/webhooks.html +6 -6
  73. package/dist/docs/hashmap.json +1 -1
  74. package/dist/docs/hillclimbing.html +5 -5
  75. package/dist/docs/index.html +7 -7
  76. package/dist/docs/quickstart.html +180 -23
  77. package/dist/docs/reference/agent-config.html +7 -7
  78. package/dist/docs/reference/channels.html +8 -8
  79. package/dist/docs/reference/cli.html +4 -4
  80. package/dist/docs/reference/connections.html +5 -5
  81. package/dist/docs/reference/hooks.html +5 -5
  82. package/dist/docs/reference/http-api.html +6 -6
  83. package/dist/docs/reference/instructions.html +13 -11
  84. package/dist/docs/reference/playground.html +4 -4
  85. package/dist/docs/reference/project-layout.html +4 -4
  86. package/dist/docs/reference/schedules.html +7 -7
  87. package/dist/docs/reference/sessions.html +4 -4
  88. package/dist/docs/reference/skills.html +6 -6
  89. package/dist/docs/reference/subagents.html +6 -6
  90. package/dist/docs/reference/tools.html +7 -7
  91. package/dist/docs/scaffolding-agents.html +5 -5
  92. package/dist/docs/storage.html +41 -0
  93. package/dist/docs/troubleshooting.html +4 -4
  94. package/dist/index.d.ts +2 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +1 -0
  97. package/dist/internal/cli-deploy.d.ts +52 -0
  98. package/dist/internal/cli-deploy.d.ts.map +1 -0
  99. package/dist/internal/cli-deploy.js +731 -0
  100. package/dist/internal/cursor/backend-client.d.ts +10 -1
  101. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  102. package/dist/internal/cursor/backend-client.js +79 -1
  103. package/dist/internal/cursor/github-credentials.d.ts +44 -0
  104. package/dist/internal/cursor/github-credentials.d.ts.map +1 -0
  105. package/dist/internal/cursor/github-credentials.js +195 -0
  106. package/dist/internal/deploy-client.d.ts +176 -0
  107. package/dist/internal/deploy-client.d.ts.map +1 -0
  108. package/dist/internal/deploy-client.js +375 -0
  109. package/dist/internal/discovery.d.ts.map +1 -1
  110. package/dist/internal/discovery.js +73 -5
  111. package/dist/internal/distribution.d.ts.map +1 -1
  112. package/dist/internal/distribution.js +1 -0
  113. package/dist/internal/eval-run-store.d.ts +22 -3
  114. package/dist/internal/eval-run-store.d.ts.map +1 -1
  115. package/dist/internal/eval-run-store.js +37 -19
  116. package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
  117. package/dist/internal/handleAgentServeTrigger.js +10 -12
  118. package/dist/internal/host-platforms.d.ts +7 -2
  119. package/dist/internal/host-platforms.d.ts.map +1 -1
  120. package/dist/internal/host-platforms.js +15 -10
  121. package/dist/internal/hosting.d.ts +37 -0
  122. package/dist/internal/hosting.d.ts.map +1 -0
  123. package/dist/internal/hosting.js +67 -0
  124. package/dist/internal/reminder-runner.d.ts +7 -0
  125. package/dist/internal/reminder-runner.d.ts.map +1 -1
  126. package/dist/internal/reminder-runner.js +50 -6
  127. package/dist/internal/reminder-store.d.ts +2 -0
  128. package/dist/internal/reminder-store.d.ts.map +1 -1
  129. package/dist/internal/reminder-store.js +18 -0
  130. package/dist/internal/server.d.ts.map +1 -1
  131. package/dist/internal/server.js +173 -42
  132. package/dist/internal/session-engine.d.ts +49 -0
  133. package/dist/internal/session-engine.d.ts.map +1 -1
  134. package/dist/internal/session-engine.js +246 -17
  135. package/dist/internal/storage-coordinator.d.ts +139 -0
  136. package/dist/internal/storage-coordinator.d.ts.map +1 -0
  137. package/dist/internal/storage-coordinator.js +499 -0
  138. package/dist/playground/assets/cursor-icons-outline-BxTT_FVJ.woff2 +0 -0
  139. package/dist/playground/assets/index-B1Qc9h2u.css +1 -0
  140. package/dist/playground/assets/{index-FlWjhg3x.js → index-CpDYCj8W.js} +42 -42
  141. package/dist/playground/index.html +2 -2
  142. package/dist/storage.d.ts +204 -0
  143. package/dist/storage.d.ts.map +1 -0
  144. package/dist/storage.js +153 -0
  145. package/dist/types.d.ts +44 -3
  146. package/dist/types.d.ts.map +1 -1
  147. package/docs/README.md +3 -2
  148. package/docs/guides/github.md +43 -8
  149. package/docs/guides/slack.md +1 -1
  150. package/docs/quickstart.md +329 -51
  151. package/docs/reference/instructions.md +8 -6
  152. package/docs/scaffolding-agents.md +1 -1
  153. package/docs/storage.md +98 -0
  154. package/package.json +8 -1
  155. package/skills/github/SKILL.md +9 -1
  156. package/src/bin/agent-serve.ts +139 -0
  157. package/src/channels/github/api.ts +42 -23
  158. package/src/channels/github/cursor-account.ts +165 -0
  159. package/src/channels/github/github-channel.ts +66 -6
  160. package/src/channels/github/index.ts +2 -2
  161. package/src/channels/github/types.ts +19 -0
  162. package/src/channels/slack/slack-channel.ts +17 -3
  163. package/src/channels/slack/types.ts +44 -3
  164. package/src/index.ts +13 -0
  165. package/src/internal/cli-deploy.ts +940 -0
  166. package/src/internal/cursor/backend-client.ts +103 -1
  167. package/src/internal/cursor/github-credentials.ts +248 -0
  168. package/src/internal/deploy-client.ts +591 -0
  169. package/src/internal/discovery.ts +88 -1
  170. package/src/internal/distribution.ts +1 -0
  171. package/src/internal/eval-run-store.ts +48 -19
  172. package/src/internal/handleAgentServeTrigger.ts +10 -12
  173. package/src/internal/host-platforms.ts +28 -11
  174. package/src/internal/hosting.ts +77 -0
  175. package/src/internal/reminder-runner.ts +50 -6
  176. package/src/internal/reminder-store.ts +21 -0
  177. package/src/internal/server.ts +213 -27
  178. package/src/internal/session-engine.ts +285 -7
  179. package/src/internal/storage-coordinator.ts +615 -0
  180. package/src/storage.ts +325 -0
  181. package/src/types.ts +40 -2
  182. package/dist/docs/assets/chunks/@localSearchIndexroot.CcVk1uKq.js +0 -1
  183. package/dist/docs/assets/index.md.BPKcj5AI.js +0 -20
  184. package/dist/docs/assets/quickstart.md.tVPiGK_L.js +0 -35
  185. package/dist/docs/assets/quickstart.md.tVPiGK_L.lean.js +0 -1
  186. package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.js +0 -1
  187. package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.lean.js +0 -1
  188. package/dist/playground/assets/cursor-icons-outline-oY2V_mvK.woff2 +0 -0
  189. package/dist/playground/assets/index-1K-hG-7p.css +0 -1
  190. /package/dist/docs/assets/{evals.md.DPZ_MAnI.lean.js → evals.md.DAgEc_hL.lean.js} +0 -0
  191. /package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.lean.js → guides_agent-to-agent.md.Bpzgq2Pq.lean.js} +0 -0
  192. /package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.lean.js → guides_cloud-runtime.md.gVzabdQL.lean.js} +0 -0
  193. /package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.lean.js → guides_human-in-the-loop.md.DlUqsp1S.lean.js} +0 -0
  194. /package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.lean.js → guides_webhooks.md.B1EswtUu.lean.js} +0 -0
  195. /package/dist/docs/assets/{reference_connections.md.C3vNH_DE.lean.js → reference_connections.md.zaEYCLHT.lean.js} +0 -0
  196. /package/dist/docs/assets/{reference_http-api.md.DBAahtdz.lean.js → reference_http-api.md.Dx_nmDG6.lean.js} +0 -0
  197. /package/dist/docs/assets/{reference_schedules.md.D7qijxLk.lean.js → reference_schedules.md.w_F2mXB6.lean.js} +0 -0
  198. /package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.lean.js → reference_subagents.md.zWAMNfi1.lean.js} +0 -0
  199. /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
+ }