@cursor/july 0.1.62 → 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 (286) hide show
  1. package/AGENTS.md +4 -5
  2. package/README.md +23 -11
  3. package/dist/bin/agent-serve.js +11 -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/channel-watch.d.ts +2 -1
  22. package/dist/channels/slack/channel-watch.d.ts.map +1 -1
  23. package/dist/channels/slack/channel-watch.js +7 -3
  24. package/dist/channels/slack/dispatch.d.ts +9 -0
  25. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  26. package/dist/channels/slack/dispatch.js +41 -21
  27. package/dist/channels/slack/manifest.d.ts +2 -2
  28. package/dist/channels/slack/manifest.d.ts.map +1 -1
  29. package/dist/channels/slack/manifest.js +14 -11
  30. package/dist/channels/slack/nudge-store.d.ts +16 -0
  31. package/dist/channels/slack/nudge-store.d.ts.map +1 -1
  32. package/dist/channels/slack/nudge-store.js +53 -0
  33. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  34. package/dist/channels/slack/slack-channel.js +4 -1
  35. package/dist/channels/slack/types.d.ts +24 -7
  36. package/dist/channels/slack/types.d.ts.map +1 -1
  37. package/dist/connections.d.ts +17 -5
  38. package/dist/connections.d.ts.map +1 -1
  39. package/dist/connections.js +17 -5
  40. package/dist/docs/404.html +2 -2
  41. package/dist/docs/ab.html +3 -3
  42. package/dist/docs/assets/{app.B-biogkN.js → app.CdbcFD8T.js} +1 -1
  43. package/dist/docs/assets/chunks/@localSearchIndexroot.DQad--_z.js +1 -0
  44. package/dist/docs/assets/chunks/{VPLocalSearchBox.DZqKtFZS.js → VPLocalSearchBox.D_NKImM7.js} +1 -1
  45. package/dist/docs/assets/chunks/{theme.BwC5zP2r.js → theme.CMvz7cu9.js} +2 -2
  46. package/dist/docs/assets/{concepts.md.CqOsxbMU.js → concepts.md.BXAm6G-C.js} +1 -1
  47. package/dist/docs/assets/{guides_mcp-oauth.md.C7G7IykG.js → guides_mcp-oauth.md.CzEB6RaG.js} +1 -1
  48. package/dist/docs/assets/{reference_agent-config.md.DHkkvcEZ.js → reference_agent-config.md.BZnEKfEc.js} +3 -4
  49. package/dist/docs/assets/{reference_agent-config.md.DHkkvcEZ.lean.js → reference_agent-config.md.BZnEKfEc.lean.js} +1 -1
  50. package/dist/docs/assets/{reference_connections.md.CSQiNzre.js → reference_connections.md.Cu3N-S3Q.js} +26 -9
  51. package/dist/docs/assets/reference_connections.md.Cu3N-S3Q.lean.js +1 -0
  52. package/dist/docs/assets/{reference_sessions.md.DBVFi2Sx.js → reference_sessions.md.B0DdlM-K.js} +1 -1
  53. package/dist/docs/assets/{reference_tools.md.kJfJAP2m.js → reference_tools.md.DTg_kEsx.js} +2 -2
  54. package/dist/docs/assets/{reference_tools.md.kJfJAP2m.lean.js → reference_tools.md.DTg_kEsx.lean.js} +1 -1
  55. package/dist/docs/assets/troubleshooting.md.DQ-hYuZT.js +1 -0
  56. package/dist/docs/building-with-agents.html +3 -3
  57. package/dist/docs/concepts.html +5 -5
  58. package/dist/docs/deployment.html +3 -3
  59. package/dist/docs/evals.html +3 -3
  60. package/dist/docs/example-agents/approval-buddy.html +3 -3
  61. package/dist/docs/example-agents/benny.html +3 -3
  62. package/dist/docs/example-agents/bugbot.html +3 -3
  63. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  64. package/dist/docs/example-agents/codeowners-review.html +3 -3
  65. package/dist/docs/example-agents/concierge.html +3 -3
  66. package/dist/docs/example-agents/fsd.html +3 -3
  67. package/dist/docs/example-agents/index.html +3 -3
  68. package/dist/docs/example-agents/knowledge-base.html +3 -3
  69. package/dist/docs/example-agents/oncall.html +3 -3
  70. package/dist/docs/example-agents/security-reviewer.html +3 -3
  71. package/dist/docs/example-agents/slack-agent.html +3 -3
  72. package/dist/docs/example-agents/weather-agent.html +3 -3
  73. package/dist/docs/guides/agent-to-agent.html +3 -3
  74. package/dist/docs/guides/cloud-runtime.html +3 -3
  75. package/dist/docs/guides/convert-automation.html +3 -3
  76. package/dist/docs/guides/github.html +3 -3
  77. package/dist/docs/guides/human-in-the-loop.html +3 -3
  78. package/dist/docs/guides/mcp-oauth.html +5 -5
  79. package/dist/docs/guides/opentelemetry.html +3 -3
  80. package/dist/docs/guides/slack.html +3 -3
  81. package/dist/docs/guides/webhooks.html +3 -3
  82. package/dist/docs/hashmap.json +1 -1
  83. package/dist/docs/hillclimbing.html +3 -3
  84. package/dist/docs/index.html +3 -3
  85. package/dist/docs/quickstart.html +3 -3
  86. package/dist/docs/reference/agent-config.html +5 -6
  87. package/dist/docs/reference/artifacts.html +3 -3
  88. package/dist/docs/reference/channels.html +3 -3
  89. package/dist/docs/reference/cli.html +3 -3
  90. package/dist/docs/reference/connections.html +28 -11
  91. package/dist/docs/reference/hooks.html +3 -3
  92. package/dist/docs/reference/http-api.html +3 -3
  93. package/dist/docs/reference/instructions.html +3 -3
  94. package/dist/docs/reference/playground.html +3 -3
  95. package/dist/docs/reference/project-layout.html +3 -3
  96. package/dist/docs/reference/prompt.html +3 -3
  97. package/dist/docs/reference/schedules.html +3 -3
  98. package/dist/docs/reference/sessions.html +5 -5
  99. package/dist/docs/reference/skills.html +3 -3
  100. package/dist/docs/reference/subagents.html +3 -3
  101. package/dist/docs/reference/tools.html +5 -5
  102. package/dist/docs/scaffolding-agents.html +3 -3
  103. package/dist/docs/storage.html +3 -3
  104. package/dist/docs/templates/agentic-owners.html +3 -3
  105. package/dist/docs/templates/demo.html +3 -3
  106. package/dist/docs/templates/pr-autofixer.html +3 -3
  107. package/dist/docs/templates/security-reviewer.html +3 -3
  108. package/dist/docs/templates/triage.html +3 -3
  109. package/dist/docs/troubleshooting.html +5 -5
  110. package/dist/files-backends/agent-store-presigned-url.js +1 -1
  111. package/dist/index.d.ts +2 -2
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +1 -1
  114. package/dist/internal/advertise-tools.d.ts +79 -0
  115. package/dist/internal/advertise-tools.d.ts.map +1 -0
  116. package/dist/internal/advertise-tools.js +185 -0
  117. package/dist/internal/cli-deploy.d.ts.map +1 -1
  118. package/dist/internal/cli-deploy.js +7 -1
  119. package/dist/internal/cli-manifest.d.ts +19 -0
  120. package/dist/internal/cli-manifest.d.ts.map +1 -0
  121. package/dist/internal/cli-manifest.js +49 -0
  122. package/dist/internal/cli-slack.d.ts.map +1 -1
  123. package/dist/internal/cli-slack.js +9 -1
  124. package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -1
  125. package/dist/internal/convert-automation/convert-workflow.js +2 -10
  126. package/dist/internal/cursor/account-mcp.d.ts +29 -3
  127. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  128. package/dist/internal/cursor/account-mcp.js +147 -67
  129. package/dist/internal/cursor/backend-client.d.ts +11 -6
  130. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  131. package/dist/internal/cursor/backend-client.js +32 -10
  132. package/dist/internal/cursor-account-mcp-auth.d.ts +11 -0
  133. package/dist/internal/cursor-account-mcp-auth.d.ts.map +1 -1
  134. package/dist/internal/cursor-account-mcp-auth.js +69 -0
  135. package/dist/internal/cursor-agent-template.d.ts +9 -0
  136. package/dist/internal/cursor-agent-template.d.ts.map +1 -0
  137. package/dist/internal/cursor-agent-template.js +37 -0
  138. package/dist/internal/deploy-client.d.ts +1 -0
  139. package/dist/internal/deploy-client.d.ts.map +1 -1
  140. package/dist/internal/deploy-client.js +18 -2
  141. package/dist/internal/deploy-manifest.d.ts +76 -0
  142. package/dist/internal/deploy-manifest.d.ts.map +1 -0
  143. package/dist/internal/deploy-manifest.js +117 -0
  144. package/dist/internal/deployment-scm-event-stream.d.ts +116 -0
  145. package/dist/internal/deployment-scm-event-stream.d.ts.map +1 -0
  146. package/dist/internal/deployment-scm-event-stream.js +474 -0
  147. package/dist/internal/discovery.d.ts.map +1 -1
  148. package/dist/internal/discovery.js +111 -10
  149. package/dist/internal/init-project.d.ts.map +1 -1
  150. package/dist/internal/init-project.js +12 -123
  151. package/dist/internal/init-scaffold.d.ts +8 -0
  152. package/dist/internal/init-scaffold.d.ts.map +1 -0
  153. package/dist/internal/init-scaffold.js +168 -0
  154. package/dist/internal/inline-stdio-mcp.d.ts +2 -0
  155. package/dist/internal/inline-stdio-mcp.d.ts.map +1 -1
  156. package/dist/internal/inline-stdio-mcp.js +10 -8
  157. package/dist/internal/json-schema.d.ts +7 -0
  158. package/dist/internal/json-schema.d.ts.map +1 -1
  159. package/dist/internal/json-schema.js +18 -3
  160. package/dist/internal/mcp-endpoint.js +7 -7
  161. package/dist/internal/mcp-host.d.ts +18 -0
  162. package/dist/internal/mcp-host.d.ts.map +1 -1
  163. package/dist/internal/mcp-host.js +43 -5
  164. package/dist/internal/mcp-oauth.d.ts +22 -0
  165. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  166. package/dist/internal/mcp-oauth.js +40 -1
  167. package/dist/internal/package-inline-stdio.d.ts.map +1 -1
  168. package/dist/internal/package-inline-stdio.js +5 -5
  169. package/dist/internal/review-comments.d.ts +97 -0
  170. package/dist/internal/review-comments.d.ts.map +1 -1
  171. package/dist/internal/review-comments.js +151 -0
  172. package/dist/internal/sanitize-mcp-tool-name.d.ts +20 -0
  173. package/dist/internal/sanitize-mcp-tool-name.d.ts.map +1 -0
  174. package/dist/internal/sanitize-mcp-tool-name.js +30 -0
  175. package/dist/internal/sdk-runner.d.ts +1 -1
  176. package/dist/internal/sdk-runner.d.ts.map +1 -1
  177. package/dist/internal/sdk-runner.js +36 -13
  178. package/dist/internal/server.d.ts.map +1 -1
  179. package/dist/internal/server.js +18 -0
  180. package/dist/internal/session-engine.d.ts +66 -0
  181. package/dist/internal/session-engine.d.ts.map +1 -1
  182. package/dist/internal/session-engine.js +428 -110
  183. package/dist/internal/shallow-clone.d.ts +170 -0
  184. package/dist/internal/shallow-clone.d.ts.map +1 -0
  185. package/dist/internal/shallow-clone.js +412 -0
  186. package/dist/internal/slack-provision-client.d.ts +2 -0
  187. package/dist/internal/slack-provision-client.d.ts.map +1 -1
  188. package/dist/internal/slack-provision-client.js +1 -0
  189. package/dist/internal/terminal-style.d.ts +18 -0
  190. package/dist/internal/terminal-style.d.ts.map +1 -1
  191. package/dist/internal/terminal-style.js +46 -1
  192. package/dist/internal/turn-lease-store.d.ts +44 -0
  193. package/dist/internal/turn-lease-store.d.ts.map +1 -0
  194. package/dist/internal/turn-lease-store.js +139 -0
  195. package/dist/internal/workspace.d.ts +14 -0
  196. package/dist/internal/workspace.d.ts.map +1 -1
  197. package/dist/internal/workspace.js +33 -0
  198. package/dist/playground/assets/index-77uhu5Kr.css +1 -0
  199. package/dist/playground/assets/index-D4buox4m.js +85 -0
  200. package/dist/playground/index.html +2 -2
  201. package/dist/storage-protocol.d.ts +122 -0
  202. package/dist/storage-protocol.d.ts.map +1 -0
  203. package/dist/storage-protocol.js +173 -0
  204. package/dist/storage.d.ts +2 -72
  205. package/dist/storage.d.ts.map +1 -1
  206. package/dist/storage.js +2 -82
  207. package/dist/tools.d.ts +7 -0
  208. package/dist/tools.d.ts.map +1 -1
  209. package/dist/types.d.ts +141 -6
  210. package/dist/types.d.ts.map +1 -1
  211. package/docs/concepts.md +4 -4
  212. package/docs/guides/mcp-oauth.md +3 -1
  213. package/docs/reference/agent-config.md +11 -12
  214. package/docs/reference/connections.md +108 -10
  215. package/docs/reference/sessions.md +2 -2
  216. package/docs/reference/tools.md +14 -0
  217. package/docs/troubleshooting.md +3 -2
  218. package/package.json +7 -2
  219. package/skills/create-agent/SKILL.md +12 -9
  220. package/skills/debug/SKILL.md +2 -1
  221. package/skills/framework-map/SKILL.md +6 -6
  222. package/skills/mcp-auth/SKILL.md +8 -6
  223. package/src/bin/agent-serve.ts +12 -2
  224. package/src/channels/github/api.ts +6 -1
  225. package/src/channels/github/binding.ts +21 -0
  226. package/src/channels/github/checks.ts +179 -0
  227. package/src/channels/github/cursor-account.ts +9 -6
  228. package/src/channels/github/index.ts +1 -0
  229. package/src/channels/github/types.ts +8 -0
  230. package/src/channels/slack/channel-watch.ts +6 -3
  231. package/src/channels/slack/dispatch.ts +40 -3
  232. package/src/channels/slack/manifest.ts +9 -10
  233. package/src/channels/slack/nudge-store.ts +56 -0
  234. package/src/channels/slack/slack-channel.ts +3 -0
  235. package/src/channels/slack/types.ts +24 -7
  236. package/src/connections.ts +17 -5
  237. package/src/index.ts +5 -0
  238. package/src/internal/advertise-tools.ts +278 -0
  239. package/src/internal/cli-deploy.ts +11 -1
  240. package/src/internal/cli-manifest.ts +63 -0
  241. package/src/internal/cli-slack.ts +10 -1
  242. package/src/internal/convert-automation/convert-workflow.ts +2 -11
  243. package/src/internal/cursor/account-mcp.ts +208 -73
  244. package/src/internal/cursor/backend-client.ts +45 -11
  245. package/src/internal/cursor-account-mcp-auth.ts +83 -0
  246. package/src/internal/cursor-agent-template.ts +46 -0
  247. package/src/internal/deploy-client.ts +25 -6
  248. package/src/internal/deploy-manifest.ts +161 -0
  249. package/src/internal/deployment-scm-event-stream.ts +593 -0
  250. package/src/internal/discovery.ts +132 -3
  251. package/src/internal/init-project.ts +18 -153
  252. package/src/internal/init-scaffold.ts +176 -0
  253. package/src/internal/inline-stdio-mcp.ts +13 -0
  254. package/src/internal/json-schema.ts +24 -3
  255. package/src/internal/mcp-endpoint.ts +6 -0
  256. package/src/internal/mcp-host.ts +70 -9
  257. package/src/internal/mcp-oauth.ts +55 -1
  258. package/src/internal/package-inline-stdio.ts +5 -1
  259. package/src/internal/review-comments.ts +245 -0
  260. package/src/internal/sanitize-mcp-tool-name.ts +33 -0
  261. package/src/internal/sdk-runner.ts +50 -11
  262. package/src/internal/server.ts +19 -0
  263. package/src/internal/session-engine.ts +392 -19
  264. package/src/internal/shallow-clone.ts +595 -0
  265. package/src/internal/slack-provision-client.ts +3 -0
  266. package/src/internal/terminal-style.ts +67 -1
  267. package/src/internal/turn-lease-store.ts +147 -0
  268. package/src/internal/workspace.ts +33 -0
  269. package/src/storage-protocol.ts +247 -0
  270. package/src/storage.ts +16 -110
  271. package/src/tools.ts +7 -0
  272. package/src/types.ts +149 -7
  273. package/templates/agentic-owners/agent/agent.ts +3 -0
  274. package/templates/demo/agent/agent.ts +3 -0
  275. package/templates/pr-autofixer/agent/lib/drive-schemas.ts +4 -1
  276. package/templates/security-reviewer/agent/agent.ts +3 -0
  277. package/templates/triage/agent/agent.ts +3 -0
  278. package/dist/docs/assets/chunks/@localSearchIndexroot.5ozYNf74.js +0 -1
  279. package/dist/docs/assets/reference_connections.md.CSQiNzre.lean.js +0 -1
  280. package/dist/docs/assets/troubleshooting.md.CuVztCKs.js +0 -1
  281. package/dist/playground/assets/index-B4rcOgeP.css +0 -1
  282. package/dist/playground/assets/index-DXcY42Jv.js +0 -85
  283. /package/dist/docs/assets/{concepts.md.CqOsxbMU.lean.js → concepts.md.BXAm6G-C.lean.js} +0 -0
  284. /package/dist/docs/assets/{guides_mcp-oauth.md.C7G7IykG.lean.js → guides_mcp-oauth.md.CzEB6RaG.lean.js} +0 -0
  285. /package/dist/docs/assets/{reference_sessions.md.DBVFi2Sx.lean.js → reference_sessions.md.B0DdlM-K.lean.js} +0 -0
  286. /package/dist/docs/assets/{troubleshooting.md.CuVztCKs.lean.js → troubleshooting.md.DQ-hYuZT.lean.js} +0 -0
