@cursor/july 0.1.88 → 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 (193) 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 +21 -8
  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/advertise-tools.d.ts.map +1 -1
  84. package/dist/internal/advertise-tools.js +1 -1
  85. package/dist/internal/cli-ax.js +1 -1
  86. package/dist/internal/cli-convert-automation.d.ts +1 -1
  87. package/dist/internal/cli-convert-automation.js +1 -1
  88. package/dist/internal/cli-cursor.d.ts +12 -4
  89. package/dist/internal/cli-cursor.d.ts.map +1 -1
  90. package/dist/internal/cli-cursor.js +11 -4
  91. package/dist/internal/cli-deploy.d.ts +3 -2
  92. package/dist/internal/cli-deploy.d.ts.map +1 -1
  93. package/dist/internal/cli-deploy.js +84 -9
  94. package/dist/internal/convert-automation/convert-workflow.d.ts +1 -1
  95. package/dist/internal/convert-automation/convert-workflow.js +9 -9
  96. package/dist/internal/convert-automation/types.d.ts +1 -1
  97. package/dist/internal/convert-automation/types.js +1 -1
  98. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  99. package/dist/internal/cursor/account-mcp.js +8 -4
  100. package/dist/internal/cursor/backend-client.d.ts +17 -1
  101. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  102. package/dist/internal/cursor/backend-client.js +42 -7
  103. package/dist/internal/cursor/credentials.d.ts +35 -9
  104. package/dist/internal/cursor/credentials.d.ts.map +1 -1
  105. package/dist/internal/cursor/credentials.js +92 -39
  106. package/dist/internal/deploy-client.d.ts +42 -1
  107. package/dist/internal/deploy-client.d.ts.map +1 -1
  108. package/dist/internal/deploy-client.js +70 -2
  109. package/dist/internal/discovery.js +1 -1
  110. package/dist/internal/distribution.d.ts +18 -0
  111. package/dist/internal/distribution.d.ts.map +1 -1
  112. package/dist/internal/distribution.js +19 -0
  113. package/dist/internal/eval-runner.js +1 -1
  114. package/dist/internal/grokbot/runner.d.ts +5 -2
  115. package/dist/internal/grokbot/runner.d.ts.map +1 -1
  116. package/dist/internal/grokbot/runner.js +10 -6
  117. package/dist/internal/init-project.d.ts.map +1 -1
  118. package/dist/internal/init-project.js +8 -1
  119. package/dist/internal/mcp-endpoint.js +2 -1
  120. package/dist/internal/sdk-runner.d.ts +5 -0
  121. package/dist/internal/sdk-runner.d.ts.map +1 -1
  122. package/dist/internal/sdk-runner.js +10 -5
  123. package/dist/internal/server.d.ts.map +1 -1
  124. package/dist/internal/server.js +80 -36
  125. package/dist/internal/session-engine.d.ts +9 -2
  126. package/dist/internal/session-engine.d.ts.map +1 -1
  127. package/dist/internal/session-engine.js +85 -28
  128. package/dist/internal/tool-policy.d.ts +33 -1
  129. package/dist/internal/tool-policy.d.ts.map +1 -1
  130. package/dist/internal/tool-policy.js +53 -0
  131. package/dist/playground/assets/index-BpVS-paP.css +1 -0
  132. package/dist/playground/assets/{index-Bqn91tW4.js → index-DxapiDj_.js} +45 -45
  133. package/dist/playground/index.html +2 -2
  134. package/dist/reminders.d.ts +1 -1
  135. package/dist/reminders.js +1 -1
  136. package/dist/schedules.d.ts +1 -1
  137. package/dist/schedules.js +1 -1
  138. package/dist/skills.d.ts +1 -1
  139. package/dist/skills.js +1 -1
  140. package/dist/storage.d.ts +1 -1
  141. package/dist/storage.js +1 -1
  142. package/dist/tools.d.ts +1 -1
  143. package/dist/tools.js +1 -1
  144. package/dist/types.d.ts +24 -2
  145. package/dist/types.d.ts.map +1 -1
  146. package/dist/types.js +1 -1
  147. package/docs/concepts.md +2 -1
  148. package/docs/reference/project-layout.md +5 -20
  149. package/docs/reference/tools.md +53 -3
  150. package/docs/scaffolding-agents.md +2 -0
  151. package/package.json +2 -1
  152. package/skills/debug/SKILL.md +1 -2
  153. package/skills/framework-map/SKILL.md +1 -2
  154. package/src/ab.ts +1 -1
  155. package/src/artifacts.ts +1 -1
  156. package/src/bin/agent-serve.ts +21 -7
  157. package/src/channels.ts +1 -1
  158. package/src/connections.ts +1 -1
  159. package/src/evals.ts +1 -1
  160. package/src/hooks.ts +1 -1
  161. package/src/index.ts +2 -2
  162. package/src/internal/advertise-tools.ts +1 -0
  163. package/src/internal/cli-ax.ts +1 -1
  164. package/src/internal/cli-convert-automation.ts +1 -1
  165. package/src/internal/cli-cursor.ts +18 -5
  166. package/src/internal/cli-deploy.ts +123 -15
  167. package/src/internal/convert-automation/convert-workflow.ts +9 -9
  168. package/src/internal/convert-automation/types.ts +1 -1
  169. package/src/internal/cursor/account-mcp.ts +15 -1
  170. package/src/internal/cursor/backend-client.ts +62 -4
  171. package/src/internal/cursor/credentials.ts +87 -24
  172. package/src/internal/deploy-client.ts +122 -2
  173. package/src/internal/discovery.ts +1 -1
  174. package/src/internal/distribution.ts +31 -0
  175. package/src/internal/eval-runner.ts +1 -1
  176. package/src/internal/grokbot/runner.ts +15 -6
  177. package/src/internal/init-project.ts +11 -1
  178. package/src/internal/mcp-endpoint.ts +2 -1
  179. package/src/internal/sdk-runner.ts +10 -4
  180. package/src/internal/server.ts +95 -33
  181. package/src/internal/session-engine.ts +86 -6
  182. package/src/internal/tool-policy.ts +80 -1
  183. package/src/reminders.ts +1 -1
  184. package/src/schedules.ts +1 -1
  185. package/src/skills.ts +1 -1
  186. package/src/storage.ts +1 -1
  187. package/src/tools.ts +1 -1
  188. package/src/types.ts +21 -2
  189. package/dist/docs/assets/chunks/@localSearchIndexroot.CxCtxfDE.js +0 -1
  190. package/dist/docs/assets/reference_project-layout.md.D3MdHM2z.lean.js +0 -1
  191. package/dist/playground/assets/index-C0_5hOsf.css +0 -1
  192. /package/dist/docs/assets/{concepts.md.BXAm6G-C.lean.js → concepts.md.2NfkGjqM.lean.js} +0 -0
  193. /package/dist/docs/assets/{scaffolding-agents.md.CiGsJ1aw.lean.js → scaffolding-agents.md.BpMFXv2J.lean.js} +0 -0
