@cursor/july 0.1.89 → 0.1.90
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/README.md +95 -94
- package/dist/ab.d.ts +1 -1
- package/dist/ab.js +1 -1
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/bin/agent-serve.js +20 -7
- package/dist/channels.d.ts +1 -1
- package/dist/channels.js +1 -1
- package/dist/connections.d.ts +1 -1
- package/dist/connections.js +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.DUOPbN18.js → app.wiNkt6G7.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.FV0R6kOb.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.CmWbGcGk.js → VPLocalSearchBox.ByhUGu47.js} +1 -1
- package/dist/docs/assets/chunks/{theme.S57OeOLA.js → theme.Dx7j_-0n.js} +2 -2
- package/dist/docs/assets/{concepts.md.BXAm6G-C.js → concepts.md.2NfkGjqM.js} +1 -1
- package/dist/docs/assets/{reference_project-layout.md.D3MdHM2z.js → reference_project-layout.md.CueaKpjr.js} +5 -20
- package/dist/docs/assets/reference_project-layout.md.CueaKpjr.lean.js +1 -0
- package/dist/docs/assets/{reference_tools.md.DTg_kEsx.js → reference_tools.md.XmeFP_3d.js} +23 -2
- package/dist/docs/assets/{reference_tools.md.DTg_kEsx.lean.js → reference_tools.md.XmeFP_3d.lean.js} +1 -1
- package/dist/docs/assets/{scaffolding-agents.md.CiGsJ1aw.js → scaffolding-agents.md.BpMFXv2J.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +5 -5
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/convert-automation.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +8 -23
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +27 -6
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +3 -3
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/evals.d.ts +1 -1
- package/dist/evals.js +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/internal/cli-convert-automation.d.ts +1 -1
- package/dist/internal/cli-convert-automation.js +1 -1
- package/dist/internal/cli-cursor.d.ts +12 -4
- package/dist/internal/cli-cursor.d.ts.map +1 -1
- package/dist/internal/cli-cursor.js +11 -4
- package/dist/internal/convert-automation/convert-workflow.d.ts +1 -1
- package/dist/internal/convert-automation/convert-workflow.js +9 -9
- package/dist/internal/convert-automation/types.d.ts +1 -1
- package/dist/internal/convert-automation/types.js +1 -1
- package/dist/internal/cursor/backend-client.d.ts +9 -1
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +19 -6
- package/dist/internal/cursor/credentials.d.ts +35 -9
- package/dist/internal/cursor/credentials.d.ts.map +1 -1
- package/dist/internal/cursor/credentials.js +92 -39
- package/dist/internal/discovery.js +1 -1
- package/dist/internal/distribution.d.ts +18 -0
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +19 -0
- package/dist/internal/eval-runner.js +1 -1
- package/dist/internal/grokbot/runner.d.ts +5 -2
- package/dist/internal/grokbot/runner.d.ts.map +1 -1
- package/dist/internal/grokbot/runner.js +10 -6
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +8 -1
- package/dist/internal/sdk-runner.d.ts +5 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +10 -5
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +80 -36
- package/dist/playground/assets/index-BpVS-paP.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/reminders.d.ts +1 -1
- package/dist/reminders.js +1 -1
- package/dist/schedules.d.ts +1 -1
- package/dist/schedules.js +1 -1
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/docs/concepts.md +2 -1
- package/docs/reference/project-layout.md +5 -20
- package/docs/reference/tools.md +53 -3
- package/docs/scaffolding-agents.md +2 -0
- package/package.json +2 -1
- package/src/ab.ts +1 -1
- package/src/artifacts.ts +1 -1
- package/src/bin/agent-serve.ts +20 -6
- package/src/channels.ts +1 -1
- package/src/connections.ts +1 -1
- package/src/evals.ts +1 -1
- package/src/hooks.ts +1 -1
- package/src/index.ts +2 -2
- package/src/internal/cli-convert-automation.ts +1 -1
- package/src/internal/cli-cursor.ts +18 -5
- package/src/internal/convert-automation/convert-workflow.ts +9 -9
- package/src/internal/convert-automation/types.ts +1 -1
- package/src/internal/cursor/backend-client.ts +29 -4
- package/src/internal/cursor/credentials.ts +87 -24
- package/src/internal/discovery.ts +1 -1
- package/src/internal/distribution.ts +31 -0
- package/src/internal/eval-runner.ts +1 -1
- package/src/internal/grokbot/runner.ts +15 -6
- package/src/internal/init-project.ts +11 -1
- package/src/internal/sdk-runner.ts +10 -4
- package/src/internal/server.ts +95 -33
- package/src/reminders.ts +1 -1
- package/src/schedules.ts +1 -1
- package/src/skills.ts +1 -1
- package/src/storage.ts +1 -1
- package/src/tools.ts +1 -1
- package/src/types.ts +2 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.CxCtxfDE.js +0 -1
- package/dist/docs/assets/reference_project-layout.md.D3MdHM2z.lean.js +0 -1
- package/dist/playground/assets/index-DQGZnAI0.css +0 -1
- /package/dist/docs/assets/{concepts.md.BXAm6G-C.lean.js → concepts.md.2NfkGjqM.lean.js} +0 -0
- /package/dist/docs/assets/{scaffolding-agents.md.CiGsJ1aw.lean.js → scaffolding-agents.md.BpMFXv2J.lean.js} +0 -0
- /package/dist/playground/assets/{index-BmiIjFlM.js → index-DxapiDj_.js} +0 -0
package/src/connections.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `hostOnly: true` to keep it off the model (still on `host.mcp`).
|
|
9
9
|
*
|
|
10
10
|
* ```ts
|
|
11
|
-
* import { defineConnection } from "@
|
|
11
|
+
* import { defineConnection } from "@cursor/july/connections";
|
|
12
12
|
*
|
|
13
13
|
* export default defineConnection({
|
|
14
14
|
* url: "https://mcp.linear.app/mcp",
|
package/src/evals.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `t.messageIncludes(...)`, or `t.check(value, includes(...))`.
|
|
8
8
|
*
|
|
9
9
|
* ```ts
|
|
10
|
-
* import { defineEval, includes } from "@
|
|
10
|
+
* import { defineEval, includes } from "@cursor/july/evals";
|
|
11
11
|
*
|
|
12
12
|
* // One datapoint — id is the file path (evals/weather/nyc.eval.ts → weather/nyc)
|
|
13
13
|
* export default defineEval({
|
package/src/hooks.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @cursor/july
|
|
3
3
|
*
|
|
4
4
|
* Filesystem-first framework for Cursor agents: define an agent as markdown
|
|
5
5
|
* and TypeScript files under an `agent/` directory, and serve it over
|
|
6
6
|
* channels with the Cursor SDK as the execution harness.
|
|
7
7
|
*
|
|
8
8
|
* ```ts
|
|
9
|
-
* import { serve } from "@
|
|
9
|
+
* import { serve } from "@cursor/july";
|
|
10
10
|
*
|
|
11
11
|
* const handle = await serve(".", { port: 3000 });
|
|
12
12
|
* console.log(`agent listening on ${handle.url}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `agent-serve convert-automation <url>` — export a Cursor Automation into
|
|
3
|
-
* an
|
|
3
|
+
* an agentsdk project.
|
|
4
4
|
*
|
|
5
5
|
* Fetches the automation's workflow from the backend export endpoint
|
|
6
6
|
* (Bearer user API key, same credential chain as deploy), runs the pure
|
|
@@ -148,10 +148,12 @@ export async function cmdLogin(options: CursorCliOptions): Promise<number> {
|
|
|
148
148
|
/**
|
|
149
149
|
* Gate for commands that need a Cursor credential before they can do useful
|
|
150
150
|
* work (`serve`, `dev`, interactive `init`). Already-signed-in hosts
|
|
151
|
-
* (`--api-key` / `CURSOR_API_KEY` / stored login) return immediately.
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
151
|
+
* (`--api-key` / `CURSOR_API_KEY` / `CURSOR_API_KEY_FILE` / stored login) return immediately.
|
|
152
|
+
* Hosted `--allow-anonymous` (v1 alias-token engines and v2 freeze bake)
|
|
153
|
+
* skips this gate so health/manifest can bind without a Cursor key.
|
|
154
|
+
* Self-hosted omit the flag: an interactive host runs {@link cmdLogin};
|
|
155
|
+
* a non-interactive one (CI, Docker) fails fast with {@link SIGN_IN_HINT}
|
|
156
|
+
* instead of polling a browser that will never open.
|
|
155
157
|
*/
|
|
156
158
|
export async function ensureCursorLogin(
|
|
157
159
|
options: {
|
|
@@ -161,6 +163,12 @@ export async function ensureCursorLogin(
|
|
|
161
163
|
* TTY.
|
|
162
164
|
*/
|
|
163
165
|
interactive?: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* `serve --allow-anonymous`: listen without a Cursor credential.
|
|
168
|
+
* Model turns still fail later if a key is required; freeze only
|
|
169
|
+
* needs `/v1/health` and `/v1/manifest`.
|
|
170
|
+
*/
|
|
171
|
+
allowAnonymous?: boolean;
|
|
164
172
|
/** Test seam. */
|
|
165
173
|
login?: (options: CursorCliOptions) => Promise<number>;
|
|
166
174
|
} = {}
|
|
@@ -169,6 +177,9 @@ export async function ensureCursorLogin(
|
|
|
169
177
|
if (resolved !== undefined) {
|
|
170
178
|
return 0;
|
|
171
179
|
}
|
|
180
|
+
if (options.allowAnonymous === true) {
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
172
183
|
const interactive = options.interactive ?? process.stderr.isTTY === true;
|
|
173
184
|
if (!interactive) {
|
|
174
185
|
process.stderr.write(`Not signed in to Cursor. ${SIGN_IN_HINT}\n`);
|
|
@@ -243,12 +254,14 @@ function formatIdentity(identity: CursorIdentity): string {
|
|
|
243
254
|
return name === "" ? `${email}${team}` : `${name} <${email}>${team}`;
|
|
244
255
|
}
|
|
245
256
|
|
|
246
|
-
function describeSource(source: "option" | "env" | "login"): string {
|
|
257
|
+
function describeSource(source: "option" | "env" | "file" | "login"): string {
|
|
247
258
|
switch (source) {
|
|
248
259
|
case "option":
|
|
249
260
|
return "explicit --api-key / serve({ apiKey })";
|
|
250
261
|
case "env":
|
|
251
262
|
return "CURSOR_API_KEY environment variable";
|
|
263
|
+
case "file":
|
|
264
|
+
return "CURSOR_API_KEY_FILE (task bind)";
|
|
252
265
|
case "login":
|
|
253
266
|
return `stored login (${credentialsFilePath()})`;
|
|
254
267
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure Workflow →
|
|
2
|
+
* Pure Workflow → agentsdk project converter.
|
|
3
3
|
*
|
|
4
4
|
* Maps an exported Cursor automation (proto3 workflow JSON + enriched MCP
|
|
5
5
|
* server list) to project files, warnings, and setup steps. No I/O, no
|
|
@@ -185,7 +185,7 @@ function renderInstructions(
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
/**
|
|
188
|
-
* Strip Cursor-backend prompt markup that means nothing in
|
|
188
|
+
* Strip Cursor-backend prompt markup that means nothing in agentsdk:
|
|
189
189
|
* `@[label](action:...)` action tokens become their label text, and
|
|
190
190
|
* unresolvable `%%INLINECODE<n>%%` storage placeholders are flagged (their
|
|
191
191
|
* content is not in the export).
|
|
@@ -972,7 +972,7 @@ function emitSlackChannel(
|
|
|
972
972
|
? "channel-created"
|
|
973
973
|
: "reaction-added";
|
|
974
974
|
out.warnings.push(
|
|
975
|
-
`Slack ${kind} trigger has no
|
|
975
|
+
`Slack ${kind} trigger has no agentsdk equivalent — its config is inlined as a comment in ${path}; handle the Slack event yourself on a dedicated Socket Mode app.`
|
|
976
976
|
);
|
|
977
977
|
}
|
|
978
978
|
let reactionDropped = false;
|
|
@@ -995,7 +995,7 @@ function emitSlackChannel(
|
|
|
995
995
|
}
|
|
996
996
|
if (reactionDropped) {
|
|
997
997
|
out.warnings.push(
|
|
998
|
-
"The Slack trigger marked triggering messages with a completion reaction;
|
|
998
|
+
"The Slack trigger marked triggering messages with a completion reaction; agentsdk has no completion-reaction option, so that signal is lost unless agent/instructions.md tells the agent to add the reaction itself via Slack tools."
|
|
999
999
|
);
|
|
1000
1000
|
}
|
|
1001
1001
|
if (socketMode) {
|
|
@@ -1042,8 +1042,8 @@ function emitSlackChannel(
|
|
|
1042
1042
|
if (blocksUnauthenticated) {
|
|
1043
1043
|
out.warnings.push(
|
|
1044
1044
|
includeBotPosts
|
|
1045
|
-
? "Some Slack watches only fired for authenticated Cursor users while others allowed anyone;
|
|
1046
|
-
: "The Slack trigger only fired for authenticated Cursor users (linked accounts on the owning team);
|
|
1045
|
+
? "Some Slack watches only fired for authenticated Cursor users while others allowed anyone; agentsdk has one bot-post setting per channel, so the export keeps bot posts ENABLED (the open watches depend on them) and the authenticated-only watches lose both their sender gate and their bot exclusion. Split the channels or add an onChannelPost gate."
|
|
1046
|
+
: "The Slack trigger only fired for authenticated Cursor users (linked accounts on the owning team); agentsdk has no equivalent sender gate, so the exported watch dispatches for ANY channel member (bot posts stay excluded). Add an onChannelPost gate in the channel if that matters."
|
|
1047
1047
|
);
|
|
1048
1048
|
}
|
|
1049
1049
|
const engagement: string[] = [];
|
|
@@ -1309,9 +1309,9 @@ const ACTION_STEPS: Partial<Record<keyof ActionJson, string>> = {
|
|
|
1309
1309
|
resolveReviewThreads:
|
|
1310
1310
|
"Verify: resolve_review_threads — no built-in equivalent; resolve threads via the GitHub GraphQL API in a hook or tool.",
|
|
1311
1311
|
microsoftTeams:
|
|
1312
|
-
"Verify: microsoft_teams post —
|
|
1312
|
+
"Verify: microsoft_teams post — agentsdk has no Teams channel; wire the Microsoft Teams API yourself.",
|
|
1313
1313
|
readMicrosoftTeams:
|
|
1314
|
-
"Verify: read_microsoft_teams —
|
|
1314
|
+
"Verify: read_microsoft_teams — agentsdk has no Teams tools; wire the Microsoft Teams API yourself.",
|
|
1315
1315
|
};
|
|
1316
1316
|
|
|
1317
1317
|
interface SlackIntegrationState {
|
|
@@ -1375,7 +1375,7 @@ function behaviorSteps(
|
|
|
1375
1375
|
const disabled = workflow.disabledDefaultTools ?? [];
|
|
1376
1376
|
if (disabled.length > 0) {
|
|
1377
1377
|
steps.push(
|
|
1378
|
-
`Verify: disabled_default_tools (${disabled.join(", ")}) —
|
|
1378
|
+
`Verify: disabled_default_tools (${disabled.join(", ")}) — agentsdk has no default automation tools; enforce the equivalent restriction in agent/instructions.md.`
|
|
1379
1379
|
);
|
|
1380
1380
|
}
|
|
1381
1381
|
return steps;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Types for `convertWorkflow` — the pure automation →
|
|
2
|
+
* Types for `convertWorkflow` — the pure automation → agentsdk project
|
|
3
3
|
* converter behind `agent-serve convert-automation`.
|
|
4
4
|
*
|
|
5
5
|
* The workflow arrives as generic proto3 JSON (lowerCamelCase field names,
|
|
@@ -296,6 +296,12 @@ async function parseRpcResponse(
|
|
|
296
296
|
export interface CursorBackendClientOptions {
|
|
297
297
|
backendUrl: string;
|
|
298
298
|
apiKey: string;
|
|
299
|
+
/**
|
|
300
|
+
* Re-read the key on each authenticated call. Hosted freeze listens
|
|
301
|
+
* unsigned and the binder drops the key after restore; a constructor-frozen
|
|
302
|
+
* key would miss that bind.
|
|
303
|
+
*/
|
|
304
|
+
getApiKey?: () => string;
|
|
299
305
|
logger?: (line: string) => void;
|
|
300
306
|
/** Test seam; defaults to global fetch. */
|
|
301
307
|
fetchImpl?: typeof fetch;
|
|
@@ -303,21 +309,26 @@ export interface CursorBackendClientOptions {
|
|
|
303
309
|
|
|
304
310
|
export class CursorBackendClient {
|
|
305
311
|
private readonly backendUrl: string;
|
|
306
|
-
private readonly
|
|
312
|
+
private readonly resolveApiKey: () => string;
|
|
307
313
|
private readonly logger: (line: string) => void;
|
|
308
314
|
private readonly fetchImpl: typeof fetch;
|
|
309
315
|
private token: { value: string; expiresAtMs: number } | undefined;
|
|
310
316
|
private tokenExchange: Promise<string> | undefined;
|
|
317
|
+
private lastExchangedApiKey: string | undefined;
|
|
311
318
|
|
|
312
319
|
constructor(options: CursorBackendClientOptions) {
|
|
313
320
|
this.backendUrl = options.backendUrl.endsWith("/")
|
|
314
321
|
? options.backendUrl.slice(0, -1)
|
|
315
322
|
: options.backendUrl;
|
|
316
|
-
this.
|
|
323
|
+
this.resolveApiKey = options.getApiKey ?? (() => options.apiKey);
|
|
317
324
|
this.logger = options.logger ?? (() => {});
|
|
318
325
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
319
326
|
}
|
|
320
327
|
|
|
328
|
+
private currentApiKey(): string {
|
|
329
|
+
return this.resolveApiKey().trim();
|
|
330
|
+
}
|
|
331
|
+
|
|
321
332
|
/** Who this API key acts as. */
|
|
322
333
|
async getMe(): Promise<CursorIdentity> {
|
|
323
334
|
return getMeResponseSchema.parse(await this.rpc("GetMe", {}));
|
|
@@ -443,7 +454,7 @@ export class CursorBackendClient {
|
|
|
443
454
|
method: "POST",
|
|
444
455
|
headers: {
|
|
445
456
|
"content-type": "application/json",
|
|
446
|
-
authorization: `Bearer ${this.
|
|
457
|
+
authorization: `Bearer ${this.currentApiKey()}`,
|
|
447
458
|
},
|
|
448
459
|
body: JSON.stringify({ repos: [...repos], permissions }),
|
|
449
460
|
signal: AbortSignal.timeout(30_000),
|
|
@@ -525,6 +536,18 @@ export class CursorBackendClient {
|
|
|
525
536
|
* when none is cached or it is near expiry.
|
|
526
537
|
*/
|
|
527
538
|
private getAccessToken(): Promise<string> {
|
|
539
|
+
const apiKey = this.currentApiKey();
|
|
540
|
+
if (apiKey === "") {
|
|
541
|
+
return Promise.reject(
|
|
542
|
+
new CursorBackendError(
|
|
543
|
+
"The Cursor backend client has no API key yet. Bind CURSOR_API_KEY or CURSOR_API_KEY_FILE before authenticated calls."
|
|
544
|
+
)
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
if (this.lastExchangedApiKey !== apiKey) {
|
|
548
|
+
this.token = undefined;
|
|
549
|
+
this.tokenExchange = undefined;
|
|
550
|
+
}
|
|
528
551
|
if (
|
|
529
552
|
this.token !== undefined &&
|
|
530
553
|
Date.now() < this.token.expiresAtMs - TOKEN_EXPIRY_LEEWAY_MS
|
|
@@ -541,13 +564,14 @@ export class CursorBackendClient {
|
|
|
541
564
|
}
|
|
542
565
|
|
|
543
566
|
private async exchangeApiKey(): Promise<string> {
|
|
567
|
+
const apiKey = this.currentApiKey();
|
|
544
568
|
let response: Response;
|
|
545
569
|
try {
|
|
546
570
|
response = await this.fetchImpl(`${this.backendUrl}${EXCHANGE_PATH}`, {
|
|
547
571
|
method: "POST",
|
|
548
572
|
headers: {
|
|
549
573
|
"content-type": "application/json",
|
|
550
|
-
authorization: `Bearer ${
|
|
574
|
+
authorization: `Bearer ${apiKey}`,
|
|
551
575
|
},
|
|
552
576
|
body: "{}",
|
|
553
577
|
signal: AbortSignal.timeout(30_000),
|
|
@@ -576,6 +600,7 @@ export class CursorBackendClient {
|
|
|
576
600
|
value: body.data.accessToken,
|
|
577
601
|
expiresAtMs: jwtExpiryMs(body.data.accessToken),
|
|
578
602
|
};
|
|
603
|
+
this.lastExchangedApiKey = apiKey;
|
|
579
604
|
this.logger("[agent-sdk] cursor account: minted a fresh access token");
|
|
580
605
|
return body.data.accessToken;
|
|
581
606
|
}
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* dashboard-revocable user API key — only the key is stored.
|
|
10
10
|
* - `CURSOR_API_KEY` (or `serve({ apiKey })`) wins over the stored key, so
|
|
11
11
|
* CI and hosted deployments stay env-driven.
|
|
12
|
+
* - `CURSOR_API_KEY_FILE` (or the hosted default path) is next: freeze bind
|
|
13
|
+
* drops the key at task time without restarting July.
|
|
14
|
+
* - Stored `july login` is last. Same order for hosted and self-hosted:
|
|
15
|
+
* option → env → file → login.
|
|
12
16
|
*
|
|
13
17
|
* There is deliberately no session-token storage, refresh-token rotation, or
|
|
14
18
|
* cross-tool credential sharing here: short-lived access tokens are minted
|
|
@@ -16,6 +20,7 @@
|
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
19
24
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
20
25
|
import { homedir } from "node:os";
|
|
21
26
|
import { dirname, join } from "node:path";
|
|
@@ -43,10 +48,11 @@ export function cursorBackendUrl(): string {
|
|
|
43
48
|
|
|
44
49
|
/**
|
|
45
50
|
* External API base URL for routes served only by the external-api
|
|
46
|
-
* microlith (`/internal/agent-serve/*`, engine aliases).
|
|
47
|
-
* these are mounted on api.cursor.com and 404 on
|
|
48
|
-
* deploy surface needs its own default.
|
|
49
|
-
* overrides everything for local dev, where
|
|
51
|
+
* microlith (`/internal/agent-serve/*`, `/v0/grokbot/*`, engine aliases).
|
|
52
|
+
* In production these are mounted on api.cursor.com and 404 on
|
|
53
|
+
* api2.cursor.sh, so the deploy surface needs its own default.
|
|
54
|
+
* `CURSOR_API_BASE_URL` still overrides everything for local dev, where
|
|
55
|
+
* one server mounts all routes.
|
|
50
56
|
*/
|
|
51
57
|
export function cursorExternalApiUrl(): string {
|
|
52
58
|
return trimTrailingSlash(
|
|
@@ -100,20 +106,13 @@ export function credentialsFilePath(): string {
|
|
|
100
106
|
return join(configDirPath(), "credentials.json");
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
let raw: string;
|
|
107
|
-
try {
|
|
108
|
-
raw = await readFile(credentialsFilePath(), "utf8");
|
|
109
|
-
} catch {
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
109
|
+
function parseStoredCredentialsJson(
|
|
110
|
+
raw: string
|
|
111
|
+
): StoredCredentials | undefined {
|
|
112
112
|
let parsed: unknown;
|
|
113
113
|
try {
|
|
114
114
|
parsed = JSON.parse(raw) as unknown;
|
|
115
115
|
} catch {
|
|
116
|
-
// Corrupt file — treat as signed out; login rewrites it.
|
|
117
116
|
return undefined;
|
|
118
117
|
}
|
|
119
118
|
const credentials = storedCredentialsSchema.safeParse(parsed);
|
|
@@ -126,6 +125,28 @@ export async function readStoredCredentials(): Promise<
|
|
|
126
125
|
};
|
|
127
126
|
}
|
|
128
127
|
|
|
128
|
+
export function readStoredCredentialsSync(): StoredCredentials | undefined {
|
|
129
|
+
let raw: string;
|
|
130
|
+
try {
|
|
131
|
+
raw = readFileSync(credentialsFilePath(), "utf8");
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return parseStoredCredentialsJson(raw);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function readStoredCredentials(): Promise<
|
|
139
|
+
StoredCredentials | undefined
|
|
140
|
+
> {
|
|
141
|
+
let raw: string;
|
|
142
|
+
try {
|
|
143
|
+
raw = await readFile(credentialsFilePath(), "utf8");
|
|
144
|
+
} catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return parseStoredCredentialsJson(raw);
|
|
148
|
+
}
|
|
149
|
+
|
|
129
150
|
export async function writeStoredCredentials(
|
|
130
151
|
credentials: StoredCredentials
|
|
131
152
|
): Promise<void> {
|
|
@@ -148,10 +169,10 @@ export async function clearStoredCredentials(): Promise<boolean> {
|
|
|
148
169
|
}
|
|
149
170
|
|
|
150
171
|
// ============================================================================
|
|
151
|
-
// Resolution (option > env > stored login)
|
|
172
|
+
// Resolution (option > env > file > stored login)
|
|
152
173
|
// ============================================================================
|
|
153
174
|
|
|
154
|
-
export type ApiKeySource = "option" | "env" | "login";
|
|
175
|
+
export type ApiKeySource = "option" | "env" | "file" | "login";
|
|
155
176
|
|
|
156
177
|
export interface ResolvedApiKey {
|
|
157
178
|
apiKey: string;
|
|
@@ -160,15 +181,43 @@ export interface ResolvedApiKey {
|
|
|
160
181
|
}
|
|
161
182
|
|
|
162
183
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
184
|
+
* Hosted task-bind path. Not a secret: the snapshot may name this path.
|
|
185
|
+
* The binder writes the key here after restore; July re-reads on each use.
|
|
186
|
+
* Override with `CURSOR_API_KEY_FILE`. A missing file is unsigned — self-hosted
|
|
187
|
+
* then uses `CURSOR_API_KEY` or stored login.
|
|
167
188
|
*/
|
|
168
|
-
export
|
|
189
|
+
export const DEFAULT_CURSOR_API_KEY_FILE: string =
|
|
190
|
+
"/run/cursor/secrets/CURSOR_API_KEY";
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Read `CURSOR_API_KEY_FILE` (or {@link DEFAULT_CURSOR_API_KEY_FILE} when
|
|
194
|
+
* unset). Missing/empty files are unsigned, not an error — freeze bake
|
|
195
|
+
* listens before the binder drops the key; self-hosted falls through to
|
|
196
|
+
* env or login.
|
|
197
|
+
*/
|
|
198
|
+
export function readApiKeyFromFile(filePath?: string): string | undefined {
|
|
199
|
+
const configured = filePath ?? process.env.CURSOR_API_KEY_FILE;
|
|
200
|
+
const path =
|
|
201
|
+
configured !== undefined && configured.trim() !== ""
|
|
202
|
+
? configured.trim()
|
|
203
|
+
: DEFAULT_CURSOR_API_KEY_FILE;
|
|
204
|
+
try {
|
|
205
|
+
const raw = readFileSync(path, "utf8").trim();
|
|
206
|
+
return raw === "" ? undefined : raw;
|
|
207
|
+
} catch {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* First present source wins. We define the order; operators do not.
|
|
214
|
+
* `--api-key` / `serve({ apiKey })` → `CURSOR_API_KEY` → `CURSOR_API_KEY_FILE`
|
|
215
|
+
* (hosted default path if unset) → stored `july login`.
|
|
216
|
+
*/
|
|
217
|
+
export function resolveApiKeySync(options?: {
|
|
169
218
|
apiKey?: string;
|
|
170
219
|
backendUrl?: string;
|
|
171
|
-
}):
|
|
220
|
+
}): ResolvedApiKey | undefined {
|
|
172
221
|
const backendUrl = options?.backendUrl ?? cursorBackendUrl();
|
|
173
222
|
if (options?.apiKey !== undefined && options.apiKey.trim() !== "") {
|
|
174
223
|
return { apiKey: options.apiKey.trim(), source: "option", backendUrl };
|
|
@@ -177,15 +226,29 @@ export async function resolveApiKey(options?: {
|
|
|
177
226
|
if (envKey !== undefined && envKey.trim() !== "") {
|
|
178
227
|
return { apiKey: envKey.trim(), source: "env", backendUrl };
|
|
179
228
|
}
|
|
180
|
-
const
|
|
229
|
+
const fileKey = readApiKeyFromFile();
|
|
230
|
+
if (fileKey !== undefined) {
|
|
231
|
+
return { apiKey: fileKey, source: "file", backendUrl };
|
|
232
|
+
}
|
|
233
|
+
const stored = readStoredCredentialsSync();
|
|
181
234
|
if (stored !== undefined && stored.backendUrl === backendUrl) {
|
|
182
235
|
return { apiKey: stored.apiKey, source: "login", backendUrl };
|
|
183
236
|
}
|
|
184
237
|
return undefined;
|
|
185
238
|
}
|
|
186
239
|
|
|
240
|
+
/**
|
|
241
|
+
* {@link resolveApiKeySync} as a Promise for call sites that already await.
|
|
242
|
+
*/
|
|
243
|
+
export async function resolveApiKey(options?: {
|
|
244
|
+
apiKey?: string;
|
|
245
|
+
backendUrl?: string;
|
|
246
|
+
}): Promise<ResolvedApiKey | undefined> {
|
|
247
|
+
return resolveApiKeySync(options);
|
|
248
|
+
}
|
|
249
|
+
|
|
187
250
|
/** One-line hint for "no credentials" errors, shared across surfaces. */
|
|
188
|
-
export const SIGN_IN_HINT: string = `Run \`${CLI_COMMAND_NAME} login\` (or set CURSOR_API_KEY / pass serve({ apiKey })).`;
|
|
251
|
+
export const SIGN_IN_HINT: string = `Run \`${CLI_COMMAND_NAME} login\` (or set CURSOR_API_KEY / CURSOR_API_KEY_FILE / pass serve({ apiKey })).`;
|
|
189
252
|
|
|
190
253
|
// ============================================================================
|
|
191
254
|
// PKCE browser login (mirrors cursor-agent / Sand)
|
|
@@ -2021,7 +2021,7 @@ async function loadABConfig(
|
|
|
2021
2021
|
severity: "error",
|
|
2022
2022
|
path: displayPath(ctx, path),
|
|
2023
2023
|
message:
|
|
2024
|
-
"ab.config.ts must default-export defineABConfig({ ... }) from @
|
|
2024
|
+
"ab.config.ts must default-export defineABConfig({ ... }) from @cursor/july/ab.",
|
|
2025
2025
|
});
|
|
2026
2026
|
return undefined;
|
|
2027
2027
|
}
|
|
@@ -81,6 +81,37 @@ export function packageVersion(): string {
|
|
|
81
81
|
return typeof parsed.version === "string" ? parsed.version : "0.0.0";
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** Facts `agent-sdk version` / `--version` print. */
|
|
85
|
+
export interface CliVersionInfo {
|
|
86
|
+
/** Published npm name (`@cursor/july`). */
|
|
87
|
+
name: string;
|
|
88
|
+
/** `package.json` version of this install. */
|
|
89
|
+
version: string;
|
|
90
|
+
/** Bin the process was invoked as (`agent-sdk` / `agent-serve`). */
|
|
91
|
+
cli: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function cliVersionInfo(): CliVersionInfo {
|
|
95
|
+
return {
|
|
96
|
+
name: PACKAGE_NAME,
|
|
97
|
+
version: packageVersion(),
|
|
98
|
+
cli: CLI_COMMAND_NAME,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Text or JSON for `version` / `--version` / `-V`.
|
|
104
|
+
* Text is the bare `package.json` version (npm-style). `--json` is the
|
|
105
|
+
* complete `{ name, version, cli }` payload.
|
|
106
|
+
*/
|
|
107
|
+
export function formatCliVersion(options?: { json?: boolean }): string {
|
|
108
|
+
const info = cliVersionInfo();
|
|
109
|
+
if (options?.json === true) {
|
|
110
|
+
return `${JSON.stringify(info, null, 2)}\n`;
|
|
111
|
+
}
|
|
112
|
+
return `${info.version}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
84
115
|
/**
|
|
85
116
|
* Authoring entry subpaths and their dist-relative output files — the
|
|
86
117
|
* runtime mirror of the package `exports` map (the source of truth;
|
|
@@ -479,7 +479,7 @@ function resolveEvalMaxConcurrency(config: Partial<EvalConfigFile>): number {
|
|
|
479
479
|
if (config.maxConcurrency === undefined) {
|
|
480
480
|
throw new Error(
|
|
481
481
|
`evals/evals.config.ts must define maxConcurrency (1–${EVAL_MAX_CONCURRENCY_LIMIT}; hard limit is due to model provider request limits). Example:\n` +
|
|
482
|
-
` import { defineEvalConfig } from "@
|
|
482
|
+
` import { defineEvalConfig } from "@cursor/july/evals";\n` +
|
|
483
483
|
` export default defineEvalConfig({ maxConcurrency: 20 });`
|
|
484
484
|
);
|
|
485
485
|
}
|
|
@@ -13,15 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
15
|
import type { RunnerTurnRequest, TurnOutcome } from "../../types.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
cursorExternalApiUrl,
|
|
18
|
+
resolveApiKeySync,
|
|
19
|
+
} from "../cursor/credentials.js";
|
|
17
20
|
import type { AgentRunner } from "./../sdk-runner.js";
|
|
18
21
|
|
|
19
22
|
export interface GrokBotRunnerOptions {
|
|
20
|
-
/** Cursor API key.
|
|
23
|
+
/** Cursor API key. Resolved as option → env → file → stored login. */
|
|
21
24
|
apiKey?: string;
|
|
22
25
|
/** Test seam. Defaults to global fetch. */
|
|
23
26
|
fetchImpl?: typeof fetch;
|
|
24
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Test seam. Defaults to {@link cursorExternalApiUrl} (`api.cursor.com`).
|
|
29
|
+
* `/v0/grokbot` is an external-api microlith route — it 404s on api2.
|
|
30
|
+
*/
|
|
25
31
|
backendUrl?: string;
|
|
26
32
|
/** Test seam. Poll interval while a turn runs. */
|
|
27
33
|
pollIntervalMs?: number;
|
|
@@ -89,14 +95,17 @@ class GrokBotApiClient {
|
|
|
89
95
|
|
|
90
96
|
constructor(options: GrokBotRunnerOptions) {
|
|
91
97
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
92
|
-
this.baseUrl = (options.backendUrl ??
|
|
98
|
+
this.baseUrl = (options.backendUrl ?? cursorExternalApiUrl()).replace(
|
|
93
99
|
/\/$/,
|
|
94
100
|
""
|
|
95
101
|
);
|
|
96
|
-
const apiKey =
|
|
102
|
+
const apiKey = resolveApiKeySync({
|
|
103
|
+
apiKey: options.apiKey,
|
|
104
|
+
backendUrl: options.backendUrl,
|
|
105
|
+
})?.apiKey;
|
|
97
106
|
if (apiKey === undefined || apiKey === "") {
|
|
98
107
|
throw new Error(
|
|
99
|
-
'runtime: "grokbot" needs a Cursor API key (CURSOR_API_KEY or agent-sdk login) to reach the hosted Grok Bot harness.'
|
|
108
|
+
'runtime: "grokbot" needs a Cursor API key (CURSOR_API_KEY, CURSOR_API_KEY_FILE, or agent-sdk login) to reach the hosted Grok Bot harness.'
|
|
100
109
|
);
|
|
101
110
|
}
|
|
102
111
|
this.apiKey = apiKey;
|
|
@@ -6,6 +6,7 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
8
8
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
9
|
+
import { runfiles } from "@bazel/runfiles";
|
|
9
10
|
import type { ModelSetting } from "../types.js";
|
|
10
11
|
import {
|
|
11
12
|
canonicalTemplateName,
|
|
@@ -103,7 +104,7 @@ export function resolveTemplate(name: string): TemplateResolution {
|
|
|
103
104
|
};
|
|
104
105
|
}
|
|
105
106
|
const entry = TEMPLATES[resolved];
|
|
106
|
-
const dir =
|
|
107
|
+
const dir = resolvePackagePath("templates", entry.dir);
|
|
107
108
|
return {
|
|
108
109
|
ok: true,
|
|
109
110
|
name: resolved,
|
|
@@ -115,6 +116,15 @@ export function resolveTemplate(name: string): TemplateResolution {
|
|
|
115
116
|
};
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
function resolvePackagePath(...segments: string[]): string {
|
|
120
|
+
if (runfiles.workspace !== undefined) {
|
|
121
|
+
return runfiles.resolveWorkspaceRelative(
|
|
122
|
+
["packages", "agent-serve", ...segments].join("/")
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return join(packageRootDir(), ...segments);
|
|
126
|
+
}
|
|
127
|
+
|
|
118
128
|
export type InitTargetCheck = { ok: true } | { ok: false; message: string };
|
|
119
129
|
|
|
120
130
|
/**
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type TurnUsage,
|
|
30
30
|
} from "../types.js";
|
|
31
31
|
import { mapSdkTokenUsage, usageTokenSum } from "./cloud-turn-cost.js";
|
|
32
|
+
import { resolveApiKeySync } from "./cursor/credentials.js";
|
|
32
33
|
|
|
33
34
|
/** Internal seam between the session engine and the Cursor SDK harness. */
|
|
34
35
|
export interface AgentRunner {
|
|
@@ -427,14 +428,19 @@ export class CursorSdkRunner implements AgentRunner {
|
|
|
427
428
|
(this.options.logger ?? (() => {}))(line);
|
|
428
429
|
}
|
|
429
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Re-resolve on every turn. Hosted freeze snapshots listen unsigned; the
|
|
433
|
+
* binder drops `CURSOR_API_KEY` / `CURSOR_API_KEY_FILE` at task time into
|
|
434
|
+
* the already-running process, which must not have closed over `undefined`.
|
|
435
|
+
*/
|
|
430
436
|
private requireApiKey(): string {
|
|
431
|
-
const
|
|
432
|
-
if (
|
|
437
|
+
const resolved = resolveApiKeySync({ apiKey: this.options.apiKey });
|
|
438
|
+
if (resolved === undefined) {
|
|
433
439
|
throw new Error(
|
|
434
|
-
"The Cursor SDK runner requires a Cursor credential: run `agent-serve login`, set CURSOR_API_KEY, or pass serve({ apiKey })."
|
|
440
|
+
"The Cursor SDK runner requires a Cursor credential: run `agent-serve login`, set CURSOR_API_KEY or CURSOR_API_KEY_FILE, or pass serve({ apiKey })."
|
|
435
441
|
);
|
|
436
442
|
}
|
|
437
|
-
return apiKey;
|
|
443
|
+
return resolved.apiKey;
|
|
438
444
|
}
|
|
439
445
|
|
|
440
446
|
private buildSharedOptions(
|