@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.
Files changed (165) hide show
  1. package/README.md +95 -94
  2. package/dist/ab.d.ts +1 -1
  3. package/dist/ab.js +1 -1
  4. package/dist/artifacts.d.ts +1 -1
  5. package/dist/artifacts.js +1 -1
  6. package/dist/bin/agent-serve.js +20 -7
  7. package/dist/channels.d.ts +1 -1
  8. package/dist/channels.js +1 -1
  9. package/dist/connections.d.ts +1 -1
  10. package/dist/connections.js +1 -1
  11. package/dist/docs/404.html +2 -2
  12. package/dist/docs/ab.html +3 -3
  13. package/dist/docs/assets/{app.DUOPbN18.js → app.wiNkt6G7.js} +1 -1
  14. package/dist/docs/assets/chunks/@localSearchIndexroot.FV0R6kOb.js +1 -0
  15. package/dist/docs/assets/chunks/{VPLocalSearchBox.CmWbGcGk.js → VPLocalSearchBox.ByhUGu47.js} +1 -1
  16. package/dist/docs/assets/chunks/{theme.S57OeOLA.js → theme.Dx7j_-0n.js} +2 -2
  17. package/dist/docs/assets/{concepts.md.BXAm6G-C.js → concepts.md.2NfkGjqM.js} +1 -1
  18. package/dist/docs/assets/{reference_project-layout.md.D3MdHM2z.js → reference_project-layout.md.CueaKpjr.js} +5 -20
  19. package/dist/docs/assets/reference_project-layout.md.CueaKpjr.lean.js +1 -0
  20. package/dist/docs/assets/{reference_tools.md.DTg_kEsx.js → reference_tools.md.XmeFP_3d.js} +23 -2
  21. package/dist/docs/assets/{reference_tools.md.DTg_kEsx.lean.js → reference_tools.md.XmeFP_3d.lean.js} +1 -1
  22. package/dist/docs/assets/{scaffolding-agents.md.CiGsJ1aw.js → scaffolding-agents.md.BpMFXv2J.js} +1 -1
  23. package/dist/docs/building-with-agents.html +3 -3
  24. package/dist/docs/concepts.html +5 -5
  25. package/dist/docs/deployment.html +3 -3
  26. package/dist/docs/evals.html +3 -3
  27. package/dist/docs/example-agents/approval-buddy.html +3 -3
  28. package/dist/docs/example-agents/benny.html +3 -3
  29. package/dist/docs/example-agents/bugbot.html +3 -3
  30. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  31. package/dist/docs/example-agents/codeowners-review.html +3 -3
  32. package/dist/docs/example-agents/concierge.html +3 -3
  33. package/dist/docs/example-agents/fsd.html +3 -3
  34. package/dist/docs/example-agents/index.html +3 -3
  35. package/dist/docs/example-agents/knowledge-base.html +3 -3
  36. package/dist/docs/example-agents/oncall.html +3 -3
  37. package/dist/docs/example-agents/security-reviewer.html +3 -3
  38. package/dist/docs/example-agents/slack-agent.html +3 -3
  39. package/dist/docs/example-agents/weather-agent.html +3 -3
  40. package/dist/docs/guides/agent-to-agent.html +3 -3
  41. package/dist/docs/guides/cloud-runtime.html +3 -3
  42. package/dist/docs/guides/convert-automation.html +3 -3
  43. package/dist/docs/guides/github.html +3 -3
  44. package/dist/docs/guides/human-in-the-loop.html +3 -3
  45. package/dist/docs/guides/mcp-oauth.html +3 -3
  46. package/dist/docs/guides/opentelemetry.html +3 -3
  47. package/dist/docs/guides/slack.html +3 -3
  48. package/dist/docs/guides/webhooks.html +3 -3
  49. package/dist/docs/hashmap.json +1 -1
  50. package/dist/docs/hillclimbing.html +3 -3
  51. package/dist/docs/index.html +3 -3
  52. package/dist/docs/quickstart.html +3 -3
  53. package/dist/docs/reference/agent-config.html +3 -3
  54. package/dist/docs/reference/artifacts.html +3 -3
  55. package/dist/docs/reference/channels.html +3 -3
  56. package/dist/docs/reference/cli.html +3 -3
  57. package/dist/docs/reference/connections.html +3 -3
  58. package/dist/docs/reference/hooks.html +3 -3
  59. package/dist/docs/reference/http-api.html +3 -3
  60. package/dist/docs/reference/instructions.html +3 -3
  61. package/dist/docs/reference/playground.html +3 -3
  62. package/dist/docs/reference/project-layout.html +8 -23
  63. package/dist/docs/reference/prompt.html +3 -3
  64. package/dist/docs/reference/schedules.html +3 -3
  65. package/dist/docs/reference/sessions.html +3 -3
  66. package/dist/docs/reference/skills.html +3 -3
  67. package/dist/docs/reference/subagents.html +3 -3
  68. package/dist/docs/reference/tools.html +27 -6
  69. package/dist/docs/scaffolding-agents.html +5 -5
  70. package/dist/docs/storage.html +3 -3
  71. package/dist/docs/templates/agentic-owners.html +3 -3
  72. package/dist/docs/templates/demo.html +3 -3
  73. package/dist/docs/templates/pr-autofixer.html +3 -3
  74. package/dist/docs/templates/security-reviewer.html +3 -3
  75. package/dist/docs/templates/triage.html +3 -3
  76. package/dist/docs/troubleshooting.html +3 -3
  77. package/dist/evals.d.ts +1 -1
  78. package/dist/evals.js +1 -1
  79. package/dist/hooks.d.ts +1 -1
  80. package/dist/hooks.js +1 -1
  81. package/dist/index.d.ts +2 -2
  82. package/dist/index.js +2 -2
  83. package/dist/internal/cli-convert-automation.d.ts +1 -1
  84. package/dist/internal/cli-convert-automation.js +1 -1
  85. package/dist/internal/cli-cursor.d.ts +12 -4
  86. package/dist/internal/cli-cursor.d.ts.map +1 -1
  87. package/dist/internal/cli-cursor.js +11 -4
  88. package/dist/internal/convert-automation/convert-workflow.d.ts +1 -1
  89. package/dist/internal/convert-automation/convert-workflow.js +9 -9
  90. package/dist/internal/convert-automation/types.d.ts +1 -1
  91. package/dist/internal/convert-automation/types.js +1 -1
  92. package/dist/internal/cursor/backend-client.d.ts +9 -1
  93. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  94. package/dist/internal/cursor/backend-client.js +19 -6
  95. package/dist/internal/cursor/credentials.d.ts +35 -9
  96. package/dist/internal/cursor/credentials.d.ts.map +1 -1
  97. package/dist/internal/cursor/credentials.js +92 -39
  98. package/dist/internal/discovery.js +1 -1
  99. package/dist/internal/distribution.d.ts +18 -0
  100. package/dist/internal/distribution.d.ts.map +1 -1
  101. package/dist/internal/distribution.js +19 -0
  102. package/dist/internal/eval-runner.js +1 -1
  103. package/dist/internal/grokbot/runner.d.ts +5 -2
  104. package/dist/internal/grokbot/runner.d.ts.map +1 -1
  105. package/dist/internal/grokbot/runner.js +10 -6
  106. package/dist/internal/init-project.d.ts.map +1 -1
  107. package/dist/internal/init-project.js +8 -1
  108. package/dist/internal/sdk-runner.d.ts +5 -0
  109. package/dist/internal/sdk-runner.d.ts.map +1 -1
  110. package/dist/internal/sdk-runner.js +10 -5
  111. package/dist/internal/server.d.ts.map +1 -1
  112. package/dist/internal/server.js +80 -36
  113. package/dist/playground/assets/index-BpVS-paP.css +1 -0
  114. package/dist/playground/index.html +2 -2
  115. package/dist/reminders.d.ts +1 -1
  116. package/dist/reminders.js +1 -1
  117. package/dist/schedules.d.ts +1 -1
  118. package/dist/schedules.js +1 -1
  119. package/dist/skills.d.ts +1 -1
  120. package/dist/skills.js +1 -1
  121. package/dist/storage.d.ts +1 -1
  122. package/dist/storage.js +1 -1
  123. package/dist/tools.d.ts +1 -1
  124. package/dist/tools.js +1 -1
  125. package/dist/types.d.ts +2 -2
  126. package/dist/types.d.ts.map +1 -1
  127. package/dist/types.js +1 -1
  128. package/docs/concepts.md +2 -1
  129. package/docs/reference/project-layout.md +5 -20
  130. package/docs/reference/tools.md +53 -3
  131. package/docs/scaffolding-agents.md +2 -0
  132. package/package.json +2 -1
  133. package/src/ab.ts +1 -1
  134. package/src/artifacts.ts +1 -1
  135. package/src/bin/agent-serve.ts +20 -6
  136. package/src/channels.ts +1 -1
  137. package/src/connections.ts +1 -1
  138. package/src/evals.ts +1 -1
  139. package/src/hooks.ts +1 -1
  140. package/src/index.ts +2 -2
  141. package/src/internal/cli-convert-automation.ts +1 -1
  142. package/src/internal/cli-cursor.ts +18 -5
  143. package/src/internal/convert-automation/convert-workflow.ts +9 -9
  144. package/src/internal/convert-automation/types.ts +1 -1
  145. package/src/internal/cursor/backend-client.ts +29 -4
  146. package/src/internal/cursor/credentials.ts +87 -24
  147. package/src/internal/discovery.ts +1 -1
  148. package/src/internal/distribution.ts +31 -0
  149. package/src/internal/eval-runner.ts +1 -1
  150. package/src/internal/grokbot/runner.ts +15 -6
  151. package/src/internal/init-project.ts +11 -1
  152. package/src/internal/sdk-runner.ts +10 -4
  153. package/src/internal/server.ts +95 -33
  154. package/src/reminders.ts +1 -1
  155. package/src/schedules.ts +1 -1
  156. package/src/skills.ts +1 -1
  157. package/src/storage.ts +1 -1
  158. package/src/tools.ts +1 -1
  159. package/src/types.ts +2 -2
  160. package/dist/docs/assets/chunks/@localSearchIndexroot.CxCtxfDE.js +0 -1
  161. package/dist/docs/assets/reference_project-layout.md.D3MdHM2z.lean.js +0 -1
  162. package/dist/playground/assets/index-DQGZnAI0.css +0 -1
  163. /package/dist/docs/assets/{concepts.md.BXAm6G-C.lean.js → concepts.md.2NfkGjqM.lean.js} +0 -0
  164. /package/dist/docs/assets/{scaffolding-agents.md.CiGsJ1aw.lean.js → scaffolding-agents.md.BpMFXv2J.lean.js} +0 -0
  165. /package/dist/playground/assets/{index-BmiIjFlM.js → index-DxapiDj_.js} +0 -0