@@ -0,0 +1,593 @@
1
+ /**
2
+ * Deployment-scoped SCM event intake for hosted multi-tenant agents.
3
+ *
4
+ * Instead of receiving HMAC-signed webhook forwards from the backend (which
5
+ * required a shared `GITHUB_WEBHOOK_SECRET` / forward-secret pair), the
6
+ * engine pulls its deliveries from the control plane's deployment-scoped
7
+ * event stream:
8
+ *
9
+ * GET /internal/agent-serve/automation-agent-sdk/events
10
+ *
11
+ * authenticated with the engine pod's `CURSOR_API_KEY` — the same
12
+ * credential (and server-side deployment allowlist) that authorizes the
13
+ * consuming agent's customer list read. The backend writes each envelope's
14
+ * `installationId` itself from the verified GitHub delivery, so agent-side
15
+ * tenant binding does not depend on an HMAC-covered payload.
16
+ *
17
+ * Each envelope is stubbed into a GitHub-shaped `pull_request` webhook body
18
+ * and POSTed to this process's own github channel route over loopback
19
+ * (admitted by the channel's no-secret `localDevStrict` policy), so the
20
+ * consuming agent's whole pipeline — Zod parse, cursorAccount credential
21
+ * lease, customer routing, tools — runs unchanged. Loopback (rather than
22
+ * the in-process `handleAgentServeTrigger` fan-out the `--cursor-events`
23
+ * relay uses) is deliberate: this stream is started from a channel's
24
+ * `onStart`, which cannot reach the serve-level mount/handler wiring.
25
+ *
26
+ * Offsets are client-owned (`<ms>-<seq>` Redis stream ids) and persisted
27
+ * under the state dir so a restart resumes where it left off instead of
28
+ * dropping deliveries (the old push path was fire-and-forget).
29
+ *
30
+ * INTERNAL: first-party host plumbing shared by agents in `factory/*`.
31
+ * Consumed via relative source imports, following
32
+ * `tenant-mcp/run-mcp-registry.ts`. Policy (which deliveries to act on,
33
+ * tenant routing) stays in the consuming agent; this module is transport
34
+ * only.
35
+ */
36
+
37
+ // Node builtins only: hosted engines install the consuming agent's directory
38
+ // alone and load this file as raw TypeScript source, so third-party imports
39
+ // (even zod) do not resolve from here. See the tenant-mcp internals for the
40
+ // same constraint.
41
+ import { createHmac } from "node:crypto";
42
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
43
+ import { join } from "node:path";
44
+
45
+ const DEFAULT_BASE_URL = "https://api.cursor.com";
46
+ const EVENTS_PATH = "/internal/agent-serve/automation-agent-sdk/events";
47
+ const POLL_WAIT_MS = 20_000;
48
+ const POLL_LIMIT = 100;
49
+ const MIN_BACKOFF_MS = 1_000;
50
+ const MAX_BACKOFF_MS = 30_000;
51
+ // Bounded self-dispatch retry before the offset advances past a delivery
52
+ // (mirrors CursorRelayCore): transient loopback failures (connection
53
+ // reset, channel briefly unavailable at startup) get a few tries; a
54
+ // poison delivery still costs only DISPATCH_ATTEMPTS and is then skipped.
55
+ const DISPATCH_ATTEMPTS = 3;
56
+ const DISPATCH_RETRY_DELAYS_MS = [250, 1_000];
57
+ /** Auth/gate failures (401/403/404) are config problems; poll slowly. */
58
+ const AUTH_FAILURE_BACKOFF_MS = 60_000;
59
+
60
+ const OFFSET_SNAPSHOT_VERSION = 1 as const;
61
+
62
+ export interface AgentSdkScmPullRequestMeta {
63
+ number: number;
64
+ url: string;
65
+ title: string;
66
+ body?: string | null | undefined;
67
+ draft?: boolean | undefined;
68
+ merged?: boolean | undefined;
69
+ authorLogin?: string | undefined;
70
+ headRef: string;
71
+ headSha: string;
72
+ baseRef: string;
73
+ baseSha?: string | undefined;
74
+ }
75
+
76
+ /** One entry from the deployment-scoped control-plane event stream. */
77
+ export interface AgentSdkScmEventEnvelope {
78
+ provider: "github";
79
+ eventName: string;
80
+ deliveryId: string;
81
+ repo: string;
82
+ receivedAtMs: number;
83
+ action: string;
84
+ /** Tenant-binding material, written by the control plane. */
85
+ installationId: number;
86
+ pullRequest: AgentSdkScmPullRequestMeta;
87
+ occurredAtMs?: number | undefined;
88
+ }
89
+
90
+ function isRecord(value: unknown): value is Record<string, unknown> {
91
+ return typeof value === "object" && value !== null && !Array.isArray(value);
92
+ }
93
+
94
+ function nonEmptyString(value: unknown): string | undefined {
95
+ return typeof value === "string" && value.length > 0 ? value : undefined;
96
+ }
97
+
98
+ function optionalString(value: unknown): value is string | undefined {
99
+ return value === undefined || typeof value === "string";
100
+ }
101
+
102
+ function optionalBoolean(value: unknown): value is boolean | undefined {
103
+ return value === undefined || typeof value === "boolean";
104
+ }
105
+
106
+ function parsePullRequestMeta(
107
+ value: unknown
108
+ ): AgentSdkScmPullRequestMeta | undefined {
109
+ if (!isRecord(value)) {
110
+ return undefined;
111
+ }
112
+ const number = value.number;
113
+ const url = nonEmptyString(value.url);
114
+ const headRef = nonEmptyString(value.headRef);
115
+ const headSha = nonEmptyString(value.headSha);
116
+ const baseRef = nonEmptyString(value.baseRef);
117
+ if (
118
+ typeof number !== "number" ||
119
+ !Number.isInteger(number) ||
120
+ number <= 0 ||
121
+ url === undefined ||
122
+ typeof value.title !== "string" ||
123
+ !(
124
+ value.body === undefined ||
125
+ value.body === null ||
126
+ typeof value.body === "string"
127
+ ) ||
128
+ !optionalBoolean(value.draft) ||
129
+ !optionalBoolean(value.merged) ||
130
+ !optionalString(value.authorLogin) ||
131
+ headRef === undefined ||
132
+ headSha === undefined ||
133
+ baseRef === undefined ||
134
+ !optionalString(value.baseSha)
135
+ ) {
136
+ return undefined;
137
+ }
138
+ return {
139
+ number,
140
+ url,
141
+ title: value.title,
142
+ body: value.body,
143
+ draft: value.draft,
144
+ merged: value.merged,
145
+ authorLogin: value.authorLogin,
146
+ headRef,
147
+ headSha,
148
+ baseRef,
149
+ baseSha: value.baseSha,
150
+ };
151
+ }
152
+
153
+ /** Unknown envelope shapes return undefined (skipped, forward compat). */
154
+ export function parseAgentSdkScmEventEnvelope(
155
+ value: unknown
156
+ ): AgentSdkScmEventEnvelope | undefined {
157
+ if (!isRecord(value) || value.provider !== "github") {
158
+ return undefined;
159
+ }
160
+ const eventName = nonEmptyString(value.eventName);
161
+ const deliveryId = nonEmptyString(value.deliveryId);
162
+ const repo = nonEmptyString(value.repo);
163
+ const action = nonEmptyString(value.action);
164
+ const installationId = value.installationId;
165
+ const pullRequest = parsePullRequestMeta(value.pullRequest);
166
+ if (
167
+ eventName === undefined ||
168
+ deliveryId === undefined ||
169
+ repo === undefined ||
170
+ typeof value.receivedAtMs !== "number" ||
171
+ action === undefined ||
172
+ typeof installationId !== "number" ||
173
+ !Number.isInteger(installationId) ||
174
+ installationId <= 0 ||
175
+ pullRequest === undefined ||
176
+ !(
177
+ value.occurredAtMs === undefined || typeof value.occurredAtMs === "number"
178
+ )
179
+ ) {
180
+ return undefined;
181
+ }
182
+ return {
183
+ provider: "github",
184
+ eventName,
185
+ deliveryId,
186
+ repo,
187
+ receivedAtMs: value.receivedAtMs,
188
+ action,
189
+ installationId,
190
+ pullRequest,
191
+ occurredAtMs: value.occurredAtMs,
192
+ };
193
+ }
194
+
195
+ type PollResponse = {
196
+ /** Unknown envelope shapes are skipped, not fatal (forward compat). */
197
+ events: Array<{ offset: string; envelope: unknown }>;
198
+ nextOffset: string;
199
+ gapDetected: boolean;
200
+ };
201
+
202
+ function parsePollResponse(value: unknown): PollResponse {
203
+ if (!isRecord(value) || !Array.isArray(value.events)) {
204
+ throw new Error("malformed poll response");
205
+ }
206
+ const events: PollResponse["events"] = [];
207
+ for (const entry of value.events) {
208
+ if (!isRecord(entry)) {
209
+ throw new Error("malformed poll response event");
210
+ }
211
+ const offset = nonEmptyString(entry.offset);
212
+ if (offset === undefined) {
213
+ throw new Error("malformed poll response event offset");
214
+ }
215
+ events.push({ offset, envelope: entry.envelope });
216
+ }
217
+ if (
218
+ typeof value.nextOffset !== "string" ||
219
+ typeof value.gapDetected !== "boolean"
220
+ ) {
221
+ throw new Error("malformed poll response fields");
222
+ }
223
+ return {
224
+ events,
225
+ nextOffset: value.nextOffset,
226
+ gapDetected: value.gapDetected,
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Marker key on self-dispatched webhook bodies so the consuming channel can
232
+ * tell stream deliveries apart from other senders (e.g. the serve host's
233
+ * cursorAccount SCM relay). Multi-tenant channels treat this stream as the
234
+ * single source and ignore unmarked deliveries — without that gate, one PR
235
+ * push reached the channel twice (relay + stream) and double-ran reviews.
236
+ */
237
+ const EVENT_STREAM_PAYLOAD_MARKER = "cursorDeploymentEventStream";
238
+
239
+ /** Whether a webhook body was self-dispatched by this event stream. */
240
+ export function isDeploymentEventStreamPayload(payload: unknown): boolean {
241
+ return isRecord(payload) && payload[EVENT_STREAM_PAYLOAD_MARKER] === true;
242
+ }
243
+
244
+ /**
245
+ * Stub a GitHub-shaped `pull_request` webhook body from an envelope —
246
+ * enough for agent-serve's pull_request Zod schema, plus `installation.id`
247
+ * so the channel's `ctx.installationId` tenant binding keeps working, plus
248
+ * the {@link isDeploymentEventStreamPayload} source marker.
249
+ * Pure — exported for tests.
250
+ */
251
+ export function envelopeToGitHubPayload(
252
+ envelope: AgentSdkScmEventEnvelope
253
+ ): Record<string, unknown> {
254
+ const slash = envelope.repo.indexOf("/");
255
+ const owner = slash === -1 ? "" : envelope.repo.slice(0, slash);
256
+ const name = slash === -1 ? envelope.repo : envelope.repo.slice(slash + 1);
257
+ const pr = envelope.pullRequest;
258
+ return {
259
+ action: envelope.action,
260
+ repository: {
261
+ full_name: envelope.repo,
262
+ name,
263
+ html_url: `https://github.com/${envelope.repo}`,
264
+ owner: { login: owner },
265
+ },
266
+ pull_request: {
267
+ number: pr.number,
268
+ html_url: pr.url,
269
+ title: pr.title,
270
+ body: pr.body,
271
+ draft: pr.draft,
272
+ merged: pr.merged,
273
+ user:
274
+ pr.authorLogin === undefined ? undefined : { login: pr.authorLogin },
275
+ head: { ref: pr.headRef, sha: pr.headSha },
276
+ base: { ref: pr.baseRef, sha: pr.baseSha },
277
+ },
278
+ installation: { id: envelope.installationId },
279
+ [EVENT_STREAM_PAYLOAD_MARKER]: true,
280
+ };
281
+ }
282
+
283
+ /** Durable offset snapshot under `<stateDir>/offset.json`. */
284
+ class OffsetStore {
285
+ constructor(private readonly stateDir: string) {}
286
+
287
+ private get filePath(): string {
288
+ return join(this.stateDir, "offset.json");
289
+ }
290
+
291
+ async load(): Promise<string | undefined> {
292
+ try {
293
+ const raw = await readFile(this.filePath, "utf8");
294
+ const parsed = JSON.parse(raw) as {
295
+ version?: number;
296
+ offset?: unknown;
297
+ };
298
+ if (
299
+ parsed.version === OFFSET_SNAPSHOT_VERSION &&
300
+ typeof parsed.offset === "string" &&
301
+ parsed.offset !== ""
302
+ ) {
303
+ return parsed.offset;
304
+ }
305
+ return undefined;
306
+ } catch {
307
+ return undefined;
308
+ }
309
+ }
310
+
311
+ async save(offset: string): Promise<void> {
312
+ await mkdir(this.stateDir, { recursive: true });
313
+ // Write-then-rename so a crash mid-write never corrupts the snapshot.
314
+ const tmpPath = `${this.filePath}.tmp`;
315
+ await writeFile(
316
+ tmpPath,
317
+ JSON.stringify(
318
+ {
319
+ version: OFFSET_SNAPSHOT_VERSION,
320
+ offset,
321
+ updatedAt: new Date().toISOString(),
322
+ },
323
+ null,
324
+ 2
325
+ ),
326
+ "utf8"
327
+ );
328
+ await rename(tmpPath, this.filePath);
329
+ }
330
+ }
331
+
332
+ export interface AgentSdkEventStreamOptions {
333
+ logger?: (line: string) => void;
334
+ env?: NodeJS.ProcessEnv;
335
+ /** Injected for tests. */
336
+ fetch?: typeof fetch;
337
+ /** Injected for tests (default `process.argv`). */
338
+ argv?: readonly string[];
339
+ /**
340
+ * Prefix for the consumer-scoped env overrides
341
+ * `<prefix>_SELF_WEBHOOK_URL` and `<prefix>_EVENTS_STATE_DIR`
342
+ * (default `AGENT_SERVE`; e.g. the security reviewer passes
343
+ * `SECURITY_REVIEWER` to keep its existing deployment env vars working).
344
+ */
345
+ envPrefix?: string;
346
+ /** Log-line prefix (default `agent-sdk/deployment-events`). */
347
+ logLabel?: string;
348
+ /**
349
+ * Offset state directory name under `<cwd>/.agent-serve`
350
+ * (default `deployment-events`). Consumers with persisted offsets must
351
+ * keep their historical name or a restart re-anchors at `$`.
352
+ */
353
+ stateDirName?: string;
354
+ }
355
+
356
+ /**
357
+ * Port the serve process is actually listening on, read from its own argv.
358
+ *
359
+ * The channel runs inside the serve process, so `--port <n>` / `--port=<n>`
360
+ * on the command line is authoritative. Hosted-platform engines start with
361
+ * `--port 26100` and export no `PORT` env, so guessing a fixed default posts
362
+ * self-dispatches at a port nothing listens on ("fetch failed" for every
363
+ * delivery).
364
+ */
365
+ export function servePortFromArgv(argv: readonly string[]): string | undefined {
366
+ const flagIndex = argv.lastIndexOf("--port");
367
+ if (flagIndex !== -1) {
368
+ const value = argv[flagIndex + 1]?.trim();
369
+ if (value !== undefined && /^\d+$/.test(value)) {
370
+ return value;
371
+ }
372
+ }
373
+ for (let i = argv.length - 1; i >= 0; i--) {
374
+ const arg = argv[i];
375
+ if (arg !== undefined && arg.startsWith("--port=")) {
376
+ const value = arg.slice("--port=".length).trim();
377
+ if (/^\d+$/.test(value)) {
378
+ return value;
379
+ }
380
+ }
381
+ }
382
+ return undefined;
383
+ }
384
+
385
+ export interface AgentSdkEventStream {
386
+ stop(): Promise<void>;
387
+ }
388
+
389
+ function delay(ms: number, signal: AbortSignal): Promise<void> {
390
+ return new Promise((resolve) => {
391
+ const timer = setTimeout(finish, ms);
392
+ timer.unref?.();
393
+ signal.addEventListener("abort", finish, { once: true });
394
+ function finish(): void {
395
+ clearTimeout(timer);
396
+ signal.removeEventListener("abort", finish);
397
+ resolve();
398
+ }
399
+ });
400
+ }
401
+
402
+ /**
403
+ * Start the pull loop. Returns undefined (with a log line) when required
404
+ * config is missing so `serve` still starts — mirroring how other relays
405
+ * degrade instead of failing the host.
406
+ */
407
+ export function startAgentSdkEventStream(
408
+ options: AgentSdkEventStreamOptions = {}
409
+ ): AgentSdkEventStream | undefined {
410
+ const env = options.env ?? process.env;
411
+ const logLabel = options.logLabel ?? "agent-sdk/deployment-events";
412
+ const rawLog = options.logger ?? (() => {});
413
+ const log = (message: string): void => {
414
+ rawLog(`[${logLabel}] ${message}`);
415
+ };
416
+ const doFetch = options.fetch ?? fetch;
417
+ const envPrefix = options.envPrefix ?? "AGENT_SERVE";
418
+
419
+ const apiKey = env.CURSOR_API_KEY?.trim();
420
+ if (apiKey === undefined || apiKey === "") {
421
+ log("CURSOR_API_KEY missing; deployment event stream disabled");
422
+ return undefined;
423
+ }
424
+ const baseRaw = (env.CURSOR_API_BASE_URL ?? DEFAULT_BASE_URL).trim();
425
+ const base = baseRaw.endsWith("/") ? baseRaw.slice(0, -1) : baseRaw;
426
+ // Loopback self-dispatch into this process's github channel route, single
427
+ // agent mounting assumed (multi-agent mounts need the slugged path via the
428
+ // env var override). Port resolution: explicit env override, then `PORT`,
429
+ // then the serve process's own `--port` argv (hosted-platform engines run
430
+ // `serve --port 26100` and export no PORT), then the :3000 default of the
431
+ // corp hosted image.
432
+ const selfUrl =
433
+ env[`${envPrefix}_SELF_WEBHOOK_URL`]?.trim() ||
434
+ `http://127.0.0.1:${
435
+ env.PORT?.trim() ||
436
+ servePortFromArgv(options.argv ?? process.argv) ||
437
+ "3000"
438
+ }/v1/channels/github`;
439
+ log(`self-dispatch url: ${selfUrl}`);
440
+ const stateDir =
441
+ env[`${envPrefix}_EVENTS_STATE_DIR`]?.trim() ||
442
+ join(
443
+ process.cwd(),
444
+ ".agent-serve",
445
+ options.stateDirName ?? "deployment-events"
446
+ );
447
+ const offsetStore = new OffsetStore(stateDir);
448
+ const abort = new AbortController();
449
+
450
+ // Transition aid: while the deployment still sets GITHUB_WEBHOOK_SECRET,
451
+ // the channel's auth is HMAC-only (allowAll + signature check), so sign
452
+ // the self-dispatch with it. Once the secret is removed from the env,
453
+ // the channel falls back to its loopback policy and the unsigned POST is
454
+ // admitted — either configuration works, so rollout order doesn't matter.
455
+ const webhookSecret = env.GITHUB_WEBHOOK_SECRET?.trim();
456
+
457
+ async function dispatch(envelope: AgentSdkScmEventEnvelope): Promise<void> {
458
+ const body = JSON.stringify(envelopeToGitHubPayload(envelope));
459
+ const headers: Record<string, string> = {
460
+ "content-type": "application/json",
461
+ "x-github-event": envelope.eventName,
462
+ "x-github-delivery": envelope.deliveryId,
463
+ };
464
+ if (webhookSecret !== undefined && webhookSecret !== "") {
465
+ const signature = createHmac("sha256", webhookSecret)
466
+ .update(body)
467
+ .digest("hex");
468
+ headers["x-hub-signature-256"] = `sha256=${signature}`;
469
+ }
470
+ const response = await doFetch(selfUrl, {
471
+ method: "POST",
472
+ headers,
473
+ body,
474
+ signal: AbortSignal.timeout(POLL_WAIT_MS),
475
+ });
476
+ void response.body?.cancel().catch(() => {});
477
+ if (!response.ok) {
478
+ throw new Error(`self dispatch returned ${response.status}`);
479
+ }
480
+ }
481
+
482
+ async function run(): Promise<void> {
483
+ let after = (await offsetStore.load()) ?? "$";
484
+ let backoffMs = MIN_BACKOFF_MS;
485
+ log(`pulling ${base}${EVENTS_PATH} (after=${after})`);
486
+ while (!abort.signal.aborted) {
487
+ let parsed: PollResponse;
488
+ try {
489
+ const query = new URLSearchParams({
490
+ after,
491
+ limit: String(POLL_LIMIT),
492
+ waitMs: String(POLL_WAIT_MS),
493
+ });
494
+ const response = await doFetch(`${base}${EVENTS_PATH}?${query}`, {
495
+ headers: { authorization: `Bearer ${apiKey}` },
496
+ signal: AbortSignal.any([
497
+ abort.signal,
498
+ AbortSignal.timeout(POLL_WAIT_MS + 10_000),
499
+ ]),
500
+ });
501
+ if (!response.ok) {
502
+ void response.body?.cancel().catch(() => {});
503
+ const authFailure = [401, 403, 404].includes(response.status);
504
+ log(`poll failed (${response.status}); backing off`);
505
+ await delay(
506
+ authFailure ? AUTH_FAILURE_BACKOFF_MS : backoffMs,
507
+ abort.signal
508
+ );
509
+ backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
510
+ continue;
511
+ }
512
+ parsed = parsePollResponse(await response.json());
513
+ } catch (error) {
514
+ if (abort.signal.aborted) return;
515
+ log(
516
+ `poll error (${
517
+ error instanceof Error ? error.message : String(error)
518
+ }); backing off`
519
+ );
520
+ await delay(backoffMs, abort.signal);
521
+ backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
522
+ continue;
523
+ }
524
+ backoffMs = MIN_BACKOFF_MS;
525
+ if (parsed.gapDetected) {
526
+ log(
527
+ "gap detected: some deliveries were trimmed before this consumer read them"
528
+ );
529
+ }
530
+ for (const entry of parsed.events) {
531
+ if (abort.signal.aborted) return;
532
+ const envelope = parseAgentSdkScmEventEnvelope(entry.envelope);
533
+ if (envelope !== undefined) {
534
+ for (let attempt = 1; attempt <= DISPATCH_ATTEMPTS; attempt++) {
535
+ if (abort.signal.aborted) return;
536
+ try {
537
+ await dispatch(envelope);
538
+ log(
539
+ `dispatched ${envelope.repo}#${envelope.pullRequest.number} (${envelope.action}, delivery=${envelope.deliveryId})`
540
+ );
541
+ break;
542
+ } catch (error) {
543
+ if (abort.signal.aborted) return;
544
+ const message =
545
+ error instanceof Error ? error.message : String(error);
546
+ if (attempt === DISPATCH_ATTEMPTS) {
547
+ // Skip, don't wedge: the offset advances so one poison
548
+ // delivery cannot stall the stream.
549
+ log(
550
+ `dispatch failed for ${envelope.deliveryId} after ${attempt} attempts: ${message}`
551
+ );
552
+ break;
553
+ }
554
+ log(
555
+ `dispatch attempt ${attempt} failed for ${envelope.deliveryId}: ${message}; retrying`
556
+ );
557
+ await delay(
558
+ DISPATCH_RETRY_DELAYS_MS[
559
+ Math.min(attempt - 1, DISPATCH_RETRY_DELAYS_MS.length - 1)
560
+ ],
561
+ abort.signal
562
+ );
563
+ }
564
+ }
565
+ }
566
+ after = entry.offset;
567
+ await offsetStore.save(after).catch(() => {});
568
+ }
569
+ if (parsed.events.length === 0 && parsed.nextOffset !== after) {
570
+ // "$" anchor resolution. An empty stream anchors at "" (read from
571
+ // the beginning), which is valid in-memory but not worth persisting
572
+ // (a restart re-anchors at "$" anyway).
573
+ after = parsed.nextOffset;
574
+ if (after !== "") {
575
+ await offsetStore.save(after).catch(() => {});
576
+ }
577
+ }
578
+ }
579
+ }
580
+
581
+ const done = run().catch((error: unknown) => {
582
+ log(
583
+ `loop crashed: ${error instanceof Error ? error.message : String(error)}`
584
+ );
585
+ });
586
+
587
+ return {
588
+ stop: async () => {
589
+ abort.abort();
590
+ await done;
591
+ },
592
+ };
593
+ }