@cursor/july 0.1.95 → 0.1.96

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 (220) hide show
  1. package/AGENTS.md +3 -4
  2. package/dist/channels/deployments/types.d.ts +1 -1
  3. package/dist/channels/deployments/types.d.ts.map +1 -1
  4. package/dist/channels/deployments/types.js +1 -0
  5. package/dist/channels/origin/api.d.ts +59 -13
  6. package/dist/channels/origin/api.d.ts.map +1 -1
  7. package/dist/channels/origin/api.js +168 -27
  8. package/dist/channels/origin/binding.d.ts +2 -2
  9. package/dist/channels/origin/checks.d.ts +6 -6
  10. package/dist/channels/origin/checks.js +6 -6
  11. package/dist/channels/origin/cursor-account.d.ts +5 -3
  12. package/dist/channels/origin/cursor-account.d.ts.map +1 -1
  13. package/dist/channels/origin/cursor-account.js +5 -3
  14. package/dist/channels/slack/api.d.ts +9 -0
  15. package/dist/channels/slack/api.d.ts.map +1 -1
  16. package/dist/channels/slack/api.js +36 -5
  17. package/dist/channels/slack/defaults.d.ts.map +1 -1
  18. package/dist/channels/slack/defaults.js +17 -46
  19. package/dist/channels/slack/dispatch.d.ts +7 -1
  20. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  21. package/dist/channels/slack/dispatch.js +47 -23
  22. package/dist/channels/slack/inbound.d.ts +8 -0
  23. package/dist/channels/slack/inbound.d.ts.map +1 -1
  24. package/dist/channels/slack/inbound.js +32 -0
  25. package/dist/channels/slack/live-delivery.d.ts +26 -29
  26. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  27. package/dist/channels/slack/live-delivery.js +172 -73
  28. package/dist/channels/slack/manifest.d.ts.map +1 -1
  29. package/dist/channels/slack/manifest.js +5 -1
  30. package/dist/channels/slack/nudge-carrier.d.ts +2 -0
  31. package/dist/channels/slack/nudge-carrier.d.ts.map +1 -1
  32. package/dist/channels/slack/nudge-carrier.js +48 -1
  33. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  34. package/dist/channels/slack/slack-channel.js +2 -2
  35. package/dist/docs/404.html +2 -2
  36. package/dist/docs/ab.html +4 -4
  37. package/dist/docs/assets/{app.BBj0klBO.js → app.jXleCmOc.js} +1 -1
  38. package/dist/docs/assets/chunks/@localSearchIndexroot.DFoGuETX.js +1 -0
  39. package/dist/docs/assets/chunks/{VPLocalSearchBox.CUEXpL78.js → VPLocalSearchBox.8LhyglIT.js} +1 -1
  40. package/dist/docs/assets/chunks/{theme.DabmQnia.js → theme.9ged-pr2.js} +2 -2
  41. package/dist/docs/assets/index.md.CI93Ltlb.js +5 -0
  42. package/dist/docs/assets/{index.md.BoHaMdeZ.lean.js → index.md.CI93Ltlb.lean.js} +1 -1
  43. package/dist/docs/assets/{reference_cli.md.BsMOxDvh.js → reference_cli.md.CCnG-nJx.js} +2 -1
  44. package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.js +2 -0
  45. package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.lean.js +1 -0
  46. package/dist/docs/building-with-agents.html +4 -4
  47. package/dist/docs/concepts.html +4 -4
  48. package/dist/docs/deployment.html +4 -4
  49. package/dist/docs/evals.html +4 -4
  50. package/dist/docs/guides/agent-to-agent.html +4 -4
  51. package/dist/docs/guides/cloud-runtime.html +4 -4
  52. package/dist/docs/guides/convert-automation.html +4 -4
  53. package/dist/docs/guides/github.html +4 -4
  54. package/dist/docs/guides/human-in-the-loop.html +4 -4
  55. package/dist/docs/guides/mcp-oauth.html +4 -4
  56. package/dist/docs/guides/opentelemetry.html +4 -4
  57. package/dist/docs/guides/slack.html +4 -4
  58. package/dist/docs/guides/webhooks.html +4 -4
  59. package/dist/docs/hashmap.json +1 -1
  60. package/dist/docs/hillclimbing.html +4 -4
  61. package/dist/docs/index.html +6 -6
  62. package/dist/docs/index.md +3 -1
  63. package/dist/docs/llms-full.txt +102 -3
  64. package/dist/docs/llms.txt +1 -0
  65. package/dist/docs/quickstart.html +4 -4
  66. package/dist/docs/reference/agent-config.html +4 -4
  67. package/dist/docs/reference/artifacts.html +4 -4
  68. package/dist/docs/reference/channels.html +4 -4
  69. package/dist/docs/reference/cli.html +7 -6
  70. package/dist/docs/reference/cli.md +3 -2
  71. package/dist/docs/reference/connections.html +4 -4
  72. package/dist/docs/reference/hooks.html +4 -4
  73. package/dist/docs/reference/http-api.html +4 -4
  74. package/dist/docs/reference/instructions.html +4 -4
  75. package/dist/docs/reference/playground.html +4 -4
  76. package/dist/docs/reference/project-layout.html +4 -4
  77. package/dist/docs/reference/prompt.html +4 -4
  78. package/dist/docs/reference/schedules.html +4 -4
  79. package/dist/docs/reference/sessions.html +4 -4
  80. package/dist/docs/reference/skills.html +4 -4
  81. package/dist/docs/reference/subagents.html +4 -4
  82. package/dist/docs/reference/tools.html +4 -4
  83. package/dist/docs/scaffolding-agents.html +4 -4
  84. package/dist/docs/storage.html +4 -4
  85. package/dist/docs/templates/agentic-owners.html +4 -4
  86. package/dist/docs/templates/code-wiki.html +28 -0
  87. package/dist/docs/templates/code-wiki.md +91 -0
  88. package/dist/docs/templates/demo.html +5 -5
  89. package/dist/docs/templates/pr-autofixer.html +4 -4
  90. package/dist/docs/templates/security-help.html +4 -4
  91. package/dist/docs/templates/security-reviewer.html +5 -5
  92. package/dist/docs/templates/triage.html +4 -4
  93. package/dist/docs/troubleshooting.html +4 -4
  94. package/dist/files-backends/cursor-hosted.d.ts +26 -4
  95. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  96. package/dist/files-backends/cursor-hosted.js +32 -4
  97. package/dist/internal/advertise-tools.d.ts +2 -0
  98. package/dist/internal/advertise-tools.d.ts.map +1 -1
  99. package/dist/internal/advertise-tools.js +15 -1
  100. package/dist/internal/bounded.d.ts +7 -0
  101. package/dist/internal/bounded.d.ts.map +1 -0
  102. package/dist/internal/bounded.js +32 -0
  103. package/dist/internal/cli-deploy.d.ts.map +1 -1
  104. package/dist/internal/cli-deploy.js +7 -1
  105. package/dist/internal/conversation-mirror.d.ts +11 -8
  106. package/dist/internal/conversation-mirror.d.ts.map +1 -1
  107. package/dist/internal/conversation-mirror.js +116 -55
  108. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  109. package/dist/internal/cursor/backend-client.js +22 -48
  110. package/dist/internal/cursor/session-token.d.ts +45 -0
  111. package/dist/internal/cursor/session-token.d.ts.map +1 -0
  112. package/dist/internal/cursor/session-token.js +99 -0
  113. package/dist/internal/cursor-agent-template.d.ts +1 -1
  114. package/dist/internal/cursor-agent-template.d.ts.map +1 -1
  115. package/dist/internal/cursor-agent-template.js +1 -0
  116. package/dist/internal/framework-file-storage.d.ts +61 -0
  117. package/dist/internal/framework-file-storage.d.ts.map +1 -0
  118. package/dist/internal/framework-file-storage.js +204 -0
  119. package/dist/internal/framework-storage-selection.d.ts +11 -5
  120. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  121. package/dist/internal/framework-storage-selection.js +38 -11
  122. package/dist/internal/hosted-delivery.d.ts +7 -0
  123. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  124. package/dist/internal/hosted-delivery.js +4 -0
  125. package/dist/internal/init-project.d.ts.map +1 -1
  126. package/dist/internal/init-project.js +2 -0
  127. package/dist/internal/install-cursor-skills.d.ts.map +1 -1
  128. package/dist/internal/install-cursor-skills.js +1 -3
  129. package/dist/internal/managed-delivery.d.ts +80 -0
  130. package/dist/internal/managed-delivery.d.ts.map +1 -0
  131. package/dist/internal/managed-delivery.js +86 -0
  132. package/dist/internal/runtime-dispatch-runner.d.ts +5 -1
  133. package/dist/internal/runtime-dispatch-runner.d.ts.map +1 -1
  134. package/dist/internal/runtime-dispatch-runner.js +7 -0
  135. package/dist/internal/sdk-runner.d.ts +11 -0
  136. package/dist/internal/sdk-runner.d.ts.map +1 -1
  137. package/dist/internal/sdk-runner.js +39 -8
  138. package/dist/internal/server.js +1 -0
  139. package/dist/internal/session-engine.d.ts +13 -0
  140. package/dist/internal/session-engine.d.ts.map +1 -1
  141. package/dist/internal/session-engine.js +62 -3
  142. package/dist/internal/steering.d.ts +15 -0
  143. package/dist/internal/steering.d.ts.map +1 -0
  144. package/dist/internal/steering.js +18 -0
  145. package/dist/playground/assets/index-B1DDvBeC.css +1 -0
  146. package/dist/playground/assets/{index-CZA1uEWA.js → index-Cx9CclK_.js} +51 -51
  147. package/dist/playground/index.html +2 -2
  148. package/dist/storage-backends/cursor-hosted.d.ts +8 -5
  149. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  150. package/dist/storage-backends/cursor-hosted.js +4 -3
  151. package/dist/types.d.ts +2 -1
  152. package/dist/types.d.ts.map +1 -1
  153. package/docs/README.md +3 -1
  154. package/docs/reference/cli.md +3 -2
  155. package/docs/templates/code-wiki.md +96 -0
  156. package/package.json +2 -2
  157. package/skills/ab/SKILL.md +13 -97
  158. package/skills/create-agent/SKILL.md +123 -223
  159. package/skills/debug/SKILL.md +34 -68
  160. package/skills/evals/SKILL.md +55 -180
  161. package/skills/framework-map/SKILL.md +83 -145
  162. package/skills/github/SKILL.md +53 -111
  163. package/skills/hillclimb/SKILL.md +40 -163
  164. package/skills/mcp-auth/SKILL.md +48 -107
  165. package/skills/otel/SKILL.md +22 -68
  166. package/skills/setup-slack/SKILL.md +77 -208
  167. package/src/channels/deployments/types.ts +1 -0
  168. package/src/channels/origin/api.ts +242 -30
  169. package/src/channels/origin/binding.ts +2 -2
  170. package/src/channels/origin/checks.ts +6 -6
  171. package/src/channels/origin/cursor-account.ts +5 -3
  172. package/src/channels/slack/api.ts +41 -5
  173. package/src/channels/slack/defaults.ts +15 -49
  174. package/src/channels/slack/dispatch.ts +56 -22
  175. package/src/channels/slack/inbound.ts +43 -0
  176. package/src/channels/slack/live-delivery.ts +235 -114
  177. package/src/channels/slack/manifest.ts +5 -1
  178. package/src/channels/slack/nudge-carrier.ts +48 -1
  179. package/src/channels/slack/slack-channel.ts +2 -0
  180. package/src/files-backends/cursor-hosted.ts +40 -7
  181. package/src/internal/advertise-tools.ts +18 -2
  182. package/src/internal/bounded.ts +28 -0
  183. package/src/internal/cli-deploy.ts +12 -0
  184. package/src/internal/conversation-mirror.ts +145 -69
  185. package/src/internal/cursor/backend-client.ts +34 -58
  186. package/src/internal/cursor/session-token.ts +115 -0
  187. package/src/internal/cursor-agent-template.ts +1 -0
  188. package/src/internal/framework-file-storage.ts +222 -0
  189. package/src/internal/framework-storage-selection.ts +51 -11
  190. package/src/internal/hosted-delivery.ts +10 -0
  191. package/src/internal/init-project.ts +2 -0
  192. package/src/internal/install-cursor-skills.ts +1 -4
  193. package/src/internal/managed-delivery.ts +125 -0
  194. package/src/internal/runtime-dispatch-runner.ts +10 -0
  195. package/src/internal/sdk-runner.ts +56 -8
  196. package/src/internal/server.ts +2 -0
  197. package/src/internal/session-engine.ts +64 -4
  198. package/src/internal/steering.ts +17 -0
  199. package/src/storage-backends/cursor-hosted.ts +9 -7
  200. package/src/types.ts +2 -1
  201. package/templates/code-wiki/README.md +34 -0
  202. package/templates/code-wiki/agent/agent.ts +8 -0
  203. package/templates/code-wiki/agent/channels/github.ts +144 -0
  204. package/templates/code-wiki/agent/instructions.md +39 -0
  205. package/templates/code-wiki/agent/lib/repos.ts +5 -0
  206. package/templates/code-wiki/agent/lib/wiki.ts +22 -0
  207. package/templates/code-wiki/agent/tools/apply_wiki.ts +219 -0
  208. package/templates/code-wiki/evals/evals.config.ts +5 -0
  209. package/templates/code-wiki/evals/wiki.eval.ts +132 -0
  210. package/templates/code-wiki/init.json +25 -0
  211. package/templates/code-wiki/package.json +18 -0
  212. package/templates/code-wiki/tsconfig.json +12 -0
  213. package/dist/docs/assets/chunks/@localSearchIndexroot.oqPawjiD.js +0 -1
  214. package/dist/docs/assets/index.md.BoHaMdeZ.js +0 -5
  215. package/dist/internal/deployment-scm-event-stream.d.ts +0 -116
  216. package/dist/internal/deployment-scm-event-stream.d.ts.map +0 -1
  217. package/dist/internal/deployment-scm-event-stream.js +0 -474
  218. package/dist/playground/assets/index-CF7hUDnQ.css +0 -1
  219. package/src/internal/deployment-scm-event-stream.ts +0 -593
  220. /package/dist/docs/assets/{reference_cli.md.BsMOxDvh.lean.js → reference_cli.md.CCnG-nJx.lean.js} +0 -0