@@ -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 "@anysphere/agent-serve/connections";
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 "@anysphere/agent-serve/evals";
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
@@ -6,7 +6,7 @@
6
6
  * transcripts to your own database). Handlers are observe-only.
7
7
  *
8
8
  * ```ts
9
- * import { defineHook } from "@anysphere/agent-serve/hooks";
9
+ * import { defineHook } from "@cursor/july/hooks";
10
10
  *
11
11
  * export default defineHook({
12
12
  * events: {
package/src/index.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * @anysphere/agent-serve
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 "@anysphere/agent-serve";
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 agentkit project.
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
- * Otherwise an interactive host runs {@link cmdLogin}; a non-interactive
153
- * one (CI, Docker) fails fast with {@link SIGN_IN_HINT} instead of polling
154
- * a browser that will never open.
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 → agentkit project converter.
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 agentkit:
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 agentkit equivalent — its config is inlined as a comment in ${path}; handle the Slack event yourself on a dedicated Socket Mode app.`
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; agentkit 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."
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; agentkit 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); agentkit 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."
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 — agentkit has no Teams channel; wire the Microsoft Teams API yourself.",
1312
+ "Verify: microsoft_teams post — agentsdk has no Teams channel; wire the Microsoft Teams API yourself.",
1313
1313
  readMicrosoftTeams:
