@cursor/july 0.1.68 → 0.1.73

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 (226) hide show
  1. package/AGENTS.md +4 -5
  2. package/README.md +23 -11
  3. package/dist/bin/agent-serve.js +3 -2
  4. package/dist/channels/github/api.d.ts +2 -0
  5. package/dist/channels/github/api.d.ts.map +1 -1
  6. package/dist/channels/github/api.js +2 -1
  7. package/dist/channels/github/binding.d.ts +7 -0
  8. package/dist/channels/github/binding.d.ts.map +1 -1
  9. package/dist/channels/github/binding.js +4 -0
  10. package/dist/channels/github/checks.d.ts +99 -0
  11. package/dist/channels/github/checks.d.ts.map +1 -0
  12. package/dist/channels/github/checks.js +108 -0
  13. package/dist/channels/github/cursor-account.d.ts +4 -4
  14. package/dist/channels/github/cursor-account.d.ts.map +1 -1
  15. package/dist/channels/github/cursor-account.js +7 -5
  16. package/dist/channels/github/index.d.ts +1 -0
  17. package/dist/channels/github/index.d.ts.map +1 -1
  18. package/dist/channels/github/index.js +1 -0
  19. package/dist/channels/github/types.d.ts +8 -0
  20. package/dist/channels/github/types.d.ts.map +1 -1
  21. package/dist/channels/slack/manifest.d.ts +2 -2
  22. package/dist/channels/slack/manifest.d.ts.map +1 -1
  23. package/dist/channels/slack/manifest.js +13 -11
  24. package/dist/channels/slack/types.d.ts +2 -2
  25. package/dist/channels/slack/types.d.ts.map +1 -1
  26. package/dist/connections.d.ts +17 -5
  27. package/dist/connections.d.ts.map +1 -1
  28. package/dist/connections.js +17 -5
  29. package/dist/docs/404.html +2 -2
  30. package/dist/docs/ab.html +3 -3
  31. package/dist/docs/assets/{app.C5XUTCw9.js → app.CdbcFD8T.js} +1 -1
  32. package/dist/docs/assets/chunks/@localSearchIndexroot.DQad--_z.js +1 -0
  33. package/dist/docs/assets/chunks/{VPLocalSearchBox.CGDTAYHY.js → VPLocalSearchBox.D_NKImM7.js} +1 -1
  34. package/dist/docs/assets/chunks/{theme.lPixh250.js → theme.CMvz7cu9.js} +2 -2
  35. package/dist/docs/assets/{concepts.md.CqOsxbMU.js → concepts.md.BXAm6G-C.js} +1 -1
  36. package/dist/docs/assets/{guides_mcp-oauth.md.C7G7IykG.js → guides_mcp-oauth.md.CzEB6RaG.js} +1 -1
  37. package/dist/docs/assets/{reference_agent-config.md.DHkkvcEZ.js → reference_agent-config.md.BZnEKfEc.js} +3 -4
  38. package/dist/docs/assets/{reference_agent-config.md.DHkkvcEZ.lean.js → reference_agent-config.md.BZnEKfEc.lean.js} +1 -1
  39. package/dist/docs/assets/{reference_connections.md.Bwswbm6H.js → reference_connections.md.Cu3N-S3Q.js} +18 -11
  40. package/dist/docs/assets/{reference_connections.md.Bwswbm6H.lean.js → reference_connections.md.Cu3N-S3Q.lean.js} +1 -1
  41. package/dist/docs/assets/{reference_sessions.md.DBVFi2Sx.js → reference_sessions.md.B0DdlM-K.js} +1 -1
  42. package/dist/docs/assets/{reference_tools.md.Wmvb7Lob.js → reference_tools.md.DTg_kEsx.js} +1 -1
  43. package/dist/docs/assets/troubleshooting.md.DQ-hYuZT.js +1 -0
  44. package/dist/docs/building-with-agents.html +3 -3
  45. package/dist/docs/concepts.html +5 -5
  46. package/dist/docs/deployment.html +3 -3
  47. package/dist/docs/evals.html +3 -3
  48. package/dist/docs/example-agents/approval-buddy.html +3 -3
  49. package/dist/docs/example-agents/benny.html +3 -3
  50. package/dist/docs/example-agents/bugbot.html +3 -3
  51. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  52. package/dist/docs/example-agents/codeowners-review.html +3 -3
  53. package/dist/docs/example-agents/concierge.html +3 -3
  54. package/dist/docs/example-agents/fsd.html +3 -3
  55. package/dist/docs/example-agents/index.html +3 -3
  56. package/dist/docs/example-agents/knowledge-base.html +3 -3
  57. package/dist/docs/example-agents/oncall.html +3 -3
  58. package/dist/docs/example-agents/security-reviewer.html +3 -3
  59. package/dist/docs/example-agents/slack-agent.html +3 -3
  60. package/dist/docs/example-agents/weather-agent.html +3 -3
  61. package/dist/docs/guides/agent-to-agent.html +3 -3
  62. package/dist/docs/guides/cloud-runtime.html +3 -3
  63. package/dist/docs/guides/convert-automation.html +3 -3
  64. package/dist/docs/guides/github.html +3 -3
  65. package/dist/docs/guides/human-in-the-loop.html +3 -3
  66. package/dist/docs/guides/mcp-oauth.html +5 -5
  67. package/dist/docs/guides/opentelemetry.html +3 -3
  68. package/dist/docs/guides/slack.html +3 -3
  69. package/dist/docs/guides/webhooks.html +3 -3
  70. package/dist/docs/hashmap.json +1 -1
  71. package/dist/docs/hillclimbing.html +3 -3
  72. package/dist/docs/index.html +3 -3
  73. package/dist/docs/quickstart.html +3 -3
  74. package/dist/docs/reference/agent-config.html +5 -6
  75. package/dist/docs/reference/artifacts.html +3 -3
  76. package/dist/docs/reference/channels.html +3 -3
  77. package/dist/docs/reference/cli.html +3 -3
  78. package/dist/docs/reference/connections.html +20 -13
  79. package/dist/docs/reference/hooks.html +3 -3
  80. package/dist/docs/reference/http-api.html +3 -3
  81. package/dist/docs/reference/instructions.html +3 -3
  82. package/dist/docs/reference/playground.html +3 -3
  83. package/dist/docs/reference/project-layout.html +3 -3
  84. package/dist/docs/reference/prompt.html +3 -3
  85. package/dist/docs/reference/schedules.html +3 -3
  86. package/dist/docs/reference/sessions.html +5 -5
  87. package/dist/docs/reference/skills.html +3 -3
  88. package/dist/docs/reference/subagents.html +3 -3
  89. package/dist/docs/reference/tools.html +5 -5
  90. package/dist/docs/scaffolding-agents.html +3 -3
  91. package/dist/docs/storage.html +3 -3
  92. package/dist/docs/templates/agentic-owners.html +3 -3
  93. package/dist/docs/templates/demo.html +3 -3
  94. package/dist/docs/templates/pr-autofixer.html +3 -3
  95. package/dist/docs/templates/security-reviewer.html +3 -3
  96. package/dist/docs/templates/triage.html +3 -3
  97. package/dist/docs/troubleshooting.html +5 -5
  98. package/dist/internal/advertise-tools.d.ts +11 -6
  99. package/dist/internal/advertise-tools.d.ts.map +1 -1
  100. package/dist/internal/advertise-tools.js +63 -30
  101. package/dist/internal/cli-deploy.d.ts.map +1 -1
  102. package/dist/internal/cli-deploy.js +2 -4
  103. package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -1
  104. package/dist/internal/convert-automation/convert-workflow.js +2 -10
  105. package/dist/internal/cursor/account-mcp.d.ts +29 -3
  106. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  107. package/dist/internal/cursor/account-mcp.js +147 -67
  108. package/dist/internal/cursor/backend-client.d.ts +11 -6
  109. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  110. package/dist/internal/cursor/backend-client.js +32 -10
  111. package/dist/internal/cursor-account-mcp-auth.d.ts +11 -0
  112. package/dist/internal/cursor-account-mcp-auth.d.ts.map +1 -1
  113. package/dist/internal/cursor-account-mcp-auth.js +69 -0
  114. package/dist/internal/deploy-client.d.ts +0 -7
  115. package/dist/internal/deploy-client.d.ts.map +1 -1
  116. package/dist/internal/deploy-client.js +2 -4
  117. package/dist/internal/deploy-manifest.d.ts +15 -24
  118. package/dist/internal/deploy-manifest.d.ts.map +1 -1
  119. package/dist/internal/deploy-manifest.js +38 -27
  120. package/dist/internal/deployment-scm-event-stream.d.ts +4 -1
  121. package/dist/internal/deployment-scm-event-stream.d.ts.map +1 -1
  122. package/dist/internal/deployment-scm-event-stream.js +15 -1
  123. package/dist/internal/discovery.d.ts.map +1 -1
  124. package/dist/internal/discovery.js +42 -10
  125. package/dist/internal/init-project.d.ts.map +1 -1
  126. package/dist/internal/init-project.js +7 -106
  127. package/dist/internal/init-scaffold.d.ts +8 -0
  128. package/dist/internal/init-scaffold.d.ts.map +1 -0
  129. package/dist/internal/init-scaffold.js +168 -0
  130. package/dist/internal/inline-stdio-mcp.d.ts +2 -0
  131. package/dist/internal/inline-stdio-mcp.d.ts.map +1 -1
  132. package/dist/internal/inline-stdio-mcp.js +10 -8
  133. package/dist/internal/json-schema.d.ts +7 -0
  134. package/dist/internal/json-schema.d.ts.map +1 -1
  135. package/dist/internal/json-schema.js +18 -3
  136. package/dist/internal/mcp-endpoint.js +7 -7
  137. package/dist/internal/mcp-host.js +4 -2
  138. package/dist/internal/mcp-oauth.d.ts +22 -0
  139. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  140. package/dist/internal/mcp-oauth.js +40 -1
  141. package/dist/internal/package-inline-stdio.d.ts.map +1 -1
  142. package/dist/internal/package-inline-stdio.js +5 -5
  143. package/dist/internal/review-comments.d.ts +97 -0
  144. package/dist/internal/review-comments.d.ts.map +1 -1
  145. package/dist/internal/review-comments.js +151 -0
  146. package/dist/internal/sanitize-mcp-tool-name.d.ts +20 -0
  147. package/dist/internal/sanitize-mcp-tool-name.d.ts.map +1 -0
  148. package/dist/internal/sanitize-mcp-tool-name.js +30 -0
  149. package/dist/internal/session-engine.d.ts.map +1 -1
  150. package/dist/internal/session-engine.js +4 -6
  151. package/dist/internal/shallow-clone.d.ts +94 -39
  152. package/dist/internal/shallow-clone.d.ts.map +1 -1
  153. package/dist/internal/shallow-clone.js +188 -68
  154. package/dist/internal/terminal-style.d.ts +18 -0
  155. package/dist/internal/terminal-style.d.ts.map +1 -1
  156. package/dist/internal/terminal-style.js +46 -1
  157. package/dist/internal/workspace.d.ts +2 -0
  158. package/dist/internal/workspace.d.ts.map +1 -1
  159. package/dist/internal/workspace.js +5 -0
  160. package/dist/playground/assets/index-77uhu5Kr.css +1 -0
  161. package/dist/playground/assets/{index-yxs1heea.js → index-D4buox4m.js} +42 -42
  162. package/dist/playground/index.html +2 -2
  163. package/dist/tools.d.ts +7 -0
  164. package/dist/tools.d.ts.map +1 -1
  165. package/dist/types.d.ts +28 -3
  166. package/dist/types.d.ts.map +1 -1
  167. package/docs/concepts.md +4 -4
  168. package/docs/guides/mcp-oauth.md +3 -1
  169. package/docs/reference/agent-config.md +11 -12
  170. package/docs/reference/connections.md +44 -26
  171. package/docs/reference/sessions.md +2 -2
  172. package/docs/reference/tools.md +1 -1
  173. package/docs/troubleshooting.md +3 -2
  174. package/package.json +4 -1
  175. package/skills/create-agent/SKILL.md +12 -9
  176. package/skills/debug/SKILL.md +2 -1
  177. package/skills/framework-map/SKILL.md +6 -6
  178. package/skills/mcp-auth/SKILL.md +8 -6
  179. package/src/bin/agent-serve.ts +3 -2
  180. package/src/channels/github/api.ts +6 -1
  181. package/src/channels/github/binding.ts +21 -0
  182. package/src/channels/github/checks.ts +179 -0
  183. package/src/channels/github/cursor-account.ts +9 -6
  184. package/src/channels/github/index.ts +1 -0
  185. package/src/channels/github/types.ts +8 -0
  186. package/src/channels/slack/manifest.ts +8 -10
  187. package/src/channels/slack/types.ts +2 -2
  188. package/src/connections.ts +17 -5
  189. package/src/internal/advertise-tools.ts +72 -23
  190. package/src/internal/cli-deploy.ts +4 -15
  191. package/src/internal/convert-automation/convert-workflow.ts +2 -11
  192. package/src/internal/cursor/account-mcp.ts +208 -73
  193. package/src/internal/cursor/backend-client.ts +45 -11
  194. package/src/internal/cursor-account-mcp-auth.ts +83 -0
  195. package/src/internal/deploy-client.ts +0 -10
  196. package/src/internal/deploy-manifest.ts +33 -39
  197. package/src/internal/deployment-scm-event-stream.ts +17 -1
  198. package/src/internal/discovery.ts +47 -3
  199. package/src/internal/init-project.ts +7 -133
  200. package/src/internal/init-scaffold.ts +176 -0
  201. package/src/internal/inline-stdio-mcp.ts +13 -0
  202. package/src/internal/json-schema.ts +24 -3
  203. package/src/internal/mcp-endpoint.ts +6 -0
  204. package/src/internal/mcp-host.ts +4 -0
  205. package/src/internal/mcp-oauth.ts +55 -1
  206. package/src/internal/package-inline-stdio.ts +5 -1
  207. package/src/internal/review-comments.ts +245 -0
  208. package/src/internal/sanitize-mcp-tool-name.ts +33 -0
  209. package/src/internal/session-engine.ts +7 -0
  210. package/src/internal/shallow-clone.ts +293 -105
  211. package/src/internal/terminal-style.ts +67 -1
  212. package/src/internal/workspace.ts +6 -0
  213. package/src/tools.ts +7 -0
  214. package/src/types.ts +32 -4
  215. package/templates/agentic-owners/agent/agent.ts +3 -0
  216. package/templates/demo/agent/agent.ts +3 -0
  217. package/templates/security-reviewer/agent/agent.ts +3 -0
  218. package/templates/triage/agent/agent.ts +3 -0
  219. package/dist/docs/assets/chunks/@localSearchIndexroot.DvUR3lv9.js +0 -1
  220. package/dist/docs/assets/troubleshooting.md.CuVztCKs.js +0 -1
  221. package/dist/playground/assets/index-CoL7xCJl.css +0 -1
  222. /package/dist/docs/assets/{concepts.md.CqOsxbMU.lean.js → concepts.md.BXAm6G-C.lean.js} +0 -0
  223. /package/dist/docs/assets/{guides_mcp-oauth.md.C7G7IykG.lean.js → guides_mcp-oauth.md.CzEB6RaG.lean.js} +0 -0
  224. /package/dist/docs/assets/{reference_sessions.md.DBVFi2Sx.lean.js → reference_sessions.md.B0DdlM-K.lean.js} +0 -0
  225. /package/dist/docs/assets/{reference_tools.md.Wmvb7Lob.lean.js → reference_tools.md.DTg_kEsx.lean.js} +0 -0
  226. /package/dist/docs/assets/{troubleshooting.md.CuVztCKs.lean.js → troubleshooting.md.DQ-hYuZT.lean.js} +0 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * MCP tool-name sanitization shared by the Cursor-account bridge