@@ -1,22 +1,37 @@
1
1
  /**
2
2
  * Origin public REST helpers (`https://api.cursor.com/v1/origin/...`).
3
3
  *
4
- * Origin writes such as CI check runs are installation-only: they need an
5
- * `oit_…` installation token minted from Origin app credentials (EdDSA app
6
- * JWT → `/v1/origin/app/installations/{id}/access_tokens`). This module
7
- * consolidates that flow so agents configure credentials once and call
8
- * {@link createOriginClient} — the GitHub-channel analogue of
4
+ * This module consolidates Origin auth so agents configure credentials once
5
+ * and call {@link createOriginClient} the GitHub-channel analogue of
9
6
  * `createGitHubOctokit`.
10
7
  *
11
- * Credentials resolve from options first, then the environment:
12
- * `ORIGIN_APP_ID` (or `AGENT_SERVE_ORIGIN_APP_ID`),
13
- * `ORIGIN_APP_INSTALLATION_ID`, `ORIGIN_APP_PRIVATE_KEY` (PKCS8 PEM for the
14
- * app's Ed25519 key; literal `\n` sequences are normalized), and
15
- * `ORIGIN_API_BASE_URL` for the public REST base.
8
+ * Self-configured channels with their own Origin app resolve credentials
9
+ * from options first, then the environment: `ORIGIN_APP_ID` (or
10
+ * `AGENT_SERVE_ORIGIN_APP_ID`), `ORIGIN_APP_INSTALLATION_ID`,
11
+ * `ORIGIN_APP_PRIVATE_KEY` (PKCS8 PEM for the app's Ed25519 key; literal
12
+ * `\n` sequences are normalized), and `ORIGIN_API_BASE_URL` for the public
13
+ * REST base.
14
+ *
15
+ * Hosted v2 forks configure none of that: their only credential is the
16
+ * deployment service account's `CURSOR_API_KEY`, exchanged for a Cursor
17
+ * session ({@link cursorSessionTokenProvider}, `POST
18
+ * /auth/exchange_user_api_key`) and sent as the Origin bearer. Reads and
19
+ * writes both run as the deployment **service-account principal**; Origin's
20
+ * `agent-serve` allowlist grants team-namespace read/write, so there is no
21
+ * app-installation token exchange for managed deployments.
16
22
  */