1314
- "Verify: read_microsoft_teams — agentkit has no Teams tools; wire the Microsoft Teams API yourself.",
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(", ")}) — agentkit has no default automation tools; enforce the equivalent restriction in agent/instructions.md.`
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 → agentkit project
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 apiKey: string;
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.apiKey = options.apiKey;
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.apiKey}`,
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 ${this.apiKey}`,
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). In production
47
- * these are mounted on api.cursor.com and 404 on api2.cursor.sh, so the
48
- * deploy surface needs its own default. `CURSOR_API_BASE_URL` still
49
- * overrides everything for local dev, where one server mounts all routes.
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
- export async function readStoredCredentials(): Promise<
104
- StoredCredentials | undefined
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
- * Resolve the account API key for `backendUrl` (defaults to the active
164
- * backend): explicit option `CURSOR_API_KEY` stored login. A stored key
165
- * minted against a different backend is ignored rather than sent to the
166
- * wrong host.
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 async function resolveApiKey(options?: {
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
- }): Promise<ResolvedApiKey | undefined> {
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 stored = await readStoredCredentials();
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 @anysphere/agent-serve/ab.",
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 "@anysphere/agent-serve/evals";\n` +
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 { cursorBackendUrl } from "../cursor/credentials.js";
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. Defaults to the `CURSOR_API_KEY` environment variable. */
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
- /** Test seam. Defaults to {@link cursorBackendUrl}. */
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 ?? cursorBackendUrl()).replace(
98
+ this.baseUrl = (options.backendUrl ?? cursorExternalApiUrl()).replace(
93
99
  /\/$/,
94
100
  ""
95
101
  );
96
- const apiKey = options.apiKey ?? process.env.CURSOR_API_KEY;
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 = join(packageRootDir(), "templates", entry.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 apiKey = this.options.apiKey ?? process.env.CURSOR_API_KEY;
432
- if (apiKey === undefined || apiKey.trim() === "") {
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(