@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
@@ -218,6 +218,9 @@ function buildToolMcpServer(
218
218
  name: tool.name,
219
219
  description: tool.description,
220
220
  inputSchema: tool.inputSchema,
221
+ ...(tool.outputSchema === undefined
222
+ ? {}
223
+ : { outputSchema: tool.outputSchema }),
221
224
  })),
222
225
  })
223
226
  );
@@ -298,6 +301,9 @@ function buildConnectionBridgeMcpServer(
298
301
  ? {}
299
302
  : { description: tool.description }),
300
303
  inputSchema: tool.inputSchema ?? { type: "object" },
304
+ ...(tool.outputSchema === undefined
305
+ ? {}
306
+ : { outputSchema: tool.outputSchema }),
301
307
  })),
302
308
  };
303
309
  });
@@ -29,6 +29,18 @@ interface OpenedConnection {
29
29
  close: () => Promise<void>;
30
30
  }
31
31
 
32
+ /**
33
+ * A non-caching view of one connection: every operation opens a fresh
34
+ * client and closes it afterwards. See {@link McpHost.oneOff}.
35
+ */
36
+ export interface HostMcpOneOff {
37
+ listTools(): Promise<HostMcpToolInfo[]>;
38
+ callTool(
39
+ toolName: string,
40
+ args?: Record<string, unknown>
41
+ ): Promise<HostMcpCallResult>;
42
+ }
43
+
32
44
  export class McpHost implements HostMcpRegistry {
33
45
  private readonly byName = new Map<string, DiscoveredConnection>();
34
46
  private readonly clients = new Map<string, Promise<OpenedConnection>>();
@@ -54,18 +66,47 @@ export class McpHost implements HostMcpRegistry {
54
66
  async listTools(connectionName: string): Promise<HostMcpToolInfo[]> {
55
67
  return this.withClient(connectionName, async (client) => {
56
68
  const listed = await client.listTools();
57
- return listed.tools.map((tool) => ({
58
- name: tool.name,
59
- ...(tool.description === undefined
60
- ? {}
61
- : { description: tool.description }),
62
- ...(tool.inputSchema === undefined
63
- ? {}
64
- : { inputSchema: tool.inputSchema as JsonObject }),
65
- }));
69
+ return listed.tools.map(toToolInfo);
66
70
  });
67
71
  }
68
72
 
73
+ /**
74
+ * A one-off view of a connection that is not (or not exactly) in the
75
+ * registry — e.g. a registered connection with per-session auth headers
76
+ * substituted in. Every operation opens a fresh client and closes it
77
+ * afterwards: the registry's cached per-name client would leak one
78
+ * session's identity into another, and stateless streamable-HTTP
79
+ * servers answer a fresh initialize cheaply. Results ride the same
80
+ * mapping as registry calls.
81
+ */
82
+ oneOff(connection: DiscoveredConnection): HostMcpOneOff {
83
+ const withFreshClient = async <T>(
84
+ run: (client: Client) => Promise<T>
85
+ ): Promise<T> => {
86
+ const opened = await openConnection(connection);
87
+ try {
88
+ return await run(opened.client);
89
+ } finally {
90
+ await opened.close();
91
+ }
92
+ };
93
+ return {
94
+ listTools: () =>
95
+ withFreshClient(async (client) => {
96
+ const listed = await client.listTools();
97
+ return listed.tools.map(toToolInfo);
98
+ }),
99
+ callTool: (toolName, args = {}) =>
100
+ withFreshClient(async (client) => {
101
+ const result = await client.callTool({
102
+ name: toolName,
103
+ arguments: args,
104
+ });
105
+ return normalizeCallResult(result);
106
+ }),
107
+ };
108
+ }
109
+
69
110
  async callTool(
70
111
  connectionName: string,
71
112
  toolName: string,
@@ -172,6 +213,26 @@ export function isStaleMcpSessionError(error: unknown): boolean {
172
213
  return code === 404;
173
214
  }
174
215
 
216
+ function toToolInfo(tool: {
217
+ name: string;
218
+ description?: string;
219
+ inputSchema?: unknown;
220
+ outputSchema?: unknown;
221
+ }): HostMcpToolInfo {
222
+ return {
223
+ name: tool.name,
224
+ ...(tool.description === undefined
225
+ ? {}
226
+ : { description: tool.description }),
227
+ ...(tool.inputSchema === undefined
228
+ ? {}
229
+ : { inputSchema: tool.inputSchema as JsonObject }),
230
+ ...(tool.outputSchema === undefined
231
+ ? {}
232
+ : { outputSchema: tool.outputSchema as JsonObject }),
233
+ };
234
+ }
235
+
175
236
  async function openConnection(
176
237
  connection: DiscoveredConnection
177
238
  ): Promise<OpenedConnection> {
@@ -259,8 +259,52 @@ type ProviderArgs = {
259
259
  onRedirect?: (url: URL) => void;
260
260
  tokens?: OAuthTokens;
261
261
  clientInformation?: OAuthClientInformationFull;
262
+ /**
263
+ * True when authorize/token requests must NOT carry the RFC 8707
264
+ * `resource` indicator, read off the server's protected-resource
265
+ * metadata (`cursor_omit_resource_indicator`). Entra rejects the
266
+ * indicator with AADSTS9010010, so Microsoft 365 flows through Cursor's
267
+ * REST-as-MCP translator fail pre-auth without this.
268
+ */
269
+ omitResourceIndicator?: boolean;
262
270
  };
263
271
 
272
+ /**
273
+ * Reads the Cursor REST-as-MCP translator's OAuth quirk off the resource's
274
+ * RFC 9728 protected-resource metadata. Deliberately self-contained
275
+ * (`@cursor/july` publishes to public npm with no private workspace
276
+ * dependencies — see `waitForMcpOAuthCallback`): matches the translator
277
+ * path shape on any origin so local translators work, and fails to the
278
+ * default behavior (indicator kept) when the PRM is unreachable.
279
+ */
280
+ export async function fetchOmitResourceIndicatorFromPrm(
281
+ resourceUrl: string,
282
+ fetchImpl: typeof fetch = fetch
283
+ ): Promise<boolean> {
284
+ let url: URL;
285
+ try {
286
+ url = new URL(resourceUrl);
287
+ } catch {
288
+ return false;
289
+ }
290
+ if (!/^\/rest-mcp\/[^/]+\/mcp\/?$/.test(url.pathname)) {
291
+ return false;
292
+ }
293
+ const prmUrl = `${url.origin}/.well-known/oauth-protected-resource${url.pathname.replace(/\/+$/, "")}`;
294
+ try {
295
+ const response = await fetchImpl(prmUrl);
296
+ if (!response.ok) {
297
+ return false;
298
+ }
299
+ const document = (await response.json()) as {
300
+ cursor_omit_resource_indicator?: unknown;
301
+ };
302
+ return document.cursor_omit_resource_indicator === true;
303
+ } catch {
304
+ return false;
305
+ }
306
+ }
307
+
264
308
  export class HostMcpOAuthProvider implements OAuthClientProvider {
265
309
  private tokensValue: OAuthTokens | undefined;
266
310
  private clientInfo: OAuthClientInformationFull | undefined;
@@ -268,11 +312,19 @@ export class HostMcpOAuthProvider implements OAuthClientProvider {
268
312
  private redirect: URL | undefined;
269
313
  private readonly meta: OAuthClientMetadata;
270
314
  private readonly args: ProviderArgs;
315
+ /**
316
+ * Assigned only when the PRM sets `cursor_omit_resource_indicator`; left
317
+ * absent otherwise so the SDK's default resource validation applies.
318
+ */
319
+ readonly validateResourceURL: (() => Promise<URL | undefined>) | undefined;
271
320
 
272
321
  constructor(args: ProviderArgs) {
273
322
  this.args = args;
274
323
  this.tokensValue = args.tokens;
275
324
  this.clientInfo = args.clientInformation;
325
+ if (args.omitResourceIndicator === true) {
326
+ this.validateResourceURL = async () => undefined;
327
+ }
276
328
  this.meta = {
277
329
  redirect_uris: [args.redirectUrl ?? MCP_OAUTH_CALLBACK_URL],
278
330
  token_endpoint_auth_method: "none",
@@ -366,9 +418,10 @@ export async function createHostMcpOAuthProvider(args: {
366
418
  onRedirect?: (url: URL) => void;
367
419
  }): Promise<HostMcpOAuthProvider> {
368
420
  const store = args.store ?? new FileMcpOAuthStore();
369
- const [fileTokens, fileClient] = await Promise.all([
421
+ const [fileTokens, fileClient, omitResourceIndicator] = await Promise.all([
370
422
  store.loadTokens(args.identifier, args.resourceUrl),
371
423
  store.loadClientInformation(args.identifier, args.resourceUrl),
424
+ fetchOmitResourceIndicatorFromPrm(args.resourceUrl),
372
425
  ]);
373
426
  return new HostMcpOAuthProvider({
374
427
  identifier: args.identifier,
@@ -377,6 +430,7 @@ export async function createHostMcpOAuthProvider(args: {
377
430
  tokens: fileTokens ?? tokensFromEnv(args.identifier),
378
431
  clientInformation: fileClient ?? clientIdFromEnv(args.identifier),
379
432
  onRedirect: args.onRedirect,
433
+ omitResourceIndicator,
380
434
  });
381
435
  }
382
436
 
@@ -14,7 +14,7 @@ import type {
14
14
  JsonSchemaObject,
15
15
  } from "../types.js";
16
16
  import type { InlineStdioCatalogEntry } from "./inline-stdio-mcp.js";
17
- import { toolInputJsonSchema } from "./json-schema.js";
17
+ import { toolInputJsonSchema, toolOutputJsonSchema } from "./json-schema.js";
18
18
 
19
19
  const require = createRequire(import.meta.url);
20
20
 
@@ -35,9 +35,13 @@ function jsonSchemaCatalog(
35
35
  ): Record<string, InlineStdioCatalogEntry> {
36
36
  const catalog: Record<string, InlineStdioCatalogEntry> = {};
37
37
  for (const [name, tool] of Object.entries(tools)) {
38
+ const outputSchema = toolOutputJsonSchema(tool.outputSchema);
38
39
  catalog[name] = {
39
40
  description: tool.description,
40
41
  inputSchema: toolInputJsonSchema(tool.inputSchema) as JsonSchemaObject,
42
+ ...(outputSchema === undefined
43
+ ? {}
44
+ : { outputSchema: outputSchema as JsonSchemaObject }),
41
45
  };
42
46
  }
43
47
  return catalog;
@@ -9,6 +9,21 @@ export type ReviewCommentPostResult = {
9
9
  mode: "review_with_inline_comments" | "review_body_only" | "skipped";
10
10
  inlineCommentCount: number;
11
11
  detail: string;
12
+ /**
13
+ * Ids of the inline comments this post created, when the publisher could
14
+ * read them back. Callers that later re-read a PR's comments use these as
15
+ * the provenance allowlist: a comment id we did not create is not ours,
16
+ * no matter what its body looks like. Empty when the publisher cannot
17
+ * resolve them (it never guesses).
18
+ */
19
+ postedComments?: PostedReviewComment[];
20
+ };
21
+
22
+ /** One inline comment this agent created, as the SCM assigned it. */
23
+ export type PostedReviewComment = {
24
+ id: number;
25
+ file: string;
26
+ line: number;
12
27
  };
13
28
 
14
29
  export type ReviewInlineComment = {
@@ -41,6 +56,175 @@ export type ReviewCommentPublisherImplementation = {
41
56
  postReview: (args: ReviewCommentPostArgs) => Promise<ReviewCommentPostResult>;
42
57
  };
43
58
 
59
+ /**
60
+ * SCMs this module can read review comments back from. Same growth seam as
61
+ * `shallow-clone.ts`'s {@link import("./shallow-clone.js").ScmProvider}:
62
+ * adding GitLab/Bitbucket/Origin means a resolver case plus a reader
63
+ * implementation, and nothing above this line changes.
64
+ */
65
+ export type ScmReviewProvider = "github";
66
+
67
+ /** Which SCM's PR to read review comments from. */
68
+ export type ScmReviewTarget = {
69
+ provider: ScmReviewProvider;
70
+ /** `owner/name`. */
71
+ repo: string;
72
+ prNumber: number;
73
+ };
74
+
75
+ /**
76
+ * One existing inline review comment, provider-neutral.
77
+ *
78
+ * Field names follow Bugbot's `ExistingInlineComment`
79
+ * (`backend/server/src/bugbot/review/applyBugbotPRReview.ts`), which is the
80
+ * shape its resolution judge consumes after normalizing each provider's
81
+ * `getPRComments` output — so the same review logic reads the same way here.
82
+ */
83
+ export type ExistingReviewComment = {
84
+ id: number;
85
+ file: string;
86
+ startLine: number;
87
+ endLine: number;
88
+ body: string;
89
+ };
90
+
91
+ export type GetReviewCommentsArgs = {
92
+ target: ScmReviewTarget;
93
+ /** Comment ids to fetch; ids the SCM no longer has are omitted. */
94
+ commentIds: readonly number[];
95
+ signal?: AbortSignal;
96
+ };
97
+
98
+ /**
99
+ * Read side of the review-comment seam, mirroring
100
+ * {@link ReviewCommentPublisherImplementation}. Bugbot's equivalent is
101
+ * `IGitProvider.getPRComments`.
102
+ *
103
+ * By id rather than by listing: callers only ever want comments they
104
+ * recorded creating, and a busy PR's comment list runs to thousands, where
105
+ * any page cap silently drops exactly the older comments a re-review needs.
106
+ */
107
+ export type ReviewCommentReaderImplementation = {
108
+ getReviewComments: (
109
+ args: GetReviewCommentsArgs
110
+ ) => Promise<ExistingReviewComment[]>;
111
+ };
112
+
113
+ /** Parse a PR URL into the SCM target to read comments from. */
114
+ export function resolveScmReviewTarget(
115
+ prUrl: string
116
+ ): ScmReviewTarget | undefined {
117
+ const parsed = parseGithubPrUrl(prUrl);
118
+ return parsed.kind === "valid"
119
+ ? {
120
+ provider: "github",
121
+ repo: parsed.value.repo,
122
+ prNumber: parsed.value.prNumber,
123
+ }
124
+ : undefined;
125
+ }
126
+
127
+ /**
128
+ * Reader for a resolved target, mirroring Bugbot's `createPrGitProvider`
129
+ * dispatch. Returns undefined when the host cannot serve that provider (no
130
+ * GitHub client bound), so callers degrade instead of throwing.
131
+ */
132
+ export function createReviewCommentReader(args: {
133
+ target: ScmReviewTarget;
134
+ github?: HostGitHubClient;
135
+ }): ReviewCommentReaderImplementation | undefined {
136
+ switch (args.target.provider) {
137
+ case "github":
138
+ return args.github === undefined
139
+ ? undefined
140
+ : createHostGithubReviewCommentReader(args.github);
141
+ }
142
+ }
143
+
144
+ /** Cap on pages pulled per PR; GitHub serves 100 comments per page. */
145
+ const REVIEW_COMMENTS_MAX_PAGES = 5;
146
+ const REVIEW_COMMENTS_PAGE_SIZE = 100;
147
+
148
+ /**
149
+ * Reads inline review comments through the host GitHub client (Cursor
150
+ * account lease, App installation token, or PAT).
151
+ */
152
+ export function createHostGithubReviewCommentReader(
153
+ github: HostGitHubClient
154
+ ): ReviewCommentReaderImplementation {
155
+ return {
156
+ async getReviewComments({ target, commentIds, signal }) {
157
+ const [owner, repo] = target.repo.split("/", 2);
158
+ if (owner === undefined || repo === undefined || repo === "") {
159
+ return [];
160
+ }
161
+ const octokit = await github.getOctokit();
162
+ const comments: ExistingReviewComment[] = [];
163
+ for (const commentId of commentIds) {
164
+ signal?.throwIfAborted();
165
+ try {
166
+ const response = await octokit.rest.pulls.getReviewComment({
167
+ owner,
168
+ repo,
169
+ comment_id: commentId,
170
+ });
171
+ const normalized = normalizeGithubReviewComment(response.data);
172
+ if (normalized !== undefined) {
173
+ comments.push(normalized);
174
+ }
175
+ } catch {
176
+ // Deleted or otherwise unreadable: that finding simply has no
177
+ // prior text to judge, which is the same as it not being there.
178
+ }
179
+ }
180
+ return comments;
181
+ },
182
+ };
183
+ }
184
+
185
+ type GithubReviewCommentLike = {
186
+ id: number;
187
+ path: string;
188
+ body: string;
189
+ line?: number | null;
190
+ start_line?: number | null;
191
+ original_line?: number | null;
192
+ original_start_line?: number | null;
193
+ };
194
+
195
+ /**
196
+ * GitHub review comment → {@link ExistingReviewComment}.
197
+ *
198
+ * Prefers the `original_*` fields — the path/lines/commit as of the review
199
+ * the comment belongs to. `line` drifts as the PR gains commits, so a
200
+ * caller correlating a comment with the review that produced it needs the
201
+ * original position, the same fields Bugbot carries on
202
+ * `PullRequestCommentOutput`.
203
+ */
204
+ export function normalizeGithubReviewComment(
205
+ comment: GithubReviewCommentLike
206
+ ): ExistingReviewComment | undefined {
207
+ // Take both lines from the same position set. Mixing them (original end
208
+ // line with the drifted current start line) would describe a range that
209
+ // never existed in either revision.
210
+ const original = comment.original_line ?? undefined;
211
+ const endLine = original ?? comment.line ?? undefined;
212
+ if (endLine === undefined || endLine === null) {
213
+ return undefined;
214
+ }
215
+ const startLine =
216
+ (original === undefined
217
+ ? comment.start_line
218
+ : comment.original_start_line) ?? endLine;
219
+ return {
220
+ id: comment.id,
221
+ file: comment.path,
222
+ startLine,
223
+ endLine,
224
+ body: comment.body,
225
+ };
226
+ }
227
+
44
228
  type ParsedGithubPrUrl = {
45
229
  repo: string;
46
230
  prNumber: number;
@@ -148,6 +332,15 @@ async function postReviewViaHostGithub(
148
332
  detail: htmlUrl
149
333
  ? `posted via host GitHub: ${htmlUrl}`
150
334
  : `posted via host GitHub with ${inlineCommentCount} inline comment(s)`,
335
+ postedComments: await readPostedComments({
336
+ octokit,
337
+ owner,
338
+ repo,
339
+ prNumber: request.prNumber,
340
+ reviewId: response.data.id,
341
+ expected: inlineCommentCount,
342
+ signal: args.signal,
343
+ }),
151
344
  };
152
345
  } catch (error) {
153
346
  if (args.signal?.aborted) {
@@ -159,6 +352,58 @@ async function postReviewViaHostGithub(
159
352
  }
160
353
  }
161
354
 
355
+ /**
356
+ * Ids of the inline comments a just-created review owns.
357
+ *
358
+ * `pulls.createReview` returns the review, not its comments, so they come
359
+ * from the review-scoped listing. Scoping to the review matters: the
360
+ * PR-wide list is ordered oldest-first and runs to thousands on a busy PR,
361
+ * so the comments we just created are the ones a page cap would drop.
362
+ * Best-effort: any failure yields an empty list, which costs the next run
363
+ * its prior findings rather than risking a wrong id. Never throws — the
364
+ * review is already posted by this point.
365
+ */
366
+ async function readPostedComments(args: {
367
+ octokit: Awaited<ReturnType<HostGitHubClient["getOctokit"]>>;
368
+ owner: string;
369
+ repo: string;
370
+ prNumber: number;
371
+ reviewId: number;
372
+ expected: number;
373
+ signal?: AbortSignal;
374
+ }): Promise<PostedReviewComment[]> {
375
+ if (args.expected === 0) {
376
+ return [];
377
+ }
378
+ try {
379
+ const posted: PostedReviewComment[] = [];
380
+ for (let page = 1; page <= REVIEW_COMMENTS_MAX_PAGES; page++) {
381
+ args.signal?.throwIfAborted();
382
+ const response = await args.octokit.rest.pulls.listCommentsForReview({
383
+ owner: args.owner,
384
+ repo: args.repo,
385
+ pull_number: args.prNumber,
386
+ review_id: args.reviewId,
387
+ per_page: REVIEW_COMMENTS_PAGE_SIZE,
388
+ page,
389
+ });
390
+ for (const comment of response.data) {
391
+ const line = comment.original_line ?? comment.line;
392
+ if (line === null || line === undefined) {
393
+ continue;
394
+ }
395
+ posted.push({ id: comment.id, file: comment.path, line });
396
+ }
397
+ if (response.data.length < REVIEW_COMMENTS_PAGE_SIZE) {
398
+ break;
399
+ }
400
+ }
401
+ return posted;
402
+ } catch {
403
+ return [];
404
+ }
405
+ }
406
+
162
407
  async function postReviewViaLocalGh(
163
408
  args: ReviewCommentPostArgs
164
409
  ): Promise<ReviewCommentPostResult> {
@@ -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
+ }
@@ -55,6 +55,7 @@ export type RunnerPrewarmRequest = Pick<
55
55
  | "sandbox"
56
56
  | "tools"
57
57
  | "mcpServers"
58
+ | "perSendMcpServers"
58
59
  | "subagents"
59
60
  | "model"
60
61
  >;
@@ -159,6 +160,28 @@ export function isAuthErrorMessage(message: string | undefined): boolean {
159
160
  return /authentication error|try logging out and back in/i.test(message);
160
161
  }
161
162
 
163
+ /** Cloud `wait()` copy for `stream_expired` / a missing start-state blob. */
164
+ function isStreamExpiredErrorMessage(message: string | undefined): boolean {
165
+ if (message === undefined) {
166
+ return false;
167
+ }
168
+ return /run stream is no longer available|\[stream_expired\]/i.test(message);
169
+ }
170
+
171
+ function retryReason(
172
+ message: string | undefined,
173
+ isFirstTurn: boolean
174
+ ): string | undefined {
175
+ if (isStreamExpiredErrorMessage(message)) {
176
+ return "cloud run stream expired";
177
+ }
178
+ // First-turn auth is the credential; a fresh handle cannot fix it.
179
+ if (!isFirstTurn && isAuthErrorMessage(message)) {
180
+ return "authentication failure";
181
+ }
182
+ return undefined;
183
+ }
184
+
162
185
  export class CursorSdkRunner implements AgentRunner {
163
186
  private readonly agents = new Map<string, CachedAgentHandle>();
164
187
  private readonly stores = new Map<string, Promise<LocalAgentStore>>();
@@ -187,26 +210,23 @@ export class CursorSdkRunner implements AgentRunner {
187
210
  const outcome = await this.runTurnAttempt(tracked);
188
211
  if (
189
212
  outcome.status !== "error" ||
190
- !isAuthErrorMessage(outcome.errorMessage) ||
191
213
  sawUpdate ||
192
- request.isFirstTurn ||
193
214
  request.signal?.aborted === true
194
215
  ) {
195
216
  return outcome;
196
217
  }
197
- // The turn died on an authentication failure before producing any output
198
- // the signature of a handle whose exchanged access token expired (the
199
- // failed attempt already evicted it). Retrying once on a fresh handle is
200
- // safe here because nothing streamed and no tool ran. First turns are
201
- // excluded: their token was exchanged moments ago, so the credential
202
- // itself is the problem and a retry cannot succeed.
218
+ const reason = retryReason(outcome.errorMessage, request.isFirstTurn);
219
+ if (reason === undefined) {
220
+ return outcome;
221
+ }
222
+ // Log the original error even when the retry succeeds so the expiry
223
+ // still shows up in Datadog.
203
224
  this.log(
204
- `[agent-sdk] session ${request.sessionId}: retrying the turn on a fresh SDK agent handle after an authentication failure`
225
+ `[agent-sdk] session ${request.sessionId}: ${reason}: ${outcome.errorMessage}; retrying the turn`
205
226
  );
206
227
  try {
207
228
  return await this.runTurnAttempt(tracked);
208
229
  } catch {
209
- // The retry was best effort; report the original failure.
210
230
  return outcome;
211
231
  }
212
232
  }
@@ -235,6 +255,12 @@ export class CursorSdkRunner implements AgentRunner {
235
255
  // Stream mapping must never reject into the SDK transport.
236
256
  }
237
257
  },
258
+ // The SDK builds this send's MCP session from exactly this set, so
259
+ // a turn always runs on the headers the engine evaluated for it —
260
+ // never on the ones a previous turn opened the cached handle with.
261
+ ...(request.runtime === "local" && request.perSendMcpServers === true
262
+ ? { mcpServers: request.mcpServers as AgentOptions["mcpServers"] }
263
+ : {}),
238
264
  ...(request.runtime === "local"
239
265
  ? {
240
266
  local: {
@@ -270,6 +296,14 @@ export class CursorSdkRunner implements AgentRunner {
270
296
  if (request.onSdkAgentId !== undefined) {
271
297
  await request.onSdkAgentId(agent.agentId);
272
298
  }
299
+ // Readable on the handle as soon as send resolves, long before wait().
300
+ if (
301
+ request.onRequestId !== undefined &&
302
+ run.requestId !== undefined &&
303
+ run.requestId !== ""
304
+ ) {
305
+ await request.onRequestId(run.requestId);
306
+ }
273
307
  try {
274
308
  const result = await run.wait();
275
309
  let outcome = toTurnOutcome(result);
@@ -401,7 +435,12 @@ export class CursorSdkRunner implements AgentRunner {
401
435
  return {
402
436
  model: toModelSelection(request.model ?? DEFAULT_MODEL),
403
437
  apiKey: this.requireApiKey(),
404
- ...(Object.keys(request.mcpServers).length > 0
438
+ // Per-send servers ride each agent.send instead (fresh per-session
439
+ // auth headers every turn). Keeping them out of the create/resume
440
+ // options also keeps the local executor cache key — which hashes
441
+ // these options — stable while credentials rotate.
442
+ ...(Object.keys(request.mcpServers).length > 0 &&
443
+ request.perSendMcpServers !== true
405
444
  ? { mcpServers: request.mcpServers as AgentOptions["mcpServers"] }
406
445
  : {}),
407
446
  ...(subagents === undefined ? {} : { agents: subagents }),
@@ -70,12 +70,14 @@ import {
70
70
  scmEventToGitHubPayload,
71
71
  scmMetadataToOriginPayload,
72
72
  } from "./cursor-event-relay.js";
73
+ import { AGENT_SERVE_GENERATION_ENV_VAR } from "./cursor-relay-core.js";
73
74
  import {
74
75
  type CursorSlackAgentRegistration,
75
76
  CursorSlackRelay,
76
77
  createCursorSlackApiCaller,
77
78
  formatCursorSlackEventLog,
78
79
  } from "./cursor-slack-relay.js";
80
+ import { buildAgentDeployManifest } from "./deploy-manifest.js";
79
81
  import {
80
82
  formatDiagnostics,
81
83
  hasErrorDiagnostics,
@@ -1111,6 +1113,23 @@ function mountAgentRoutes(
1111
1113
  handler: () => jsonResponse(projectInfo(project)),
1112
1114
  });
1113
1115
 
1116
+ // Hosted-only: the control plane's provision flow reads the deploy manifest
1117
+ // from the running engine (same derivation as `july manifest`) right after
1118
+ // the health probe, so every deploy surface — CLI, UI, redeploy — gets its
1119
+ // declarations from the installed code without a client-side extraction
1120
+ // step. `auth: "none"` mirrors `/v1/health`: hosted engines are reachable
1121
+ // only through the anyrun port proxy (network token) or the alias route
1122
+ // (alias token), and the route is not registered outside hosted mode, so
1123
+ // self-hosted `july serve` never exposes the manifest unauthenticated.
1124
+ if (process.env[AGENT_SERVE_GENERATION_ENV_VAR] !== undefined) {
1125
+ router.add({
1126
+ method: "GET",
1127
+ pattern: `${base}/v1/manifest`,
1128
+ auth: "none",
1129
+ handler: () => jsonResponse(buildAgentDeployManifest(project)),
1130
+ });
1131
+ }
1132
+
1114
1133
  // Playground Logs tab: recent ServeOptions.logger lines (channels, sessions,
1115
1134
  // webhook capture). `after` is an exclusive index cursor for polling.
1116
1135
  router.add({