17
23
 
18
24
  import { Buffer } from "node:buffer";
19
25
  import { createPrivateKey, sign } from "node:crypto";
26
+ import {
27
+ cursorBackendUrl,
28
+ resolveApiKeySync,
29
+ } from "../../internal/cursor/credentials.js";
30
+ import {
31
+ CURSOR_SESSION_EXCHANGE_PATH,
32
+ exchangeCursorApiKeyForSession,
33
+ SESSION_TOKEN_EXPIRY_LEEWAY_MS,
34
+ } from "../../internal/cursor/session-token.js";
20
35
 
21
36
  /**
22
37
  * Public Origin REST base (check runs, installation tokens). Distinct from
@@ -58,7 +73,10 @@ export class OriginApiError extends Error {
58
73
  }
59
74
  }
60
75
 
61
- /** Yields a bearer for Origin REST calls (usually a short-lived `oit_…`). */
76
+ /**
77
+ * Yields a bearer for Origin REST calls (a short-lived `oit_…` for app
78
+ * channels, or a Cursor session token for service-account access).
79
+ */
62
80
  export type OriginTokenProvider = () => Promise<string>;
63
81
 
64
82
  export interface OriginAppCredentials {
@@ -181,6 +199,147 @@ async function mintOriginInstallationToken(input: {
181
199
  return token;
182
200
  }
183
201
 
202
+ /**
203
+ * Exchange endpoint for Cursor-account session tokens. Override with
204
+ * `AGENT_SERVE_CURSOR_SESSION_EXCHANGE_URL`; defaults to the Cursor backend.
205
+ */
206
+ export function resolveCursorSessionExchangeUrl(): string {
207
+ const override = process.env.AGENT_SERVE_CURSOR_SESSION_EXCHANGE_URL;
208
+ if (override !== undefined && override.trim() !== "") {
209
+ return override.trim().replace(/\/+$/, "");
210
+ }
211
+ return `${cursorBackendUrl()}${CURSOR_SESSION_EXCHANGE_PATH}`;
212
+ }
213
+
214
+ /**
215
+ * A session-backed Origin token provider: {@link OriginTokenProvider} plus
216
+ * an `invalidate` hook `createOriginClient` calls when Origin rejects the
217
+ * bearer (401), so the next mint re-exchanges instead of serving the
218
+ * rejected session until its local renewal deadline.
219
+ */
220
+ export type CursorSessionTokenProvider = OriginTokenProvider & {
221
+ invalidate: (token: string) => void;
222
+ };
223
+
224
+ /**
225
+ * Token provider backed by the shared Cursor session exchange
226
+ * (`exchangeCursorApiKeyForSession`, `POST /auth/exchange_user_api_key` —
227
+ * the same primitive `CursorBackendClient` mints through).
228
+ *
229
+ * Hosted v2 forks hold exactly one credential — the deployment service
230
+ * account's `CURSOR_API_KEY` — and Origin authenticates the exchanged
231
+ * session directly: reads and writes run as the deployment
232
+ * **service-account principal**, confined server-side to namespaces owned
233
+ * by the deployment's team (`agent-serve` allowlist). Local runs work the
234
+ * same way with a personal key: Origin authorizes the user principal.
235
+ *
236
+ * The API key is re-read on every mint so Bind can drop
237
+ * `CURSOR_API_KEY_FILE` onto a listening process; sessions are cached until
238
+ * shortly before their JWT expiry. The cache is **process-wide**, keyed by
239
+ * (endpoint, API key): hosted channels build a fresh `createOriginClient()`
240
+ * per event, so a per-provider cache would re-exchange on every read/write
241
+ * and concurrent first calls would race. In-flight exchanges are deduped
242
+ * for the same reason.
243
+ */
244
+ const cursorSessionTokenCache = new Map<
245
+ string,
246
+ { token: string; renewAtMs: number }
247
+ >();
248
+ const cursorSessionTokenInFlight = new Map<string, Promise<string>>();
249
+
250
+ /** Test-only: drop the process-wide exchanged-session cache. */
251
+ export function _clearCursorSessionTokenCacheForTesting(): void {
252
+ cursorSessionTokenCache.clear();
253
+ cursorSessionTokenInFlight.clear();
254
+ }
255
+
256
+ export function cursorSessionTokenProvider(input?: {
257
+ /** Cursor API key source. Defaults to the standard host resolution. */
258
+ getApiKey?: () => string | undefined;
259
+ /** Exchange endpoint. Defaults to {@link resolveCursorSessionExchangeUrl}. */
260
+ exchangeUrl?: string;
261
+ fetch?: typeof fetch;
262
+ now?: () => number;
263
+ }): CursorSessionTokenProvider {
264
+ const getApiKey =
265
+ input?.getApiKey ?? ((): string | undefined => resolveApiKeySync()?.apiKey);
266
+ const fetchImpl = input?.fetch ?? fetch;
267
+ const now = input?.now ?? Date.now;
268
+
269
+ const cacheKey = (url: string, apiKey: string): string =>
270
+ `${url}\u0000${apiKey}`;
271
+ const resolveUrl = (): string =>
272
+ input?.exchangeUrl ?? resolveCursorSessionExchangeUrl();
273
+
274
+ const mint = async (url: string, apiKey: string): Promise<string> => {
275
+ const nowMs = now();
276
+ let session: { token: string; expiresAtMs: number };
277
+ try {
278
+ session = await exchangeCursorApiKeyForSession({
279
+ exchangeUrl: url,
280
+ apiKey,
281
+ fetchImpl,
282
+ nowMs,
283
+ });
284
+ } catch (error) {
285
+ throw new Error(
286
+ `originClient: ${
287
+ error instanceof Error ? error.message : String(error)
288
+ } Check CURSOR_API_KEY / CURSOR_API_KEY_FILE, or pass an explicit token.`
289
+ );
290
+ }
291
+ const renewAtMs = Math.max(
292
+ nowMs + 1_000,
293
+ session.expiresAtMs - SESSION_TOKEN_EXPIRY_LEEWAY_MS
294
+ );
295
+ cursorSessionTokenCache.set(cacheKey(url, apiKey), {
296
+ token: session.token,
297
+ renewAtMs,
298
+ });
299
+ return session.token;
300
+ };
301
+
302
+ const provider = async (): Promise<string> => {
303
+ const apiKey = getApiKey();
304
+ if (apiKey === undefined || apiKey === "") {
305
+ throw new Error(
306
+ "originClient: no Cursor credential available for the session exchange (set CURSOR_API_KEY or CURSOR_API_KEY_FILE)."
307
+ );
308
+ }
309
+ const url = resolveUrl();
310
+ const key = cacheKey(url, apiKey);
311
+ const cached = cursorSessionTokenCache.get(key);
312
+ if (cached !== undefined && now() < cached.renewAtMs) {
313
+ return cached.token;
314
+ }
315
+ // Dedupe concurrent first calls: one exchange per (endpoint, key).
316
+ const inFlight = cursorSessionTokenInFlight.get(key);
317
+ if (inFlight !== undefined) {
318
+ return await inFlight;
319
+ }
320
+ const pending = mint(url, apiKey).finally(() => {
321
+ cursorSessionTokenInFlight.delete(key);
322
+ });
323
+ cursorSessionTokenInFlight.set(key, pending);
324
+ return await pending;
325
+ };
326
+
327
+ // Origin answered 401 for this bearer: drop it (unless a newer session
328
+ // already replaced it) so the next call re-exchanges immediately.
329
+ const invalidate = (token: string): void => {
330
+ const apiKey = getApiKey();
331
+ if (apiKey === undefined || apiKey === "") {
332
+ return;
333
+ }
334
+ const key = cacheKey(resolveUrl(), apiKey);
335
+ if (cursorSessionTokenCache.get(key)?.token === token) {
336
+ cursorSessionTokenCache.delete(key);
337
+ }
338
+ };
339
+
340
+ return Object.assign(provider, { invalidate });
341
+ }
342
+
184
343
  /**
185
344
  * A cached per-use token provider for one set of app credentials. Every call
186
345
  * inside the reuse window returns the same `oit_…`; the first call after it
@@ -224,6 +383,18 @@ export interface OriginClientOptions {
224
383
  apiBaseUrl?: string;
225
384
  /** Injectable fetch for tests. */
226
385
  fetch?: typeof fetch;
386
+ /**
387
+ * Cursor-account session exchange fallback (hosted v2 deployment SAs and
388
+ * local personal keys). `true` (default) engages when no explicit token
389
+ * or app credentials resolve and a Cursor credential is present; `false`
390
+ * disables it; an object injects the key source / endpoint.
391
+ */
392
+ cursorAccountExchange?:
393
+ | boolean
394
+ | {
395
+ getApiKey?: () => string | undefined;
396
+ exchangeUrl?: string;
397
+ };
227
398
  }
228
399
 
229
400
  /** Minimal authenticated Origin REST client (the `Octokit` analogue). */
@@ -242,8 +413,11 @@ export interface OriginClient {
242
413
 
243
414
  /**
244
415
  * Build an authenticated Origin REST client. Auth resolution: explicit
245
- * token/provider, then app credentials (options or environment). Missing
246
- * auth fails at the first request with a clear message.
416
+ * token/provider, then app credentials (options or environment, for
417
+ * channels with their own Origin app), then the Cursor-account session
418
+ * exchange (hosted v2 deployment SAs — reads and writes both run as the
419
+ * deployment service-account principal). Missing auth fails at the first
420
+ * request with a clear message.
247
421
  */
248
422
  export function createOriginClient(
249
423
  options: OriginClientOptions = {}
@@ -255,6 +429,10 @@ export function createOriginClient(
255
429
  const fetchImpl = options.fetch ?? fetch;
256
430
 
257
431
  let tokenProvider: OriginTokenProvider | undefined;
432
+ // Set when the session-exchange fallback is engaged: a 401 from Origin
433
+ // invalidates the cached session and retries once with a fresh one, the
434
+ // `CursorBackendClient` re-exchange behavior.
435
+ let sessionProvider: CursorSessionTokenProvider | undefined;
258
436
  const getToken = async (): Promise<string> => {
259
437
  if (typeof options.token === "string") {
260
438
  return options.token;
@@ -264,20 +442,55 @@ export function createOriginClient(
264
442
  }
265
443
  if (tokenProvider === undefined) {
266
444
  const credentials = options.credentials ?? resolveOriginAppCredentials();
267
- if (credentials === undefined) {
445
+ const exchange = options.cursorAccountExchange ?? true;
446
+ const exchangeOptions =
447
+ exchange === false ? undefined : exchange === true ? {} : exchange;
448
+ const getExchangeApiKey =
449
+ exchangeOptions?.getApiKey ??
450
+ ((): string | undefined => resolveApiKeySync()?.apiKey);
451
+ if (credentials !== undefined) {
452
+ tokenProvider = originAppTokenProvider({
453
+ credentials,
454
+ apiBaseUrl,
455
+ fetch: fetchImpl,
456
+ });
457
+ } else if (
458
+ exchangeOptions !== undefined &&
459
+ (getExchangeApiKey() ?? "") !== ""
460
+ ) {
461
+ sessionProvider = cursorSessionTokenProvider({
462
+ fetch: fetchImpl,
463
+ getApiKey: getExchangeApiKey,
464
+ ...(exchangeOptions.exchangeUrl === undefined
465
+ ? {}
466
+ : { exchangeUrl: exchangeOptions.exchangeUrl }),
467
+ });
468
+ tokenProvider = sessionProvider;
469
+ } else {
268
470
  throw new Error(
269
- "originClient: no Origin credential available (pass token / credentials, or set ORIGIN_APP_ID + ORIGIN_APP_INSTALLATION_ID + ORIGIN_APP_PRIVATE_KEY)."
471
+ "originClient: no Origin credential available (pass token / credentials, set ORIGIN_APP_ID + ORIGIN_APP_INSTALLATION_ID + ORIGIN_APP_PRIVATE_KEY, or sign in with a Cursor credential for the session exchange)."
270
472
  );
271
473
  }
272
- tokenProvider = originAppTokenProvider({
273
- credentials,
274
- apiBaseUrl,
275
- fetch: fetchImpl,
276
- });
277
474
  }
278
475
  return await tokenProvider();
279
476
  };
280
477
 
478
+ const send = async (
479
+ input: { method: OriginApiMethod; path: string; body?: unknown },
480
+ path: string,
481
+ token: string
482
+ ): Promise<Response> =>
483
+ await fetchImpl(`${apiBaseUrl}${path}`, {
484
+ method: input.method,
485
+ headers: {
486
+ authorization: `Bearer ${token}`,
487
+ ...(input.body === undefined
488
+ ? {}
489
+ : { "content-type": "application/json" }),
490
+ },
491
+ body: input.body === undefined ? undefined : JSON.stringify(input.body),
492
+ });
493
+
281
494
  return {
282
495
  apiBaseUrl,
283
496
  async request<T = unknown>(input: {
@@ -287,16 +500,15 @@ export function createOriginClient(
287
500
  }): Promise<T> {
288
501
  const path = input.path.startsWith("/") ? input.path : `/${input.path}`;
289
502
  const token = await getToken();
290
- const response = await fetchImpl(`${apiBaseUrl}${path}`, {
291
- method: input.method,
292
- headers: {
293
- authorization: `Bearer ${token}`,
294
- ...(input.body === undefined
295
- ? {}
296
- : { "content-type": "application/json" }),
297
- },
298
- body: input.body === undefined ? undefined : JSON.stringify(input.body),
299
- });
503
+ let response = await send(input, path, token);
504
+ if (response.status === 401 && sessionProvider !== undefined) {
505
+ // The session aged out server-side. Drop it — unless a concurrent
506
+ // retry already minted a newer one — and retry once.
507
+ await response.text().catch(() => undefined);
508
+ sessionProvider.invalidate(token);
509
+ const retryToken = await getToken();
510
+ response = await send(input, path, retryToken);
511
+ }
300
512
  const text = await response.text();
301
513
  let parsed: unknown;
302
514
  try {
@@ -38,8 +38,8 @@ export interface OriginHandle {
38
38
  }): Promise<T>;
39
39
  /**
40
40
  * Open a CI check run on `sha` and get a handle for updating and
41
- * concluding it. Needs an app installation with
42
- * `repository:checks:write` Origin check writes are installation-only.
41
+ * concluding it. Needs `repository:checks:write` — via an app
42
+ * installation, or the deployment service account on hosted v2 forks.
43
43
  */
44
44
  createCheck(
45
45
  input: Omit<OriginCheckCreateInput, "origin" | "owner" | "repo">
@@ -7,12 +7,12 @@
7
7
  * fresh `externalUpdatedAt`, and `resumeOriginCheck` needs only those
8
8
  * identity fields (no server id) to rebuild a handle after a restart.
9
9
  *
10
- * Posting needs `repository:checks:write` and is installation-only: the
11
- * client must authenticate with an `oit_…` installation token (see
12
- * `createOriginClient` / `originAppTokenProvider` in `api.js`). Works with
13
- * any client exposing `request()` (`ctx.origin`, `createOriginClient()`),
14
- * and is also reachable as `ctx.origin.createCheck(...)`. API failures throw
15
- * `OriginApiError`.
10
+ * Posting needs `repository:checks:write`: an `oit_…` installation token for
11
+ * app channels, or a deployment service-account session (hosted v2 forks —
12
+ * the `agent-serve` allowlist grants checks:write on team-owned namespaces).
13
+ * See `createOriginClient` in `api.js`. Works with any client exposing
14
+ * `request()` (`ctx.origin`, `createOriginClient()`), and is also reachable
15
+ * as `ctx.origin.createCheck(...)`. API failures throw `OriginApiError`.
16
16
  */
17
17
 
18
18
  import {
@@ -3,9 +3,11 @@
3
3
  *
4
4
  * Origin identity is the signed-in Cursor user (no third-party OAuth). The
5
5
  * channel declares repos at definition time; the serve host subscribes
6
- * `/v0/scm-events` with `provider: "origin"` as that user. Unlike GitHub,
7
- * there is no installation token to mint outbound Origin API calls already
8
- * authenticate as the Cursor principal.
6
+ * `/v0/scm-events` with `provider: "origin"` as that user. Outbound Origin
7
+ * API calls authenticate with the channel's app credentials when configured;
8
+ * hosted v2 deployments instead exchange the Cursor credential for a
9
+ * session (`cursorSessionTokenProvider` in ./api.ts), so reads and writes
10
+ * both run as the deployment service-account principal.
9
11
  */
10
12
 
11
13
  import type { OriginChannelOptions } from "./types.js";
@@ -193,6 +193,12 @@ export function truncateTitle(title: string, max?: number): string {
193
193
  // Assistant / stream helpers
194
194
  // ---------------------------------------------------------------------------
195
195
 
196
+ /**
197
+ * Show or clear the Working chip. Agent UX ignores empty
198
+ * `assistant.threads.setStatus`; `agents.sessions.setStatus` `active` is
199
+ * what actually clears it. The assistant call stays for loading_messages
200
+ * and older workspaces.
201
+ */
196
202
  export async function setThreadStatus(args: {
197
203
  botToken: SlackBotToken | undefined;
198
204
  channelId: string;
@@ -204,13 +210,21 @@ export async function setThreadStatus(args: {
204
210
  return { ok: false, error: "missing_channel_or_thread" };
205
211
  }
206
212
  const status = truncateTypingStatus(args.status);
213
+ const sessionResult = await callSlackApiSoft({
214
+ botToken: args.botToken,
215
+ operation: "agents.sessions.setStatus",
216
+ body: {
217
+ channel_id: args.channelId,
218
+ thread_ts: args.threadTs,
219
+ status: status.length > 0 ? "processing" : "active",
220
+ },
221
+ });
222
+
207
223
  const body: Record<string, unknown> = {
208
224
  channel_id: args.channelId,
209
225
  thread_ts: args.threadTs,
210
226
  status,
211
227
  };
212
- // Empty status clears Slack's assistant chip ("App is working…").
213
- // Sending loading_messages with it keeps the chip rotating.
214
228
  if (status.length > 0) {
215
229
  const loadingRaw =
216
230
  args.loadingMessages !== undefined && args.loadingMessages.length > 0
@@ -224,11 +238,19 @@ export async function setThreadStatus(args: {
224
238
  body.loading_messages = loading_messages;
225
239
  }
226
240
  }
227
- return callSlackApiSoft({
241
+ const assistantResult = await callSlackApiSoft({
228
242
  botToken: args.botToken,
229
243
  operation: "assistant.threads.setStatus",
230
244
  body,
231
245
  });
246
+
247
+ if (sessionResult.ok) {
248
+ return sessionResult;
249
+ }
250
+ if (sessionResult.unsupported === true || assistantResult.ok) {
251
+ return assistantResult;
252
+ }
253
+ return sessionResult;
232
254
  }
233
255
 
234
256
  export async function setThreadTitle(args: {
@@ -278,16 +300,30 @@ export async function startChatStream(args: {
278
300
  channel: string;
279
301
  thread_ts?: string;
280
302
  markdown_text?: string;
303
+ chunks?: StreamTaskChunk[];
304
+ recipient_user_id?: string;
305
+ recipient_team_id?: string;
281
306
  }): Promise<SoftSlackResult<{ ts: string; channel: string }>> {
282
307
  const body: Record<string, unknown> = {
283
308
  channel: args.channel,
284
309
  };
310
+ // Empty markdown_text is rejected or opens a stream the assistant pane
311
+ // never attaches. Recipients are required outside DMs.
285
312
  if (args.thread_ts !== undefined && args.thread_ts !== "") {
286
313
  body.thread_ts = args.thread_ts;
287
314
  }
288
- if (args.markdown_text !== undefined) {
315
+ if (args.markdown_text !== undefined && args.markdown_text !== "") {
289
316
  body.markdown_text = args.markdown_text;
290
317
  }
318
+ if (args.chunks !== undefined && args.chunks.length > 0) {
319
+ body.chunks = args.chunks;
320
+ }
321
+ if (args.recipient_user_id !== undefined && args.recipient_user_id !== "") {
322
+ body.recipient_user_id = args.recipient_user_id;
323
+ }
324
+ if (args.recipient_team_id !== undefined && args.recipient_team_id !== "") {
325
+ body.recipient_team_id = args.recipient_team_id;
326
+ }
291
327
  const result = await callSlackApiSoft({
292
328
  botToken: args.botToken,
293
329
  operation: "chat.startStream",
@@ -642,7 +678,7 @@ export function buildSlackBinding(args: {
642
678
  ...(loadingMessages === undefined ? {} : { loadingMessages }),
643
679
  });
644
680
  if (!result.ok) {
645
- log.warn("assistant.threads.setStatus not-ok", {
681
+ log.warn("slack setStatus not-ok", {
646
682
  error: result.error,
647
683
  channel: args.channelId,
648
684
  thread_ts: threadTs,
@@ -166,7 +166,7 @@ export function buildDefaultEvents(
166
166
  status: "",
167
167
  });
168
168
  if (!result.ok) {
169
- getLog().warn("assistant.threads.setStatus not-ok", {
169
+ getLog().warn("slack setStatus not-ok", {
170
170
  error: result.error,
171
171
  channel: channelId,
172
172
  thread_ts: threadTs,
@@ -205,42 +205,22 @@ export function buildDefaultEvents(
205
205
  status: "Working…",
206
206
  loadingMessages,
207
207
  });
208
-
209
- // Retry streaming each turn unless a prior call proved the feature
210
- // unsupported for this workspace/app. Thinking stays on setStatus
211
- // (Cursor-account proxy pins agent username/icon on that method).
212
- if (!streaming || state.streamingUnavailable === true) {
213
- return;
214
- }
215
-
216
- await strategy.start({
217
- channel,
218
- channelId: state.channelId,
219
- threadTs: state.threadTs,
220
- sessionId: event.sessionId,
221
- });
222
208
  },
223
209
 
224
210
  async "message.appended"(event, channel) {
225
211
  if (!streaming || channel.state.streamingUnavailable === true) {
226
212
  return;
227
213
  }
228
- const streamTs = channel.state.streamTs;
229
214
  const channelId =
230
215
  channel.state.streamChannelId ?? channel.state.channelId;
231
- if (
232
- streamTs === null ||
233
- streamTs === "" ||
234
- channelId === null ||
235
- channelId === ""
236
- ) {
216
+ const threadTs = channel.state.threadTs;
217
+ if (channelId === null || channelId === "" || threadTs === null) {
237
218
  return;
238
219
  }
239
220
 
240
221
  await strategy.append({
241
222
  channel,
242
223
  channelId,
243
- streamTs,
244
224
  text: event.data.text,
245
225
  sessionId: event.sessionId,
246
226
  });
@@ -298,24 +278,23 @@ export function buildDefaultEvents(
298
278
  strategy.inlineToolSteps &&
299
279
  thinkingSteps &&
300
280
  streaming &&
301
- channel.state.streamingUnavailable !== true &&
302
- channel.state.streamTs !== null &&
303
- channel.state.streamTs !== ""
281
+ channel.state.streamingUnavailable !== true
304
282
  ) {
305
283
  const channelId =
306
284
  channel.state.streamChannelId ?? channel.state.channelId;
307
- if (channelId !== null && channelId !== "") {
285
+ const threadTs = channel.state.threadTs;
286
+ if (channelId !== null && channelId !== "" && threadTs !== null) {
308
287
  const chunks = buildTaskChunksForCalls(
309
288
  event.data.calls,
310
289
  "in_progress"
311
290
  );
312
- const result = await appendChatStream({
313
- botToken,
314
- channel: channelId,
315
- ts: channel.state.streamTs,
291
+ const streamed = await strategy.appendChunks({
292
+ channel,
293
+ channelId,
316
294
  chunks,
295
+ sessionId: event.sessionId,
317
296
  });
318
- if (result.ok) {
297
+ if (streamed) {
319
298
  if (
320
299
  channel.state.channelId !== null &&
321
300
  channel.state.threadTs !== null
@@ -388,29 +367,17 @@ export function buildDefaultEvents(
388
367
  // Empty final text: still close any open stream / clear the skeleton
389
368
  // and clear typing.
390
369
  if (event.data.text === "") {
391
- if (state.streamTs !== null && state.streamTs !== "") {
392
- await strategy.finalizeEmpty({
393
- channel,
394
- channelId: state.streamChannelId ?? state.channelId,
395
- streamTs: state.streamTs,
396
- });
397
- channel.state.streamTs = null;
398
- }
370
+ await strategy.finalizeEmpty({ channel });
399
371
  await clearWorkingStatus(state.channelId, state.threadTs);
400
372
  return;
401
373
  }
402
374
 
403
- const canStream =
404
- streaming &&
405
- state.streamingUnavailable !== true &&
406
- state.streamTs !== null &&
407
- state.streamTs !== "";
375
+ const canStream = streaming && state.streamingUnavailable !== true;
408
376
 
409
- if (canStream && state.streamTs !== null) {
377
+ if (canStream) {
410
378
  const { delivered } = await strategy.finalize({
411
379
  channel,
412
380
  channelId: state.streamChannelId ?? state.channelId,
413
- streamTs: state.streamTs,
414
381
  text: event.data.text,
415
382
  sessionId: event.sessionId,
416
383
  });
@@ -509,11 +476,10 @@ export function buildDefaultEvents(
509
476
  const errorText = formatSlackTurnFailureMessage(event.data.message);
510
477
 
511
478
  let delivered = false;
512
- if (state.streamTs !== null && state.streamTs !== "") {
479
+ if (streaming && state.streamingUnavailable !== true) {
513
480
  ({ delivered } = await strategy.fail({
514
481
  channel,
515
482
  channelId: state.streamChannelId ?? state.channelId,
516
- streamTs: state.streamTs,
517
483
  errorText,
518
484
  sessionId: event.sessionId,
519
485
  }));