@@ -15,6 +15,8 @@
15
15
 
16
16
  import { randomUUID } from "node:crypto";
17
17
  import { z } from "zod";
18
+ import type { McpToolAnnotations } from "../../types.js";
19
+ import { boundedToolAnnotations } from "../mcp-host.js";
18
20
 
19
21
  const CLIENT_VERSION = "agent-serve-0.1.0";
20
22
  const EXCHANGE_PATH = "/auth/exchange_user_api_key";
@@ -85,6 +87,8 @@ const mcpToolSchema = z.object({
85
87
  inputSchema: z.unknown().optional(),
86
88
  /** Flat JSON-string output schema (`output_schema_json` on the proto). */
87
89
  outputSchemaJson: z.string().optional(),
90
+ /** Flat JSON-string MCP annotations (`annotations_json` on the proto). */
91
+ annotationsJson: z.string().optional(),
88
92
  });
89
93
 
90
94
  const getAvailableMcpServersResponseSchema = z.object({
@@ -154,6 +158,13 @@ export interface AccountMcpTool {
154
158
  * matching the optional MCP `Tool.outputSchema` field.
155
159
  */
156
160
  outputSchema?: Record<string, unknown>;
161
+ /**
162
+ * MCP spec tool annotations as the connector's server reported them,
163
+ * bounded to the spec fields at parse time. Absent stays absent —
164
+ * untrusted hints, and the fail-closed signal for consumers that classify
165
+ * on them (dry-run effect resolution).
166
+ */
167
+ annotations?: McpToolAnnotations;
157
168
  }
158
169
 
159
170
  export interface AccountMcpServer {
@@ -285,6 +296,12 @@ async function parseRpcResponse(
285
296
  export interface CursorBackendClientOptions {
286
297
  backendUrl: string;
287
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;
288
305
  logger?: (line: string) => void;
289
306
  /** Test seam; defaults to global fetch. */
290
307
  fetchImpl?: typeof fetch;
@@ -292,21 +309,26 @@ export interface CursorBackendClientOptions {
292
309
 
293
310
  export class CursorBackendClient {
294
311
  private readonly backendUrl: string;
295
- private readonly apiKey: string;
312
+ private readonly resolveApiKey: () => string;
296
313
  private readonly logger: (line: string) => void;
297
314
  private readonly fetchImpl: typeof fetch;
298
315
  private token: { value: string; expiresAtMs: number } | undefined;
299
316
  private tokenExchange: Promise<string> | undefined;
317
+ private lastExchangedApiKey: string | undefined;
300
318
 
301
319
  constructor(options: CursorBackendClientOptions) {
302
320
  this.backendUrl = options.backendUrl.endsWith("/")
303
321
  ? options.backendUrl.slice(0, -1)
304
322
  : options.backendUrl;
305
- this.apiKey = options.apiKey;
323
+ this.resolveApiKey = options.getApiKey ?? (() => options.apiKey);
306
324
  this.logger = options.logger ?? (() => {});
307
325
  this.fetchImpl = options.fetchImpl ?? fetch;
308
326
  }
309
327
 
328
+ private currentApiKey(): string {
329
+ return this.resolveApiKey().trim();
330
+ }
331
+
310
332
  /** Who this API key acts as. */
311
333
  async getMe(): Promise<CursorIdentity> {
312
334
  return getMeResponseSchema.parse(await this.rpc("GetMe", {}));
@@ -331,11 +353,13 @@ export class CursorBackendClient {
331
353
  status: server.status,
332
354
  tools: (server.tools ?? []).map((tool) => {
333
355
  const outputSchema = parseOutputSchemaJson(tool.outputSchemaJson);
356
+ const annotations = parseAnnotationsJson(tool.annotationsJson);
334
357
  return {
335
358
  name: tool.name,
336
359
  description: tool.description ?? "",
337
360
  inputSchema: normalizeInputSchema(tool.inputSchema),
338
361
  ...(outputSchema === undefined ? {} : { outputSchema }),
362
+ ...(annotations === undefined ? {} : { annotations }),
339
363
  };
340
364
  }),
341
365
  }));
@@ -430,7 +454,7 @@ export class CursorBackendClient {
430
454
  method: "POST",
431
455
  headers: {
432
456
  "content-type": "application/json",
433
- authorization: `Bearer ${this.apiKey}`,
457
+ authorization: `Bearer ${this.currentApiKey()}`,
434
458
  },
435
459
  body: JSON.stringify({ repos: [...repos], permissions }),
436
460
  signal: AbortSignal.timeout(30_000),
@@ -512,6 +536,18 @@ export class CursorBackendClient {
512
536
  * when none is cached or it is near expiry.
513
537
  */
514
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
+ }
515
551
  if (
516
552
  this.token !== undefined &&
517
553
  Date.now() < this.token.expiresAtMs - TOKEN_EXPIRY_LEEWAY_MS
@@ -528,13 +564,14 @@ export class CursorBackendClient {
528
564
  }
529
565
 
530
566
  private async exchangeApiKey(): Promise<string> {
567
+ const apiKey = this.currentApiKey();
531
568
  let response: Response;
532
569
  try {
533
570
  response = await this.fetchImpl(`${this.backendUrl}${EXCHANGE_PATH}`, {
534
571
  method: "POST",
535
572
  headers: {
536
573
  "content-type": "application/json",
537
- authorization: `Bearer ${this.apiKey}`,
574
+ authorization: `Bearer ${apiKey}`,
538
575
  },
539
576
  body: "{}",
540
577
  signal: AbortSignal.timeout(30_000),
@@ -563,6 +600,7 @@ export class CursorBackendClient {
563
600
  value: body.data.accessToken,
564
601
  expiresAtMs: jwtExpiryMs(body.data.accessToken),
565
602
  };
603
+ this.lastExchangedApiKey = apiKey;
566
604
  this.logger("[agent-sdk] cursor account: minted a fresh access token");
567
605
  return body.data.accessToken;
568
606
  }
@@ -707,6 +745,26 @@ function parseOutputSchemaJson(
707
745
  : undefined;
708
746
  }
709
747
 
748
+ /**
749
+ * Parse the flat JSON-string annotations off the wire and bound them to the
750
+ * MCP-spec fields. Malformed or non-object payloads behave like absent ones,
751
+ * so readers fail closed.
752
+ */
753
+ function parseAnnotationsJson(
754
+ annotationsJson: string | undefined
755
+ ): McpToolAnnotations | undefined {
756
+ if (annotationsJson === undefined) {
757
+ return undefined;
758
+ }
759
+ let parsed: unknown;
760
+ try {
761
+ parsed = JSON.parse(annotationsJson);
762
+ } catch {
763
+ return undefined;
764
+ }
765
+ return boundedToolAnnotations(parsed);
766
+ }
767
+
710
768
  type McpResultJson = z.infer<typeof mcpResultSchema>;
711
769
 
712
770
  function toCallResult(result: McpResultJson): AccountMcpCallResult {
@@ -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)
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Thin client for the Agent Serve deployment management API
3
- * (`/internal/agent-serve/deployments` on the Cursor backend). Pure
3
+ * (`/internal/agent-serve/deployments` on the Cursor backend) and the v2
4
+ * AgentSDK accept path (`/internal/agentkit/deployments`). Pure
4
5
  * request/response + error mapping — no console I/O, no polling loops;
5
6
  * rendering lives in `cli-deploy.ts`.
6
7
  *
@@ -212,6 +213,48 @@ export interface StopAccepted {
212
213
 
213
214
  export type DeleteAccepted = StopAccepted;
214
215
 
216
+ /**
217
+ * `POST /internal/agentkit/deployments` 202 body. Distinct from v1
218
+ * {@link DeployAccepted}: the v2 control plane returns catalog ids, not
219
+ * an engine alias or one-shot alias token.
220
+ */
221
+ export interface AgentkitDeployAccepted {
222
+ applicationId: string;
223
+ releaseId: string;
224
+ status: string;
225
+ }
226
+
227
+ /** Customer-visible v2 deploy status (`GET /internal/agentkit/deployments/:slug`). */
228
+ export type AgentkitCustomerStatus =
229
+ | "pending"
230
+ | "deploying"
231
+ | "running"
232
+ | "failed"
233
+ | "stopped";
234
+
235
+ export type AgentkitDeployProgress =
236
+ | "queued"
237
+ | "creating"
238
+ | "building"
239
+ | "starting"
240
+ | "finalizing"
241
+ | "running"
242
+ | "failed"
243
+ | "stopped";
244
+
245
+ export interface AgentkitDeployStatus {
246
+ applicationId: string;
247
+ releaseId: string;
248
+ slug: string;
249
+ teamId: number;
250
+ status: AgentkitCustomerStatus;
251
+ progress: AgentkitDeployProgress;
252
+ statusMessage: string | null;
253
+ }
254
+
255
+ export const TERMINAL_AGENTKIT_DEPLOY_STATUSES: ReadonlySet<AgentkitCustomerStatus> =
256
+ new Set(["running", "failed", "stopped"]);
257
+
215
258
  /** One secret's metadata from the list endpoint — names only, never values. */
216
259
  export interface SecretInfo {
217
260
  name: string;
@@ -260,6 +303,35 @@ const deployAcceptedSchema = z
260
303
  })
261
304
  .passthrough();
262
305
 
306
+ const agentkitDeployAcceptedSchema = z
307
+ .object({
308
+ applicationId: z.string().min(1),
309
+ releaseId: z.string().min(1),
310
+ status: z.string(),
311
+ })
312
+ .passthrough();
313
+
314
+ const agentkitDeployStatusSchema = z
315
+ .object({
316
+ applicationId: z.string().min(1),
317
+ releaseId: z.string().min(1),
318
+ slug: z.string().min(1),
319
+ teamId: z.number(),
320
+ status: z.enum(["pending", "deploying", "running", "failed", "stopped"]),
321
+ progress: z.enum([
322
+ "queued",
323
+ "creating",
324
+ "building",
325
+ "starting",
326
+ "finalizing",
327
+ "running",
328
+ "failed",
329
+ "stopped",
330
+ ]),
331
+ statusMessage: z.string().nullable(),
332
+ })
333
+ .passthrough();
334
+
263
335
  const deploymentSchema = z.object({
264
336
  id: z.number(),
265
337
  slug: z.string(),
@@ -482,6 +554,54 @@ export class AgentServeDeployClient {
482
554
  return deployAcceptedSchema.parse(raw);
483
555
  }
484
556
 
557
+ /**
558
+ * Create or redeploy onto the v2 control plane
559
+ * (`POST /internal/agentkit/deployments`). 202 means accepted; poll
560
+ * {@link getAgentkitDeploy} until a terminal status.
561
+ */
562
+ async deployAgentkit(args: {
563
+ teamId: number;
564
+ slug: string;
565
+ gitRepoUrl: string;
566
+ gitRef?: string;
567
+ agentPath?: string;
568
+ repositories?: string[];
569
+ }): Promise<AgentkitDeployAccepted> {
570
+ const slugError = validateDeploymentSlug(args.slug);
571
+ if (slugError !== undefined) {
572
+ throw new DeployApiError(slugError, 400);
573
+ }
574
+ const raw = await this.request({
575
+ method: "POST",
576
+ path: "/internal/agentkit/deployments",
577
+ body: {
578
+ teamId: args.teamId,
579
+ slug: args.slug,
580
+ gitRepoUrl: args.gitRepoUrl,
581
+ gitRef: args.gitRef,
582
+ agentPath: args.agentPath,
583
+ repositories:
584
+ args.repositories !== undefined && args.repositories.length > 0
585
+ ? args.repositories
586
+ : undefined,
587
+ },
588
+ context: { verb: "deploy", slug: args.slug },
589
+ });
590
+ return agentkitDeployAcceptedSchema.parse(raw);
591
+ }
592
+
593
+ async getAgentkitDeploy(args: {
594
+ teamId: number;
595
+ slug: string;
596
+ }): Promise<AgentkitDeployStatus> {
597
+ const raw = await this.request({
598
+ method: "GET",
599
+ path: `/internal/agentkit/deployments/${encodeURIComponent(args.slug)}?teamId=${args.teamId}`,
600
+ context: { verb: "get deployment", slug: args.slug },
601
+ });
602
+ return agentkitDeployStatusSchema.parse(raw);
603
+ }
604
+
485
605
  async listDeployments(args: { teamId: number }): Promise<Deployment[]> {
486
606
  const raw = await this.request({
487
607
  method: "GET",
@@ -756,7 +876,7 @@ function mapErrorResponse(
756
876
  // (api2 vs api.cursor.com), not a missing deployment / closed gate.
757
877
  if (/Route \S+ not found/i.test(bodyText)) {
758
878
  return new DeployApiError(
759
- `Agent Serve deploy API not found on this host — use the default api.cursor.com (unset CURSOR_API_BASE_URL), or point CURSOR_API_BASE_URL at a backend that mounts /internal/agent-serve/*.`,
879
+ `Agent Serve deploy API not found on this host — use the default api.cursor.com (unset CURSOR_API_BASE_URL), or point CURSOR_API_BASE_URL at a backend that mounts /internal/agent-serve/* and /internal/agentkit/*.`,
760
880
  404
761
881
  );
762
882
  }
@@ -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
  /**
@@ -318,8 +318,9 @@ function buildConnectionBridgeMcpServer(
318
318
  }
319
319
  // The one tool path that never reaches `executeServerTool`, so it asks
320
320
  // the policy itself.
321
- const answer = engine.bridgedCallAnswer({
321
+ const answer = await engine.bridgedCallAnswer({
322
322
  sessionId: sessionId ?? "",
323
+ connectionName,
323
324
  toolName: request.params.name,
324
325
  });
325
326
  if (answer.answered) {
@@ -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(