@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.
- package/AGENTS.md +3 -4
- package/dist/channels/deployments/types.d.ts +1 -1
- package/dist/channels/deployments/types.d.ts.map +1 -1
- package/dist/channels/deployments/types.js +1 -0
- package/dist/channels/origin/api.d.ts +59 -13
- package/dist/channels/origin/api.d.ts.map +1 -1
- package/dist/channels/origin/api.js +168 -27
- package/dist/channels/origin/binding.d.ts +2 -2
- package/dist/channels/origin/checks.d.ts +6 -6
- package/dist/channels/origin/checks.js +6 -6
- package/dist/channels/origin/cursor-account.d.ts +5 -3
- package/dist/channels/origin/cursor-account.d.ts.map +1 -1
- package/dist/channels/origin/cursor-account.js +5 -3
- package/dist/channels/slack/api.d.ts +9 -0
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +36 -5
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +17 -46
- package/dist/channels/slack/dispatch.d.ts +7 -1
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +47 -23
- package/dist/channels/slack/inbound.d.ts +8 -0
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +32 -0
- package/dist/channels/slack/live-delivery.d.ts +26 -29
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +172 -73
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/nudge-carrier.d.ts +2 -0
- package/dist/channels/slack/nudge-carrier.d.ts.map +1 -1
- package/dist/channels/slack/nudge-carrier.js +48 -1
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +2 -2
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.BBj0klBO.js → app.jXleCmOc.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DFoGuETX.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.CUEXpL78.js → VPLocalSearchBox.8LhyglIT.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DabmQnia.js → theme.9ged-pr2.js} +2 -2
- package/dist/docs/assets/index.md.CI93Ltlb.js +5 -0
- package/dist/docs/assets/{index.md.BoHaMdeZ.lean.js → index.md.CI93Ltlb.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.BsMOxDvh.js → reference_cli.md.CCnG-nJx.js} +2 -1
- package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.js +2 -0
- package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.lean.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +4 -4
- package/dist/docs/evals.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/convert-automation.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/mcp-oauth.html +4 -4
- package/dist/docs/guides/opentelemetry.html +4 -4
- package/dist/docs/guides/slack.html +4 -4
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +6 -6
- package/dist/docs/index.md +3 -1
- package/dist/docs/llms-full.txt +102 -3
- package/dist/docs/llms.txt +1 -0
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/artifacts.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +7 -6
- package/dist/docs/reference/cli.md +3 -2
- package/dist/docs/reference/connections.html +4 -4
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/prompt.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +4 -4
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +4 -4
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/storage.html +4 -4
- package/dist/docs/templates/agentic-owners.html +4 -4
- package/dist/docs/templates/code-wiki.html +28 -0
- package/dist/docs/templates/code-wiki.md +91 -0
- package/dist/docs/templates/demo.html +5 -5
- package/dist/docs/templates/pr-autofixer.html +4 -4
- package/dist/docs/templates/security-help.html +4 -4
- package/dist/docs/templates/security-reviewer.html +5 -5
- package/dist/docs/templates/triage.html +4 -4
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/files-backends/cursor-hosted.d.ts +26 -4
- package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/files-backends/cursor-hosted.js +32 -4
- package/dist/internal/advertise-tools.d.ts +2 -0
- package/dist/internal/advertise-tools.d.ts.map +1 -1
- package/dist/internal/advertise-tools.js +15 -1
- package/dist/internal/bounded.d.ts +7 -0
- package/dist/internal/bounded.d.ts.map +1 -0
- package/dist/internal/bounded.js +32 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +7 -1
- package/dist/internal/conversation-mirror.d.ts +11 -8
- package/dist/internal/conversation-mirror.d.ts.map +1 -1
- package/dist/internal/conversation-mirror.js +116 -55
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +22 -48
- package/dist/internal/cursor/session-token.d.ts +45 -0
- package/dist/internal/cursor/session-token.d.ts.map +1 -0
- package/dist/internal/cursor/session-token.js +99 -0
- package/dist/internal/cursor-agent-template.d.ts +1 -1
- package/dist/internal/cursor-agent-template.d.ts.map +1 -1
- package/dist/internal/cursor-agent-template.js +1 -0
- package/dist/internal/framework-file-storage.d.ts +61 -0
- package/dist/internal/framework-file-storage.d.ts.map +1 -0
- package/dist/internal/framework-file-storage.js +204 -0
- package/dist/internal/framework-storage-selection.d.ts +11 -5
- package/dist/internal/framework-storage-selection.d.ts.map +1 -1
- package/dist/internal/framework-storage-selection.js +38 -11
- package/dist/internal/hosted-delivery.d.ts +7 -0
- package/dist/internal/hosted-delivery.d.ts.map +1 -1
- package/dist/internal/hosted-delivery.js +4 -0
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +2 -0
- package/dist/internal/install-cursor-skills.d.ts.map +1 -1
- package/dist/internal/install-cursor-skills.js +1 -3
- package/dist/internal/managed-delivery.d.ts +80 -0
- package/dist/internal/managed-delivery.d.ts.map +1 -0
- package/dist/internal/managed-delivery.js +86 -0
- package/dist/internal/runtime-dispatch-runner.d.ts +5 -1
- package/dist/internal/runtime-dispatch-runner.d.ts.map +1 -1
- package/dist/internal/runtime-dispatch-runner.js +7 -0
- package/dist/internal/sdk-runner.d.ts +11 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +39 -8
- package/dist/internal/server.js +1 -0
- package/dist/internal/session-engine.d.ts +13 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +62 -3
- package/dist/internal/steering.d.ts +15 -0
- package/dist/internal/steering.d.ts.map +1 -0
- package/dist/internal/steering.js +18 -0
- package/dist/playground/assets/index-B1DDvBeC.css +1 -0
- package/dist/playground/assets/{index-CZA1uEWA.js → index-Cx9CclK_.js} +51 -51
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +8 -5
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +4 -3
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +3 -1
- package/docs/reference/cli.md +3 -2
- package/docs/templates/code-wiki.md +96 -0
- package/package.json +2 -2
- package/skills/ab/SKILL.md +13 -97
- package/skills/create-agent/SKILL.md +123 -223
- package/skills/debug/SKILL.md +34 -68
- package/skills/evals/SKILL.md +55 -180
- package/skills/framework-map/SKILL.md +83 -145
- package/skills/github/SKILL.md +53 -111
- package/skills/hillclimb/SKILL.md +40 -163
- package/skills/mcp-auth/SKILL.md +48 -107
- package/skills/otel/SKILL.md +22 -68
- package/skills/setup-slack/SKILL.md +77 -208
- package/src/channels/deployments/types.ts +1 -0
- package/src/channels/origin/api.ts +242 -30
- package/src/channels/origin/binding.ts +2 -2
- package/src/channels/origin/checks.ts +6 -6
- package/src/channels/origin/cursor-account.ts +5 -3
- package/src/channels/slack/api.ts +41 -5
- package/src/channels/slack/defaults.ts +15 -49
- package/src/channels/slack/dispatch.ts +56 -22
- package/src/channels/slack/inbound.ts +43 -0
- package/src/channels/slack/live-delivery.ts +235 -114
- package/src/channels/slack/manifest.ts +5 -1
- package/src/channels/slack/nudge-carrier.ts +48 -1
- package/src/channels/slack/slack-channel.ts +2 -0
- package/src/files-backends/cursor-hosted.ts +40 -7
- package/src/internal/advertise-tools.ts +18 -2
- package/src/internal/bounded.ts +28 -0
- package/src/internal/cli-deploy.ts +12 -0
- package/src/internal/conversation-mirror.ts +145 -69
- package/src/internal/cursor/backend-client.ts +34 -58
- package/src/internal/cursor/session-token.ts +115 -0
- package/src/internal/cursor-agent-template.ts +1 -0
- package/src/internal/framework-file-storage.ts +222 -0
- package/src/internal/framework-storage-selection.ts +51 -11
- package/src/internal/hosted-delivery.ts +10 -0
- package/src/internal/init-project.ts +2 -0
- package/src/internal/install-cursor-skills.ts +1 -4
- package/src/internal/managed-delivery.ts +125 -0
- package/src/internal/runtime-dispatch-runner.ts +10 -0
- package/src/internal/sdk-runner.ts +56 -8
- package/src/internal/server.ts +2 -0
- package/src/internal/session-engine.ts +64 -4
- package/src/internal/steering.ts +17 -0
- package/src/storage-backends/cursor-hosted.ts +9 -7
- package/src/types.ts +2 -1
- package/templates/code-wiki/README.md +34 -0
- package/templates/code-wiki/agent/agent.ts +8 -0
- package/templates/code-wiki/agent/channels/github.ts +144 -0
- package/templates/code-wiki/agent/instructions.md +39 -0
- package/templates/code-wiki/agent/lib/repos.ts +5 -0
- package/templates/code-wiki/agent/lib/wiki.ts +22 -0
- package/templates/code-wiki/agent/tools/apply_wiki.ts +219 -0
- package/templates/code-wiki/evals/evals.config.ts +5 -0
- package/templates/code-wiki/evals/wiki.eval.ts +132 -0
- package/templates/code-wiki/init.json +25 -0
- package/templates/code-wiki/package.json +18 -0
- package/templates/code-wiki/tsconfig.json +12 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.oqPawjiD.js +0 -1
- package/dist/docs/assets/index.md.BoHaMdeZ.js +0 -5
- package/dist/internal/deployment-scm-event-stream.d.ts +0 -116
- package/dist/internal/deployment-scm-event-stream.d.ts.map +0 -1
- package/dist/internal/deployment-scm-event-stream.js +0 -474
- package/dist/playground/assets/index-CF7hUDnQ.css +0 -1
- package/src/internal/deployment-scm-event-stream.ts +0 -593
- /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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
12
|
-
* `ORIGIN_APP_ID` (or
|
|
13
|
-
* `ORIGIN_APP_INSTALLATION_ID`,
|
|
14
|
-
* app's Ed25519 key; literal
|
|
15
|
-
* `ORIGIN_API_BASE_URL` for the public
|
|
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
|
-
/**
|
|
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
|
|
246
|
-
*
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
42
|
-
*
|
|
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
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
|
313
|
-
|
|
314
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
}));
|