3
+ * (`account-mcp.ts`) and advertised connections (`advertise-tools.ts`).
4
+ *
5
+ * Connector display names — and the aggregate `/v1/agent/mcp` names minted
6
+ * from them (`<serverIdentifier>-<tool>`) — can contain spaces, dots, and
7
+ * other characters that are illegal as the tool name a model sees.
8
+ *
9
+ * Character class matches `packages/agent` `normalizeMcpToolName`
10
+ * (`[^a-zA-Z0-9_-]` → `_`). Length cap and the empty fallback match the
11
+ * account bridge (MCP clients commonly enforce `^[a-zA-Z0-9_-]{1,128}$`).
12
+ * Callers that talk to the backend keep the original advertised name.
13
+ */
14
+
15
+ /** MCP clients commonly enforce `^[a-zA-Z0-9_-]{1,128}$` for tool names. */
16
+ export const MAX_MCP_TOOL_NAME_LENGTH = 128;
17
+
18
+ /** Connector display names can contain anything (spaces, dots, emoji). */
19
+ export function sanitizeMcpToolName(name: string): string {
20
+ const cleaned = name
21
+ .replace(/[^a-zA-Z0-9_-]/g, "_")
22
+ .slice(0, MAX_MCP_TOOL_NAME_LENGTH);
23
+ return cleaned === "" ? "tool" : cleaned;
24
+ }
25
+
26
+ /** `base` for the first claimant, `base_2` / `base_3` … for colliders. */
27
+ export function suffixMcpToolName(base: string, index: number): string {
28
+ if (index === 0) {
29
+ return base;
30
+ }
31
+ const suffix = `_${index + 1}`;
32
+ return `${base.slice(0, MAX_MCP_TOOL_NAME_LENGTH - suffix.length)}${suffix}`;
33
+ }
@@ -2327,6 +2327,13 @@ export class SessionEngine {
2327
2327
  tools[tool.name] = {
2328
2328
  description: tool.description,
2329
2329
  inputSchema: tool.inputSchema as SDKCustomTool["inputSchema"],
2330
+ // `SDKCustomTool.outputSchema` ships in the next published
2331
+ // @cursor/sdk; the conditional spread carries the field through the
2332
+ // pinned SDK's type until the dependency bumps, at which point local
2333
+ // runs start advertising it.
2334
+ ...(tool.outputSchema === undefined
2335
+ ? {}
2336
+ : { outputSchema: tool.outputSchema }),
2330
2337
  execute: async (args, context) => {
2331
2338
  return this.executeServerTool({
2332
2339
  tool,
@@ -5,7 +5,7 @@ import { promisify } from "node:util";
5
5
  const execFileAsync = promisify(execFile);
6
6
 
7
7
  /**
8
- * Shallow GitHub PR checkout for SDK agents that review a PR's code.
8
+ * Shallow SCM PR checkout for SDK agents that review a PR's code.
9
9
  *
10
10
  * Mirrors Bugbot's `shallow_fetch` prefetch strategy
11
11
  * (`backend/crates/napi_bugbot/src/temp_clone.rs`): instead of cloning the
@@ -16,11 +16,19 @@ const execFileAsync = promisify(execFile);
16
16
  * clone because its tools stream objects on demand; SDK agents read a real
17
17
  * working tree, so we materialize the head checkout instead.
18
18
  *
19
- * Auth also mirrors Bugbot's provider model (`IGitProvider.getAccessToken`
20
- * token handed straight to git): the clone takes one explicit repo-scoped
21
- * bearer token. The default source is the serve host's Cursor-account
22
- * GitHub lease the same backend `GithubAccessService` mint Bugbot uses —
23
- * which `CursorGithubCredentialProvider` installs as `GH_TOKEN` for child
19
+ * SCM support follows Bugbot's provider seam (`IGitProvider.getCloneTarget`
20
+ * / the Rust `clone_url` dispatch): everything provider-specific remote
21
+ * URL shape, git credential username convention, PR head ref layout — lives
22
+ * in one {@link ScmCloneTarget} produced by {@link resolveScmCloneTarget}.
23
+ * Supporting another SCM (GitLab, Bitbucket, …) means adding a resolver
24
+ * case and a default credential source; the fetch/verify/checkout machinery
25
+ * is provider-agnostic.
26
+ *
27
+ * Auth also mirrors Bugbot (`IGitProvider.getAccessToken` → token handed
28
+ * straight to git): the clone takes one explicit repo-scoped bearer token.
29
+ * For GitHub the default source is the serve host's Cursor-account lease —
30
+ * the same backend `GithubAccessService` mint Bugbot uses — which
31
+ * `CursorGithubCredentialProvider` installs as `GH_TOKEN` for child
24
32
  * processes. Where Bugbot embeds the token in the clone URL, this module
25
33
  * serves it through an inline credential helper that reads a child-process
26
34
  * env var, so the token never lands in argv or in `.git/config` inside a
@@ -31,23 +39,72 @@ const execFileAsync = promisify(execFile);
31
39
  * source imports, following `tenant-mcp/run-mcp-registry.ts`.
32
40
  */
33
41
 
34
- export type ShallowPrCheckout = {
35
- /** Directory containing the PR head working tree (detached at headSha). */
36
- checkoutDir: string;
37
- headSha: string;
38
- /** True when the merge-base commit was also fetched (enables local diffs). */
39
- baseFetched: boolean;
40
- /** How the head commit was obtained. */
41
- headSource: "sha_fetch" | "pr_ref_fetch";
42
+ /** SCMs with shallow-clone support. */
43
+ export type ScmProvider = "github";
44
+
45
+ /**
46
+ * Everything git needs to clone one SCM's PR head. Provider-specific
47
+ * conventions live here and nowhere else.
48
+ */
49
+ export type ScmCloneTarget = {
50
+ provider: ScmProvider;
51
+ /** `owner/name` — used for logs and error messages. */
52
+ repo: string;
53
+ prNumber: number;
54
+ /** HTTPS remote to fetch from. */
55
+ remoteUrl: string;
56
+ /** Username paired with the bearer token for git-over-HTTPS basic auth. */
57
+ credentialUsername: string;
58
+ /**
59
+ * Server-advertised PR head ref (e.g. GitHub `refs/pull/<n>/head`,
60
+ * GitLab `refs/merge-requests/<n>/head`) — the fallback when the pinned
61
+ * SHA cannot be fetched directly.
62
+ */
63
+ prHeadRef: string;
42
64
  };
43
65
 
44
- /** Repo-scoped bearer for git-over-HTTPS (installation token or PAT). */
66
+ /**
67
+ * Resolve a PR URL to its clone target, or `undefined` when the SCM has no
68
+ * shallow-clone support (callers then review on their fallback checkout —
69
+ * e.g. Origin PRs, where the host checkout already is the reviewed repo).
70
+ */
71
+ export function resolveScmCloneTarget(
72
+ prUrl: string
73
+ ): ScmCloneTarget | undefined {
74
+ let url: URL;
75
+ try {
76
+ url = new URL(prUrl);
77
+ } catch {
78
+ return undefined;
79
+ }
80
+ const parts = url.pathname.split("/").filter(Boolean);
81
+ if (
82
+ url.hostname === "github.com" &&
83
+ parts.length >= 4 &&
84
+ parts[2] === "pull" &&
85
+ Number.isInteger(Number(parts[3]))
86
+ ) {
87
+ const repo = `${parts[0]}/${parts[1]}`;
88
+ const prNumber = Number(parts[3]);
89
+ return {
90
+ provider: "github",
91
+ repo,
92
+ prNumber,
93
+ remoteUrl: `https://github.com/${repo}.git`,
94
+ credentialUsername: "x-access-token",
95
+ prHeadRef: `refs/pull/${prNumber}/head`,
96
+ };
97
+ }
98
+ return undefined;
99
+ }
100
+
101
+ /** Repo-scoped bearer for git-over-HTTPS. */
45
102
  export type ShallowCloneAuth = {
46
103
  token: string;
47
104
  };
48
105
 
49
106
  /**
50
- * Default clone credential: the serve host's Cursor-account GitHub lease.
107
+ * Default GitHub clone credential: the serve host's Cursor-account lease.
51
108
  * `CursorGithubCredentialProvider` mints it repo-scoped from the backend and
52
109
  * keeps `GH_TOKEN` refreshed for child processes; `GITHUB_TOKEN` covers
53
110
  * local/CI runs that export the conventional variable instead.
@@ -59,6 +116,154 @@ export function serveHostGithubAuth(
59
116
  return token === "" ? undefined : { token };
60
117
  }
61
118
 
119
+ /** Pair the provider's git-over-HTTPS username with the bearer token. */
120
+ function cloneCredential(
121
+ target: ScmCloneTarget,
122
+ auth: ShallowCloneAuth | undefined
123
+ ): GitCredential | undefined {
124
+ return auth === undefined
125
+ ? undefined
126
+ : { username: target.credentialUsername, token: auth.token };
127
+ }
128
+
129
+ /** Default clone credential source per provider. */
130
+ function defaultCloneAuth(
131
+ target: ScmCloneTarget
132
+ ): ShallowCloneAuth | undefined {
133
+ switch (target.provider) {
134
+ case "github":
135
+ return serveHostGithubAuth();
136
+ }
137
+ }
138
+
139
+ /** Cap on diff text handed to a model; beyond this it is truncated. */
140
+ const DIFF_MAX_CHARS = 80_000;
141
+
142
+ export type CommitRangeDiff =
143
+ | { status: "ok"; diff: string }
144
+ | { status: "unavailable"; reason: string };
145
+
146
+ /**
147
+ * Diff two commits inside an existing shallow PR checkout, fetching either
148
+ * end on demand when the shallow history does not already contain it.
149
+ *
150
+ * Lives here because it needs the same authenticated git the clone uses —
151
+ * a `--depth=1` checkout usually lacks the older commit, so reading a range
152
+ * means another credentialed fetch. Never throws: every failure becomes an
153
+ * `unavailable` reason the caller can act on or surface.
154
+ */
155
+ export async function tryDiffCommitRange(args: {
156
+ /** Existing checkout to run git in (see {@link createShallowPrCheckout}). */
157
+ checkoutDir: string;
158
+ /** The checkout's clone target; supplies the remote's ref + auth conventions. */
159
+ target: ScmCloneTarget;
160
+ from: string;
161
+ to: string;
162
+ /** Restrict the diff to one path when set. */
163
+ file?: string | undefined;
164
+ /** Fetch credential; defaults to the target provider's credential source. */
165
+ auth?: ShallowCloneAuth | undefined;
166
+ }): Promise<CommitRangeDiff> {
167
+ if (!isFullCommitSha(args.from) || !isFullCommitSha(args.to)) {
168
+ return {
169
+ status: "unavailable",
170
+ reason:
171
+ "Could not compute a reliable diff because a commit SHA was not a full 40-character SHA.",
172
+ };
173
+ }
174
+ const credential = cloneCredential(
175
+ args.target,
176
+ args.auth ?? defaultCloneAuth(args.target)
177
+ );
178
+ for (const sha of [args.from, args.to]) {
179
+ if (
180
+ !(await ensureCommitFetched(
181
+ args.checkoutDir,
182
+ sha,
183
+ args.target,
184
+ credential
185
+ ))
186
+ ) {
187
+ return {
188
+ status: "unavailable",
189
+ reason:
190
+ "Could not compute a reliable diff because a commit was unavailable in the checkout.",
191
+ };
192
+ }
193
+ }
194
+ try {
195
+ const diff = await git(args.checkoutDir, [
196
+ "diff",
197
+ "--no-ext-diff",
198
+ "--unified=80",
199
+ `${args.from}..${args.to}`,
200
+ ...(args.file === undefined ? [] : ["--", args.file]),
201
+ ]);
202
+ return {
203
+ status: "ok",
204
+ diff:
205
+ diff.length <= DIFF_MAX_CHARS
206
+ ? diff
207
+ : `${diff.slice(0, DIFF_MAX_CHARS)}\n\n[diff truncated at ${DIFF_MAX_CHARS} characters]`,
208
+ };
209
+ } catch {
210
+ return {
211
+ status: "unavailable",
212
+ reason: "Could not compute a reliable diff because git diff failed.",
213
+ };
214
+ }
215
+ }
216
+
217
+ function isFullCommitSha(value: string): boolean {
218
+ return /^[0-9a-f]{40}$/i.test(value);
219
+ }
220
+
221
+ /** Present already, or fetchable by SHA, or reachable from the PR head ref. */
222
+ async function ensureCommitFetched(
223
+ checkoutDir: string,
224
+ sha: string,
225
+ target: ScmCloneTarget,
226
+ credential: GitCredential | undefined
227
+ ): Promise<boolean> {
228
+ if ((await revParse(checkoutDir, sha)) !== undefined) {
229
+ return true;
230
+ }
231
+ const attempts = [
232
+ [...FETCH_ARGS, sha],
233
+ // Deeper history on the PR head ref: the older end of an inter-run range
234
+ // is usually an ancestor of the tip rather than the tip itself.
235
+ [
236
+ "fetch",
237
+ "--depth=200",
238
+ "--no-tags",
239
+ "--quiet",
240
+ "origin",
241
+ target.prHeadRef,
242
+ ],
243
+ ];
244
+ for (const attempt of attempts) {
245
+ try {
246
+ await git(checkoutDir, attempt, credential);
247
+ if ((await revParse(checkoutDir, sha)) !== undefined) {
248
+ return true;
249
+ }
250
+ } catch {
251
+ // Try the next strategy; the caller fails closed if none work.
252
+ }
253
+ }
254
+ return false;
255
+ }
256
+
257
+ export type ShallowPrCheckout = {
258
+ /** Directory containing the PR head working tree (detached at headSha). */
259
+ checkoutDir: string;
260
+ headSha: string;
261
+ /** True when the merge-base commit was also fetched (enables local diffs). */
262
+ baseFetched: boolean;
263
+ /** How the head commit was obtained. */
264
+ headSource: "sha_fetch" | "pr_ref_fetch";
265
+ };
266
+
62
267
  function logShallowClone(
63
268
  message: string,
64
269
  fields: Record<string, string | number | boolean | undefined> = {}
@@ -99,17 +304,22 @@ function gitError(args: string[], error: unknown): Error {
99
304
  }
100
305
 
101
306
  /**
102
- * The one credential helper git may use: serve the caller-provided token
103
- * from the child process env at request time. The shell reads
104
- * `$SHALLOW_CLONE_GIT_TOKEN` when git asks, keeping the token out of argv.
307
+ * The one credential helper git may use: serve the caller-provided
308
+ * credential from the child process env at request time. The shell reads
309
+ * the values when git asks, keeping the token out of argv.
105
310
  */
106
- const TOKEN_CREDENTIAL_HELPER =
107
- '!f() { test "$1" = get && printf "username=x-access-token\\npassword=$SHALLOW_CLONE_GIT_TOKEN\\n"; }; f';
311
+ const CREDENTIAL_HELPER =
312
+ '!f() { test "$1" = get && printf "username=$SHALLOW_CLONE_GIT_USERNAME\\npassword=$SHALLOW_CLONE_GIT_TOKEN\\n"; }; f';
313
+
314
+ type GitCredential = {
315
+ username: string;
316
+ token: string;
317
+ };
108
318
 
109
319
  async function git(
110
320
  cwd: string,
111
321
  args: string[],
112
- auth?: ShallowCloneAuth
322
+ credential?: GitCredential
113
323
  ): Promise<string> {
114
324
  // Async on purpose: fetches/checkouts of large repos take minutes, and the
115
325
  // serve host runs the clone in-process. A synchronous git call blocks the
@@ -119,9 +329,9 @@ async function git(
119
329
  const { stdout } = await execFileAsync(
120
330
  "git",
121
331
  [
122
- ...(auth === undefined
332
+ ...(credential === undefined
123
333
  ? []
124
- : ["-c", `credential.helper=${TOKEN_CREDENTIAL_HELPER}`]),
334
+ : ["-c", `credential.helper=${CREDENTIAL_HELPER}`]),
125
335
  ...args,
126
336
  ],
127
337
  {
@@ -138,9 +348,12 @@ async function git(
138
348
  GIT_CONFIG_GLOBAL: "/dev/null",
139
349
  GIT_CONFIG_SYSTEM: "/dev/null",
140
350
  GIT_TERMINAL_PROMPT: "0",
141
- ...(auth === undefined
351
+ ...(credential === undefined
142
352
  ? {}
143
- : { SHALLOW_CLONE_GIT_TOKEN: auth.token }),
353
+ : {
354
+ SHALLOW_CLONE_GIT_USERNAME: credential.username,
355
+ SHALLOW_CLONE_GIT_TOKEN: credential.token,
356
+ }),
144
357
  },
145
358
  }
146
359
  );
@@ -163,14 +376,17 @@ async function revParse(cwd: string, rev: string): Promise<string | undefined> {
163
376
 
164
377
  const FETCH_ARGS = ["fetch", "--depth=1", "--no-tags", "--quiet", "origin"];
165
378
 
379
+ /** Local tracking ref the PR head ref is fetched into. */
380
+ const LOCAL_PR_HEAD_REF = "refs/remotes/origin/shallow-clone-pr-head";
381
+
166
382
  /**
167
- * Create a fresh shallow checkout of a GitHub PR head at `checkoutDir`.
383
+ * Create a fresh shallow checkout of a PR head at `checkoutDir`.
168
384
  *
169
385
  * Fetch order (both single depth-1 round-trips):
170
386
  * 1. Fetch `headSha` (and `baseSha` when known) directly — GitHub advertises
171
387
  * `allow-reachable-sha1-in-want`, and this pins the exact reviewed commit
172
388
  * the way Bugbot's `shallow_fetch` does.
173
- * 2. Fall back to `refs/pull/<n>/head`, verifying it still resolves to
389
+ * 2. Fall back to the target's PR head ref, verifying it still resolves to
174
390
  * `headSha` so the working tree cannot silently drift from the pinned
175
391
  * patch in the review bundle.
176
392
  *
@@ -178,36 +394,33 @@ const FETCH_ARGS = ["fetch", "--depth=1", "--no-tags", "--quiet", "origin"];
178
394
  * patch text in their prompt); a missing head checkout throws.
179
395
  */
180
396
  export async function createShallowPrCheckout(args: {
181
- /** `owner/name`. */
182
- repo: string;
183
- prNumber?: number | undefined;
397
+ target: ScmCloneTarget;
184
398
  headSha: string;
185
399
  /** Merge-base SHA; fetched too so `git diff baseSha headSha` works locally. */
186
400
  baseSha?: string | undefined;
187
401
  checkoutDir: string;
188
- /** Clone credential; required for GitHub remotes, unused for file fixtures. */
402
+ /** Clone credential; required for HTTPS remotes, unused for file fixtures. */
189
403
  auth?: ShallowCloneAuth | undefined;
190
- /** Test seam: clone from this URL instead of `https://github.com/<repo>.git`. */
191
- remoteUrl?: string | undefined;
192
404
  }): Promise<ShallowPrCheckout> {
193
- const { repo, prNumber, headSha, baseSha, checkoutDir, auth } = args;
405
+ const { target, headSha, baseSha, checkoutDir, auth } = args;
406
+ const { repo, prNumber } = target;
407
+ const credential = cloneCredential(target, auth);
194
408
  const startedAt = Date.now();
195
409
  logShallowClone("cloning PR head", { repo, prNumber, headSha, baseSha });
196
410
  rmSync(checkoutDir, { recursive: true, force: true });
197
411
  mkdirSync(checkoutDir, { recursive: true });
198
412
  await git(checkoutDir, ["init", "--quiet"]);
199
- await git(checkoutDir, [
200
- "remote",
201
- "add",
202
- "origin",
203
- args.remoteUrl ?? `https://github.com/${repo}.git`,
204
- ]);
413
+ await git(checkoutDir, ["remote", "add", "origin", target.remoteUrl]);
205
414
 
206
415
  const shaFetch = async (): Promise<void> => {
207
416
  const extraBase =
208
417
  baseSha === undefined || baseSha === headSha ? [] : [baseSha];
209
418
  try {
210
- await git(checkoutDir, [...FETCH_ARGS, headSha, ...extraBase], auth);
419
+ await git(
420
+ checkoutDir,
421
+ [...FETCH_ARGS, headSha, ...extraBase],
422
+ credential
423
+ );
211
424
  } catch (error) {
212
425
  if (extraBase.length === 0) {
213
426
  throw error;
@@ -220,38 +433,28 @@ export async function createShallowPrCheckout(args: {
220
433
  baseSha,
221
434
  error: errorSummary(error),
222
435
  });
223
- await git(checkoutDir, [...FETCH_ARGS, headSha], auth);
436
+ await git(checkoutDir, [...FETCH_ARGS, headSha], credential);
224
437
  }
225
438
  };
226
439
 
227
440
  let headSource: ShallowPrCheckout["headSource"];
228
- let shaFetchError: string | undefined;
229
441
  try {
230
442
  await shaFetch();
231
443
  headSource = "sha_fetch";
232
444
  } catch (error) {
233
- shaFetchError = errorSummary(error);
234
- if (prNumber === undefined) {
235
- throw new Error(
236
- `Shallow SHA fetch failed for ${repo}@${headSha} and no PR number is available for a ref fallback: ${shaFetchError}`
237
- );
238
- }
445
+ const shaFetchError = errorSummary(error);
239
446
  logShallowClone("sha fetch failed; falling back to PR head ref", {
240
447
  repo,
241
448
  prNumber,
242
449
  headSha,
243
450
  error: shaFetchError,
244
451
  });
245
- const prRef = `refs/pull/${prNumber}/head`;
246
452
  await git(
247
453
  checkoutDir,
248
- [...FETCH_ARGS, `+${prRef}:refs/remotes/origin/pr/${prNumber}/head`],
249
- auth
250
- );
251
- const fetchedSha = await revParse(
252
- checkoutDir,
253
- `refs/remotes/origin/pr/${prNumber}/head`
454
+ [...FETCH_ARGS, `+${target.prHeadRef}:${LOCAL_PR_HEAD_REF}`],
455
+ credential
254
456
  );
457
+ const fetchedSha = await revParse(checkoutDir, LOCAL_PR_HEAD_REF);
255
458
  if (fetchedSha !== headSha) {
256
459
  throw new Error(
257
460
  `Shallow PR ref fetch for ${repo}#${prNumber} resolved to ${fetchedSha ?? "nothing"}, expected pinned head ${headSha} (sha fetch error: ${shaFetchError})`
@@ -279,7 +482,7 @@ export async function createShallowPrCheckout(args: {
279
482
  // Best-effort: local diffs are nice-to-have, the pinned patch in the
280
483
  // prompt is the authoritative delta either way.
281
484
  try {
282
- await git(checkoutDir, [...FETCH_ARGS, baseSha], auth);
485
+ await git(checkoutDir, [...FETCH_ARGS, baseSha], credential);
283
486
  baseFetched = (await revParse(checkoutDir, baseSha)) !== undefined;
284
487
  } catch (error) {
285
488
  logShallowClone("merge-base fetch failed; continuing without it", {
@@ -303,37 +506,18 @@ export async function createShallowPrCheckout(args: {
303
506
  return { checkoutDir, headSha, baseFetched, headSource };
304
507
  }
305
508
 
306
- /** True for `https://github.com/<owner>/<repo>/pull/<n>` URLs. */
307
- export function isGithubPrUrl(prUrl: string): boolean {
308
- try {
309
- const url = new URL(prUrl);
310
- const parts = url.pathname.split("/").filter(Boolean);
311
- return (
312
- url.hostname === "github.com" &&
313
- parts.length >= 4 &&
314
- parts[2] === "pull" &&
315
- Number.isInteger(Number(parts[3]))
316
- );
317
- } catch {
318
- return false;
319
- }
320
- }
321
-
322
509
  export type TryShallowPrCheckoutOptions = {
323
- /** PR URL; only GitHub PR URLs are cloned (others skip). */
510
+ /** PR URL; resolved to a clone target (unsupported SCMs skip). */
324
511
  prUrl: string;
325
- /** `owner/name`. */
326
- repo: string;
327
- prNumber?: number | undefined;
328
512
  /** Pinned head SHA; skipped (undefined result) when unknown. */
329
513
  headSha?: string | undefined;
330
514
  /** Merge-base SHA; fetched too so `git diff baseSha headSha` works locally. */
331
515
  baseSha?: string | undefined;
332
516
  checkoutDir: string;
333
- /** Clone credential; defaults to {@link serveHostGithubAuth}. */
517
+ /** Clone credential; defaults to the target provider's credential source. */
334
518
  auth?: ShallowCloneAuth | undefined;
335
- /** Test seam: clone from this URL instead of `https://github.com/<repo>.git`. */
336
- remoteUrl?: string | undefined;
519
+ /** Test seam: clone this target instead of resolving `prUrl`. */
520
+ target?: ScmCloneTarget | undefined;
337
521
  };
338
522
 
339
523
  export type TryShallowPrCheckoutResult = {
@@ -352,53 +536,57 @@ export type TryShallowPrCheckoutResult = {
352
536
  * reviewed repo instead of the host's own checkout.
353
537
  *
354
538
  * Returns no checkout — leaving the caller on its fallback working
355
- * directory — when the PR is not on GitHub (e.g. Origin PRs, where the host
356
- * checkout is the reviewed repo), when no head SHA is pinned, when no clone
357
- * credential is available, or when the clone fails; `fallbackReason` says
358
- * which. Failures are logged, never thrown: agents still have the pinned
359
- * patch in their prompts, so a missing checkout degrades quality rather
360
- * than the run.
539
+ * directory — when the PR's SCM has no clone support (e.g. Origin PRs,
540
+ * where the host checkout is the reviewed repo), when no head SHA is
541
+ * pinned, when no clone credential is available, or when the clone fails;
542
+ * `fallbackReason` says which. Failures are logged, never thrown: agents
543
+ * still have the pinned patch in their prompts, so a missing checkout
544
+ * degrades quality rather than the run.
361
545
  */
362
546
  export async function tryCreateShallowPrCheckout(
363
547
  options: TryShallowPrCheckoutOptions
364
548
  ): Promise<TryShallowPrCheckoutResult> {
365
- const { prUrl, repo, prNumber, headSha, baseSha, checkoutDir, remoteUrl } =
366
- options;
367
- if (remoteUrl === undefined && !isGithubPrUrl(prUrl)) {
368
- logShallowClone("skipped: not a GitHub PR URL", { prUrl });
369
- return { fallbackReason: "skipped: not a GitHub PR URL" };
549
+ const { prUrl, headSha, baseSha, checkoutDir } = options;
550
+ const target = options.target ?? resolveScmCloneTarget(prUrl);
551
+ if (target === undefined) {
552
+ logShallowClone("skipped: unsupported SCM for shallow clone", { prUrl });
553
+ return { fallbackReason: "skipped: unsupported SCM for shallow clone" };
370
554
  }
371
555
  if (headSha === undefined) {
372
- logShallowClone("skipped: no pinned head SHA", { repo, prUrl });
556
+ logShallowClone("skipped: no pinned head SHA", {
557
+ repo: target.repo,
558
+ prUrl,
559
+ });
373
560
  return { fallbackReason: "skipped: no pinned head SHA" };
374
561
  }
375
- const auth = options.auth ?? serveHostGithubAuth();
376
- if (remoteUrl === undefined && auth === undefined) {
377
- // Fail fast instead of letting an unauthenticated fetch 404/hang: real
378
- // GitHub clones need the serve host's lease (or an explicit token).
379
- logShallowClone("skipped: no GitHub clone credential", { repo, prUrl });
562
+ const auth = options.auth ?? defaultCloneAuth(target);
563
+ if (auth === undefined && target.remoteUrl.startsWith("https://")) {
564
+ // Fail fast instead of letting an unauthenticated fetch 404/hang: HTTPS
565
+ // clones need the provider's credential (or an explicit token).
566
+ logShallowClone("skipped: no clone credential", {
567
+ repo: target.repo,
568
+ provider: target.provider,
569
+ prUrl,
570
+ });
380
571
  return {
381
- fallbackReason:
382
- "skipped: no GitHub clone credential (serve host GH_TOKEN lease not installed)",
572
+ fallbackReason: `skipped: no ${target.provider} clone credential (serve host token lease not installed)`,
383
573
  };
384
574
  }
385
575
  try {
386
576
  return {
387
577
  checkout: await createShallowPrCheckout({
388
- repo,
389
- prNumber,
578
+ target,
390
579
  headSha,
391
580
  baseSha,
392
581
  checkoutDir,
393
582
  auth,
394
- remoteUrl,
395
583
  }),
396
584
  };
397
585
  } catch (error) {
398
586
  const reason = errorSummary(error);
399
587
  logShallowClone("failed; caller falls back to host checkout", {
400
- repo,
401
- prNumber,
588
+ repo: target.repo,
589
+ prNumber: target.prNumber,
402
590
  headSha,
403
591
  error: reason,
404
592
  });