@cursor/july 0.1.8 → 0.1.9

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 (119) hide show
  1. package/dist/bin/agent-serve.js +0 -0
  2. package/dist/channels/slack/post-update-delivery.d.ts +85 -0
  3. package/dist/channels/slack/post-update-delivery.d.ts.map +1 -0
  4. package/dist/channels/slack/post-update-delivery.js +318 -0
  5. package/dist/docs/404.html +2 -2
  6. package/dist/docs/ab.html +3 -3
  7. package/dist/docs/assets/{app.BP8mAVMW.js → app.Ciosf24M.js} +1 -1
  8. package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +1 -0
  9. package/dist/docs/assets/chunks/{VPLocalSearchBox.1FXIZbz8.js → VPLocalSearchBox.BcRDly7n.js} +1 -1
  10. package/dist/docs/assets/chunks/{theme.Dm1R02v9.js → theme.CMpbjdfx.js} +2 -2
  11. package/dist/docs/assets/{deployment.md.BtfEsc9S.js → deployment.md.DC5-ImcM.js} +1 -1
  12. package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.js → example-agents_approval-buddy.md.DFGBYLcc.js} +2 -2
  13. package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.lean.js → example-agents_approval-buddy.md.DFGBYLcc.lean.js} +1 -1
  14. package/dist/docs/assets/{example-agents_index.md.CwtWu5ix.js → example-agents_index.md.BPXjUtRB.js} +2 -2
  15. package/dist/docs/assets/{example-agents_index.md.CwtWu5ix.lean.js → example-agents_index.md.BPXjUtRB.lean.js} +1 -1
  16. package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.js → example-agents_security-reviewer.md.CE9v-aKm.js} +2 -2
  17. package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.lean.js → example-agents_security-reviewer.md.CE9v-aKm.lean.js} +1 -1
  18. package/dist/docs/assets/{storage.md.Cu3MwM2a.js → storage.md.DRTdnFvd.js} +7 -1
  19. package/dist/docs/assets/{storage.md.Cu3MwM2a.lean.js → storage.md.DRTdnFvd.lean.js} +1 -1
  20. package/dist/docs/building-with-agents.html +3 -3
  21. package/dist/docs/concepts.html +3 -3
  22. package/dist/docs/deployment.html +5 -5
  23. package/dist/docs/evals.html +3 -3
  24. package/dist/docs/example-agents/approval-buddy.html +5 -5
  25. package/dist/docs/example-agents/benny.html +3 -3
  26. package/dist/docs/example-agents/bugbot.html +3 -3
  27. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  28. package/dist/docs/example-agents/codeowners-review.html +3 -3
  29. package/dist/docs/example-agents/concierge.html +3 -3
  30. package/dist/docs/example-agents/fsd.html +3 -3
  31. package/dist/docs/example-agents/index.html +5 -5
  32. package/dist/docs/example-agents/knowledge-base.html +3 -3
  33. package/dist/docs/example-agents/oncall.html +3 -3
  34. package/dist/docs/example-agents/security-reviewer.html +6 -6
  35. package/dist/docs/example-agents/slack-agent.html +3 -3
  36. package/dist/docs/example-agents/weather-agent.html +3 -3
  37. package/dist/docs/guides/agent-to-agent.html +3 -3
  38. package/dist/docs/guides/cloud-runtime.html +3 -3
  39. package/dist/docs/guides/github.html +3 -3
  40. package/dist/docs/guides/human-in-the-loop.html +3 -3
  41. package/dist/docs/guides/slack.html +3 -3
  42. package/dist/docs/guides/webhooks.html +3 -3
  43. package/dist/docs/hashmap.json +1 -1
  44. package/dist/docs/hillclimbing.html +3 -3
  45. package/dist/docs/index.html +3 -3
  46. package/dist/docs/quickstart.html +3 -3
  47. package/dist/docs/reference/agent-config.html +3 -3
  48. package/dist/docs/reference/channels.html +3 -3
  49. package/dist/docs/reference/cli.html +3 -3
  50. package/dist/docs/reference/connections.html +3 -3
  51. package/dist/docs/reference/hooks.html +3 -3
  52. package/dist/docs/reference/http-api.html +3 -3
  53. package/dist/docs/reference/instructions.html +3 -3
  54. package/dist/docs/reference/playground.html +3 -3
  55. package/dist/docs/reference/project-layout.html +3 -3
  56. package/dist/docs/reference/schedules.html +3 -3
  57. package/dist/docs/reference/sessions.html +3 -3
  58. package/dist/docs/reference/skills.html +3 -3
  59. package/dist/docs/reference/subagents.html +3 -3
  60. package/dist/docs/reference/tools.html +3 -3
  61. package/dist/docs/scaffolding-agents.html +3 -3
  62. package/dist/docs/storage.html +11 -5
  63. package/dist/docs/troubleshooting.html +3 -3
  64. package/dist/internal/cli-deploy.d.ts.map +1 -1
  65. package/dist/internal/cli-deploy.js +45 -18
  66. package/dist/internal/deploy-client.d.ts +7 -1
  67. package/dist/internal/deploy-client.d.ts.map +1 -1
  68. package/dist/internal/deploy-client.js +3 -0
  69. package/dist/internal/deploy-source.d.ts +3 -0
  70. package/dist/internal/deploy-source.d.ts.map +1 -1
  71. package/dist/internal/deploy-source.js +25 -7
  72. package/dist/internal/discovery.js +1 -1
  73. package/dist/internal/distribution.d.ts.map +1 -1
  74. package/dist/internal/distribution.js +1 -0
  75. package/dist/internal/hosting.d.ts +12 -2
  76. package/dist/internal/hosting.d.ts.map +1 -1
  77. package/dist/internal/hosting.js +21 -4
  78. package/dist/internal/json-dir-store.d.ts +32 -0
  79. package/dist/internal/json-dir-store.d.ts.map +1 -0
  80. package/dist/internal/json-dir-store.js +100 -0
  81. package/dist/internal/logs-client.d.ts +16 -2
  82. package/dist/internal/logs-client.d.ts.map +1 -1
  83. package/dist/internal/logs-client.js +51 -16
  84. package/dist/internal/session-engine.d.ts.map +1 -1
  85. package/dist/internal/session-engine.js +29 -15
  86. package/dist/internal/workspace.d.ts +19 -0
  87. package/dist/internal/workspace.d.ts.map +1 -1
  88. package/dist/internal/workspace.js +47 -13
  89. package/dist/playground/assets/index-C0-QEG4O.js +86 -0
  90. package/dist/playground/assets/index-z3JY9O2x.css +1 -0
  91. package/dist/playground/index.html +2 -2
  92. package/dist/storage-backends/cursor-hosted.d.ts +65 -0
  93. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -0
  94. package/dist/storage-backends/cursor-hosted.js +203 -0
  95. package/dist/storage-backends/postgres-kv.d.ts +5 -0
  96. package/dist/storage-backends/postgres-kv.d.ts.map +1 -1
  97. package/dist/storage-backends/postgres-kv.js +5 -0
  98. package/docs/.vitepress/cache/deps/package.json +3 -0
  99. package/docs/deployment.md +7 -3
  100. package/docs/example-agents/approval-buddy.md +1 -1
  101. package/docs/example-agents/index.md +1 -1
  102. package/docs/example-agents/security-reviewer.md +5 -5
  103. package/docs/storage.md +18 -4
  104. package/package.json +8 -1
  105. package/src/internal/cli-deploy.ts +58 -20
  106. package/src/internal/deploy-client.ts +11 -0
  107. package/src/internal/deploy-source.ts +24 -6
  108. package/src/internal/discovery.ts +1 -1
  109. package/src/internal/distribution.ts +1 -0
  110. package/src/internal/hosting.ts +24 -4
  111. package/src/internal/logs-client.ts +67 -21
  112. package/src/internal/session-engine.ts +28 -11
  113. package/src/internal/workspace.ts +48 -11
  114. package/src/storage-backends/cursor-hosted.ts +246 -0
  115. package/src/storage-backends/postgres-kv.ts +5 -0
  116. package/dist/docs/assets/chunks/@localSearchIndexroot.DevOabKJ.js +0 -1
  117. package/dist/playground/assets/index-7GJIUFQj.css +0 -1
  118. package/dist/playground/assets/index-Bpr4Th4M.js +0 -319
  119. /package/dist/docs/assets/{deployment.md.BtfEsc9S.lean.js → deployment.md.DC5-ImcM.lean.js} +0 -0
@@ -39,17 +39,35 @@ const defaultGitRunner: GitRunner = async (args, cwd) => {
39
39
  * Normalize a git remote URL to the https form the control plane accepts
40
40
  * (`https://host/owner/repo`, no `.git` suffix). Returns undefined for
41
41
  * unrecognized shapes.
42
+ *
43
+ * Accepts HTTPS, `git@host:path`, GitHub org-scoped SSH
44
+ * (`org-123@github.com:owner/repo.git`), and `ssh://user@host[:port]/path`.
42
45
  */
43
46
  export function httpsGitRepoUrlFromRemote(remote: string): string | undefined {
44
47
  let url = remote.trim();
45
48
  if (url === "") {
46
49
  return undefined;
47
50
  }
48
- if (url.startsWith("git@")) {
49
- // git@host:owner/repo.git https://host/owner/repo.git
50
- url = `https://${url.slice("git@".length).replace(":", "/")}`;
51
- } else if (url.startsWith("ssh://git@")) {
52
- url = `https://${url.slice("ssh://git@".length)}`;
51
+
52
+ // SCP-like SSH: user@host:owner/repo(.git) includes git@ and GitHub
53
+ // org deploy keys (org-NNNN@github.com:…).
54
+ const scpMatch = /^([^@/\s]+)@([^:/\s]+):(.+)$/.exec(url);
55
+ if (scpMatch !== null) {
56
+ const host = scpMatch[2]!;
57
+ const path = scpMatch[3]!;
58
+ url = `https://${host}/${path}`;
59
+ } else if (/^ssh:\/\//i.test(url)) {
60
+ // ssh://user@host[:port]/owner/repo → https://host/owner/repo
61
+ try {
62
+ const ssh = new URL(url);
63
+ const path = ssh.pathname.replace(/^\/+/, "");
64
+ if (ssh.hostname === "" || path === "") {
65
+ return undefined;
66
+ }
67
+ url = `https://${ssh.hostname}/${path}`;
68
+ } catch {
69
+ return undefined;
70
+ }
53
71
  } else if (!/^https?:\/\//i.test(url)) {
54
72
  return undefined;
55
73
  }
@@ -67,7 +85,7 @@ export function httpsGitRepoUrlFromRemote(remote: string): string | undefined {
67
85
  if (path === "" || !path.includes("/")) {
68
86
  return undefined;
69
87
  }
70
- // Use hostname (not host) so ssh://git@github.com:22/… does not keep :22.
88
+ // Use hostname (not host) so ssh://…@github.com:22/… does not keep :22.
71
89
  return `https://${parsed.hostname}/${path}`;
72
90
  }
73
91
 
@@ -454,7 +454,7 @@ function resolveHosting(
454
454
  warn(`hosting.secretNames: ${nameError}`);
455
455
  } else if (isReservedSecretName(name)) {
456
456
  warn(
457
- `hosting.secretNames: ${JSON.stringify(name)} is platform-reserved (CURSOR_*) and injected automatically; it cannot be set with \`secrets set\`.`
457
+ `hosting.secretNames: ${JSON.stringify(name)} is platform-managed (CURSOR_* / Cursor-hosted storage) and injected automatically; omit it from secretNames.`
458
458
  );
459
459
  }
460
460
  }
@@ -105,4 +105,5 @@ export const AUTHORING_ENTRY_FILES: ReadonlyArray<
105
105
  ["storage", "storage.js"],
106
106
  ["storage/file-kv", "storage-backends/file-kv.js"],
107
107
  ["storage/postgres-kv", "storage-backends/postgres-kv.js"],
108
+ ["storage/cursor-hosted", "storage-backends/cursor-hosted.js"],
108
109
  ];
@@ -18,6 +18,17 @@ export const MAX_SECRET_NAME_LENGTH = 64;
18
18
  /** Secret names the platform injects; user writes are rejected. */
19
19
  export const RESERVED_SECRET_NAME_PREFIX = "CURSOR_";
20
20
 
21
+ /**
22
+ * Additional platform-managed env names for Cursor-hosted durable storage
23
+ * (Bugbot `agent_serve_*`). Mirrored from
24
+ * `@cursor/july/storage/cursor-hosted` so validate/secrets CLI stay in sync
25
+ * without importing the storage backend into the CLI graph.
26
+ */
27
+ export const PLATFORM_HOSTED_STORAGE_SECRET_NAMES = [
28
+ "BUGBOTDB_URL",
29
+ "AGENT_SERVE_DEPLOYMENT_ID",
30
+ ] as const;
31
+
21
32
  const SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
22
33
 
23
34
  // Lowercase hostname with >=2 labels, an alphabetic TLD, and at most one
@@ -49,14 +60,23 @@ export function validateSecretName(name: string): string | undefined {
49
60
  return undefined;
50
61
  }
51
62
 
52
- /** True when the platform reserves this name (`CURSOR_*`); writes are rejected. */
63
+ /**
64
+ * True when the platform manages this name (`CURSOR_*` or hosted-storage
65
+ * env vars); user writes / deletes are rejected.
66
+ */
53
67
  export function isReservedSecretName(name: string): boolean {
54
- return name.startsWith(RESERVED_SECRET_NAME_PREFIX);
68
+ if (name.startsWith(RESERVED_SECRET_NAME_PREFIX)) {
69
+ return true;
70
+ }
71
+ const upper = name.toUpperCase();
72
+ return (PLATFORM_HOSTED_STORAGE_SECRET_NAMES as readonly string[]).some(
73
+ (managed) => managed === upper
74
+ );
55
75
  }
56
76
 
57
77
  /**
58
78
  * Human error for a secret name a user is trying to write or delete:
59
- * grammar plus the reserved `CURSOR_*` prefix.
79
+ * grammar plus platform-managed names (`CURSOR_*`, hosted storage).
60
80
  */
61
81
  export function validateSettableSecretName(name: string): string | undefined {
62
82
  const nameError = validateSecretName(name);
@@ -64,7 +84,7 @@ export function validateSettableSecretName(name: string): string | undefined {
64
84
  return nameError;
65
85
  }
66
86
  if (isReservedSecretName(name)) {
67
- return `Secret name ${JSON.stringify(name)} is platform-reserved (CURSOR_*); the platform injects those values automatically.`;
87
+ return `Secret name ${JSON.stringify(name)} is platform-managed; the platform injects CURSOR_* and Cursor-hosted storage values automatically.`;
68
88
  }
69
89
  return undefined;
70
90
  }
@@ -35,7 +35,9 @@ export type LogsProgress =
35
35
  | DeploymentProgress
36
36
  | "queued"
37
37
  | "preparing"
38
+ | "creating"
38
39
  | "building"
40
+ | "starting"
39
41
  | "finalizing"
40
42
  | "running"
41
43
  | "failed"
@@ -46,7 +48,9 @@ export type LogsProgress =
46
48
  const PROGRESS_MESSAGES: Readonly<Record<LogsProgress, string>> = {
47
49
  queued: "Waiting to start…",
48
50
  preparing: "Preparing runtime…",
49
- building: "Building and starting agent…",
51
+ creating: "Creating runtime…",
52
+ building: "Checking out source and installing dependencies…",
53
+ starting: "Starting agent and waiting for health…",
50
54
  finalizing: "Finishing deployment…",
51
55
  running: "Deployment running",
52
56
  failed: "Deploy failed",
@@ -55,9 +59,15 @@ const PROGRESS_MESSAGES: Readonly<Record<LogsProgress, string>> = {
55
59
  degraded: "Deployment degraded",
56
60
  };
57
61
 
62
+ /**
63
+ * Customer-facing line for the current deploy step. Prefer the live
64
+ * `statusMessage` from the API when present; otherwise the progress
65
+ * vocabulary. Failed/degraded always append sanitized `lastError`.
66
+ */
58
67
  export function progressMessage(
59
68
  progress: LogsProgress,
60
- lastError?: string | null
69
+ lastError?: string | null,
70
+ statusMessage?: string | null
61
71
  ): string {
62
72
  if (progress === "failed") {
63
73
  const detail =
@@ -73,6 +83,13 @@ export function progressMessage(
73
83
  : `: ${sanitizeCustomerError(lastError)}`;
74
84
  return `Deployment degraded${detail}`;
75
85
  }
86
+ if (
87
+ statusMessage !== null &&
88
+ statusMessage !== undefined &&
89
+ statusMessage.trim() !== ""
90
+ ) {
91
+ return statusMessage.trim();
92
+ }
76
93
  return PROGRESS_MESSAGES[progress];
77
94
  }
78
95
 
@@ -85,7 +102,38 @@ export function sanitizeCustomerError(message: string): string {
85
102
  .replace(/\bcursorvm\.com\b/gi, "host")
86
103
  .replace(/\bx-anyrun-[a-z0-9-]+\b/gi, "header")
87
104
  .replace(/\bPROVISIONING\b/g, "building")
88
- .replace(/\bPLANNED\b/g, "preparing");
105
+ .replace(/\bPLANNED\b/g, "preparing")
106
+ .replace(/\bgitCloneCheckout\b/gi, "git clone")
107
+ .replace(/\bgitFetchCheckout\b/gi, "git fetch")
108
+ .replace(/\bgitCheckout:clone\b/gi, "checkout")
109
+ .replace(/\bgitCheckout:fetch\b/gi, "checkout");
110
+ }
111
+
112
+ /**
113
+ * One-line label for `agentkit deploy` polling (TTY spinner / non-TTY
114
+ * transitions). Uses statusMessage → progress → raw status.
115
+ */
116
+ export function deployProgressLabel(detail: {
117
+ status: string;
118
+ progress?: DeploymentProgress;
119
+ statusMessage?: string | null;
120
+ }): string {
121
+ if (
122
+ detail.statusMessage !== null &&
123
+ detail.statusMessage !== undefined &&
124
+ detail.statusMessage.trim() !== ""
125
+ ) {
126
+ return detail.statusMessage
127
+ .trim()
128
+ .replace(/…$/, "")
129
+ .replace(/\.\.\.$/, "");
130
+ }
131
+ if (detail.progress !== undefined) {
132
+ return PROGRESS_MESSAGES[detail.progress]
133
+ .replace(/…$/, "")
134
+ .replace(/\.\.\.$/, "");
135
+ }
136
+ return detail.status;
89
137
  }
90
138
 
91
139
  export async function fetchLogs(args: {
@@ -241,7 +289,7 @@ async function followProdLogs(args: {
241
289
  );
242
290
  });
243
291
 
244
- let lastProgress: string | undefined;
292
+ let lastEmittedKey: string | undefined;
245
293
  const slugHint = args.slug !== undefined && args.slug !== "" ? args.slug : "";
246
294
  if (args.once !== true) {
247
295
  const label = slugHint !== "" ? slugHint : "deployment";
@@ -260,33 +308,32 @@ async function followProdLogs(args: {
260
308
  }
261
309
 
262
310
  const progress = resolveProgress(detail);
311
+ const message = progressMessage(
312
+ progress,
313
+ detail.lastError,
314
+ detail.statusMessage
315
+ );
316
+ const emitKey = `${progress}\0${message}`;
263
317
 
264
318
  if (progress === "failed") {
265
- const message = progressMessage(progress, detail.lastError);
266
- if (progress !== lastProgress) {
267
- lastProgress = progress;
319
+ if (emitKey !== lastEmittedKey) {
320
+ lastEmittedKey = emitKey;
268
321
  emitProgress(args.write, args.json === true, progress, message);
269
322
  }
270
323
  throw new Error(`Deployment "${detail.slug}" failed.`);
271
324
  }
272
325
  if (progress === "stopped") {
273
- const message = progressMessage(progress, detail.lastError);
274
- if (progress !== lastProgress) {
275
- lastProgress = progress;
326
+ if (emitKey !== lastEmittedKey) {
327
+ lastEmittedKey = emitKey;
276
328
  emitProgress(args.write, args.json === true, progress, message);
277
329
  }
278
330
  throw new Error(`Deployment "${detail.slug}" is stopped.`);
279
331
  }
280
332
  if (progress === "running" || progress === "degraded") {
281
333
  // Hand off to runtime logs. Print degraded once; skip a "running" line.
282
- if (progress === "degraded" && progress !== lastProgress) {
283
- lastProgress = progress;
284
- emitProgress(
285
- args.write,
286
- args.json === true,
287
- progress,
288
- progressMessage(progress, detail.lastError)
289
- );
334
+ if (progress === "degraded" && emitKey !== lastEmittedKey) {
335
+ lastEmittedKey = emitKey;
336
+ emitProgress(args.write, args.json === true, progress, message);
290
337
  }
291
338
  const runtime = await resolveRuntime();
292
339
  await followRuntimeLogs({
@@ -306,9 +353,8 @@ async function followProdLogs(args: {
306
353
  return;
307
354
  }
308
355
 
309
- const message = progressMessage(progress, detail.lastError);
310
- if (progress !== lastProgress) {
311
- lastProgress = progress;
356
+ if (emitKey !== lastEmittedKey) {
357
+ lastEmittedKey = emitKey;
312
358
  emitProgress(args.write, args.json === true, progress, message);
313
359
  }
314
360
 
@@ -73,7 +73,6 @@ import {
73
73
  import { validateToolInput } from "./json-schema.js";
74
74
  import { McpHost } from "./mcp-host.js";
75
75
  import {
76
- buildIdentitySection,
77
76
  buildSessionContextBlock,
78
77
  SCHEDULE_CHANNEL_PREFIX,
79
78
  } from "./prompt-context.js";
@@ -83,7 +82,8 @@ import { SessionStore } from "./session-store.js";
83
82
  import { StorageCoordinator } from "./storage-coordinator.js";
84
83
  import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
85
84
  import {
86
- buildAgentToolsCatalog,
85
+ buildAgentsMdContent,
86
+ isNestedInGitRepo,
87
87
  materializeWorkspace,
88
88
  writeWorkspaceFiles,
89
89
  } from "./workspace.js";
@@ -1019,12 +1019,22 @@ export class SessionEngine {
1019
1019
  return rest;
1020
1020
  });
1021
1021
  }
1022
+ // The harness only loads the scaffolded AGENTS.md when the session
1023
+ // workspace is its own project root. With a per-session cwd override
1024
+ // (PR worktree) the scaffold lands elsewhere, and a workspace nested
1025
+ // inside a git repo re-roots harness rules at that repo — in both
1026
+ // cases deliver instructions inline on the first turn instead.
1027
+ const instructionsInvisibleToHarness =
1028
+ isFirstTurn &&
1029
+ (record.workspaceOverride !== undefined ||
1030
+ (await isNestedInGitRepo(harnessCwd)));
1022
1031
  const request = this.buildTurnRequest(
1023
1032
  record,
1024
1033
  agent,
1025
1034
  turnId,
1026
1035
  message,
1027
1036
  isFirstTurn,
1037
+ instructionsInvisibleToHarness,
1028
1038
  emit,
1029
1039
  async (update) => {
1030
1040
  mapper.handleUpdate(update);
@@ -1120,6 +1130,7 @@ export class SessionEngine {
1120
1130
  turnId: string,
1121
1131
  prompt: string,
1122
1132
  isFirstTurn: boolean,
1133
+ instructionsInvisibleToHarness: boolean,
1123
1134
  emit: (payload: SessionEventPayload) => void,
1124
1135
  onUpdate: RunnerTurnRequest["onUpdate"],
1125
1136
  signal: AbortSignal
@@ -1147,15 +1158,21 @@ export class SessionEngine {
1147
1158
  });
1148
1159
  turnPrompt = `${contextBlock}\n\n${prompt}`;
1149
1160
  }
1150
- if (runtime === "cloud" && isFirstTurn) {
1151
- const preamble = [
1152
- agent.instructions?.trim() ?? "",
1153
- buildIdentitySection(agent),
1154
- buildAgentToolsCatalog(agent.tools, { includeScripts: true }),
1155
- ]
1156
- .filter((part) => part !== "")
1157
- .join("\n\n");
1158
- turnPrompt = `${preamble}\n\n${turnPrompt}`;
1161
+ if (isFirstTurn) {
1162
+ if (runtime === "cloud") {
1163
+ // Cloud VMs have no materialized workspace: ship the full context,
1164
+ // including agent tool script bodies.
1165
+ const preamble = buildAgentsMdContent(agent, { includeScripts: true });
1166
+ turnPrompt = `${preamble}\n\n${turnPrompt}`;
1167
+ } else if (instructionsInvisibleToHarness) {
1168
+ // Local workspace whose AGENTS.md the harness will not load. With a
1169
+ // per-session harness cwd override, agent tool scripts live only in
1170
+ // the durable scaffold dir — embed bodies like cloud. Otherwise the
1171
+ // catalog can reference on-disk paths under the harness cwd.
1172
+ const includeScripts = record.workspaceOverride !== undefined;
1173
+ const preamble = buildAgentsMdContent(agent, { includeScripts });
1174
+ turnPrompt = `${preamble}\n\n${turnPrompt}`;
1175
+ }
1159
1176
  }
1160
1177
  return {
1161
1178
  sessionId: record.sessionId,
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { chmod, copyFile, mkdir, stat, writeFile } from "node:fs/promises";
11
- import { dirname, join, relative, resolve } from "node:path";
11
+ import { dirname, join, parse, relative, resolve } from "node:path";
12
12
  import type {
13
13
  DiscoveredSkill,
14
14
  DiscoveredTool,
@@ -123,22 +123,59 @@ export function buildAgentToolsCatalog(
123
123
  return `${parts.join("\n").trimEnd()}\n`;
124
124
  }
125
125
 
126
+ /**
127
+ * The full AGENTS.md-equivalent context for an agent: instructions, identity
128
+ * section, and the agent-side tool catalog. Written to the session workspace
129
+ * as `AGENTS.md`, and inlined into the first turn's prompt when the harness
130
+ * cannot discover that file (cloud runtime, or a local workspace the harness
131
+ * will not treat as the project root — see {@link isNestedInGitRepo}).
132
+ */
133
+ export function buildAgentsMdContent(
134
+ agent: ResolvedAgent,
135
+ options: { includeScripts: boolean }
136
+ ): string {
137
+ return [
138
+ agent.instructions?.trim() ?? "",
139
+ buildIdentitySection(agent),
140
+ buildAgentToolsCatalog(agent.tools, {
141
+ includeScripts: options.includeScripts,
142
+ }),
143
+ ]
144
+ .filter((part) => part !== "")
145
+ .join("\n\n");
146
+ }
147
+
148
+ /**
149
+ * True when `dir` sits strictly inside a git repository (a `.git` exists in
150
+ * an ancestor directory but not in `dir` itself). The Cursor harness resolves
151
+ * its project root by walking up to the nearest git root, so a nested session
152
+ * workspace loses its own `AGENTS.md` / `.cursor` scaffold: the enclosing
153
+ * repo's rules load instead, and the (necessarily gitignored) workspace files
154
+ * are skipped by nested rule discovery.
155
+ */
156
+ export async function isNestedInGitRepo(dir: string): Promise<boolean> {
157
+ let current = resolve(dir);
158
+ if (await exists(join(current, ".git"))) {
159
+ // The workspace is itself a repo root; the harness roots rules here.
160
+ return false;
161
+ }
162
+ const { root } = parse(current);
163
+ while (current !== root) {
164
+ current = dirname(current);
165
+ if (await exists(join(current, ".git"))) {
166
+ return true;
167
+ }
168
+ }
169
+ return false;
170
+ }
171
+
126
172
  export async function materializeWorkspace(
127
173
  options: MaterializeWorkspaceOptions
128
174
  ): Promise<void> {
129
175
  const { agent, workspaceDir, seed } = options;
130
176
  await mkdir(workspaceDir, { recursive: true });
131
177
 
132
- const agentToolsSection = buildAgentToolsCatalog(agent.tools, {
133
- includeScripts: false,
134
- });
135
- const agentsMd = [
136
- agent.instructions?.trim() ?? "",
137
- buildIdentitySection(agent),
138
- agentToolsSection,
139
- ]
140
- .filter((part) => part !== "")
141
- .join("\n\n");
178
+ const agentsMd = buildAgentsMdContent(agent, { includeScripts: false });
142
179
  await writeFile(join(workspaceDir, "AGENTS.md"), agentsMd, "utf8");
143
180
 
144
181
  for (const tool of agent.tools) {
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Cursor-managed durable storage for hosted agents.
3
+ *
4
+ * Talks to the control-plane storage proxy
5
+ * (`POST /internal/agent-serve/storage/*`) using the engine's pod
6
+ * `CURSOR_API_KEY`. The server resolves `deployment_id` from that service
7
+ * account — engines never receive a Bugbot DSN and cannot address another
8
+ * deployment's rows.
9
+ *
10
+ * On Cursor-managed hosting the start script pins `CURSOR_API_BASE_URL` to
11
+ * `https://api.cursor.com` (where the proxy is mounted). Locally, point
12
+ * `CURSOR_API_BASE_URL` at a backend that mounts Agent Serve routes and use
13
+ * a deployment SA key, or use `@cursor/july/storage/postgres-kv` /
14
+ * `file-kv` instead.
15
+ *
16
+ * ```ts
17
+ * import { defineStorage } from "@cursor/july/storage";
18
+ * import { cursorHostedStorage } from "@cursor/july/storage/cursor-hosted";
19
+ *
20
+ * export default defineStorage({
21
+ * ...cursorHostedStorage(),
22
+ * });
23
+ * ```
24
+ */
25
+
26
+ import type { ABSnapshot } from "../ab.js";
27
+ import type { EvalRunSnapshot } from "../evals.js";
28
+ import type {
29
+ StorageABTable,
30
+ StorageConfig,
31
+ StorageEntry,
32
+ StorageEvalsTable,
33
+ } from "../storage.js";
34
+ import type { JsonValue } from "../types.js";
35
+
36
+ /**
37
+ * Same default as {@link cursorExternalApiUrl} in the CLI credentials module,
38
+ * inlined here so this storage entrypoint does not import CLI/zod code.
39
+ * Hosted monorepo examples load this file from `packages/agent-serve/src`
40
+ * while `npm install` runs in the example package — a credentials import
41
+ * would resolve `zod` from the wrong `node_modules` tree and crash serve.
42
+ */
43
+ const DEFAULT_CURSOR_EXTERNAL_API_URL = "https://api.cursor.com";
44
+
45
+ /**
46
+ * Reserved so user `secrets set` cannot smuggle a Bugbot DSN into the pod.
47
+ * Not injected by Cursor hosting (storage is HTTP-proxied).
48
+ */
49
+ export const AGENT_SERVE_HOSTED_STORAGE_DATABASE_URL_ENV_VAR =
50
+ "BUGBOTDB_URL" as const;
51
+
52
+ /**
53
+ * Diagnostic / self-host deployment id. Not the hosted-storage authz
54
+ * boundary — the proxy scopes rows from the authenticated service account.
55
+ */
56
+ export const AGENT_SERVE_DEPLOYMENT_ID_ENV_VAR =
57
+ "AGENT_SERVE_DEPLOYMENT_ID" as const;
58
+
59
+ /**
60
+ * Platform-managed secret / env names. User `secrets set` / DELETE cannot
61
+ * claim these.
62
+ */
63
+ export const CURSOR_HOSTED_STORAGE_ENV_VARS: readonly [
64
+ typeof AGENT_SERVE_HOSTED_STORAGE_DATABASE_URL_ENV_VAR,
65
+ typeof AGENT_SERVE_DEPLOYMENT_ID_ENV_VAR,
66
+ ] = [
67
+ AGENT_SERVE_HOSTED_STORAGE_DATABASE_URL_ENV_VAR,
68
+ AGENT_SERVE_DEPLOYMENT_ID_ENV_VAR,
69
+ ];
70
+
71
+ export interface CursorHostedStorageOptions {
72
+ /**
73
+ * Control-plane base URL (default: `CURSOR_API_BASE_URL`, else
74
+ * `https://api.cursor.com`).
75
+ */
76
+ baseUrl?: string;
77
+ /** Bearer credential (default: `CURSOR_API_KEY`). */
78
+ apiKey?: string;
79
+ /** Injected for tests. */
80
+ fetch?: typeof fetch;
81
+ }
82
+
83
+ type CursorHostedStorageMethods = Required<
84
+ Pick<StorageConfig, "put" | "get" | "delete" | "list">
85
+ > & {
86
+ evals: StorageEvalsTable;
87
+ abs: StorageABTable;
88
+ };
89
+
90
+ function resolveApiKey(options: CursorHostedStorageOptions): string {
91
+ const key = (options.apiKey ?? process.env.CURSOR_API_KEY)?.trim();
92
+ if (key === undefined || key === "") {
93
+ throw new Error(
94
+ "cursorHostedStorage requires CURSOR_API_KEY (deployment pod credential on Cursor hosting)"
95
+ );
96
+ }
97
+ return key;
98
+ }
99
+
100
+ function resolveBaseUrl(options: CursorHostedStorageOptions): string {
101
+ const raw = (
102
+ options.baseUrl ??
103
+ process.env.CURSOR_API_BASE_URL ??
104
+ DEFAULT_CURSOR_EXTERNAL_API_URL
105
+ ).trim();
106
+ return raw.endsWith("/") ? raw.slice(0, -1) : raw;
107
+ }
108
+
109
+ async function storageRpc<T>(
110
+ options: CursorHostedStorageOptions,
111
+ path: string,
112
+ body: Record<string, unknown>
113
+ ): Promise<T> {
114
+ const baseUrl = resolveBaseUrl(options);
115
+ const apiKey = resolveApiKey(options);
116
+ const fetchImpl = options.fetch ?? globalThis.fetch;
117
+ if (typeof fetchImpl !== "function") {
118
+ throw new Error("cursorHostedStorage requires fetch");
119
+ }
120
+ const response = await fetchImpl(`${baseUrl}${path}`, {
121
+ method: "POST",
122
+ headers: {
123
+ Authorization: `Bearer ${apiKey}`,
124
+ "Content-Type": "application/json",
125
+ },
126
+ body: JSON.stringify(body),
127
+ });
128
+ const text = await response.text();
129
+ let parsed: unknown;
130
+ try {
131
+ parsed = text.length === 0 ? {} : JSON.parse(text);
132
+ } catch {
133
+ throw new Error(
134
+ `cursorHostedStorage ${path} returned non-JSON (${response.status})`
135
+ );
136
+ }
137
+ if (!response.ok) {
138
+ const error =
139
+ parsed !== null &&
140
+ typeof parsed === "object" &&
141
+ "error" in parsed &&
142
+ typeof (parsed as { error: unknown }).error === "string"
143
+ ? (parsed as { error: string }).error
144
+ : `HTTP ${response.status}`;
145
+ throw new Error(`cursorHostedStorage ${path}: ${error}`);
146
+ }
147
+ return parsed as T;
148
+ }
149
+
150
+ /**
151
+ * Cursor-hosted `defineStorage` methods (control-plane storage proxy →
152
+ * Bugbot `agent_serve_*` tables). Spread into `defineStorage({...})`.
153
+ */
154
+ export function cursorHostedStorage(
155
+ options: CursorHostedStorageOptions = {}
156
+ ): CursorHostedStorageMethods {
157
+ return {
158
+ async put(key, value): Promise<void> {
159
+ await storageRpc(options, "/internal/agent-serve/storage/kv/put", {
160
+ key,
161
+ value,
162
+ });
163
+ },
164
+ async get(key): Promise<JsonValue | undefined> {
165
+ const result = await storageRpc<{
166
+ found: boolean;
167
+ value: JsonValue | null;
168
+ }>(options, "/internal/agent-serve/storage/kv/get", { key });
169
+ if (!result.found) {
170
+ return undefined;
171
+ }
172
+ return result.value ?? undefined;
173
+ },
174
+ async delete(key): Promise<void> {
175
+ await storageRpc(options, "/internal/agent-serve/storage/kv/delete", {
176
+ key,
177
+ });
178
+ },
179
+ async list(prefix): Promise<StorageEntry[]> {
180
+ const result = await storageRpc<{
181
+ entries: Array<{ key: string; value: JsonValue }>;
182
+ }>(options, "/internal/agent-serve/storage/kv/list", { prefix });
183
+ return result.entries;
184
+ },
185
+ evals: {
186
+ async put(run, ctx): Promise<void> {
187
+ await storageRpc(options, "/internal/agent-serve/storage/evals/put", {
188
+ agentName: ctx.agentName,
189
+ run,
190
+ });
191
+ },
192
+ async delete(runId, ctx): Promise<void> {
193
+ await storageRpc(
194
+ options,
195
+ "/internal/agent-serve/storage/evals/delete",
196
+ {
197
+ agentName: ctx.agentName,
198
+ runId,
199
+ }
200
+ );
201
+ },
202
+ async list(ctx): Promise<EvalRunSnapshot[]> {
203
+ const result = await storageRpc<{ runs: EvalRunSnapshot[] }>(
204
+ options,
205
+ "/internal/agent-serve/storage/evals/list",
206
+ { agentName: ctx.agentName }
207
+ );
208
+ return result.runs;
209
+ },
210
+ },
211
+ abs: {
212
+ async putSample(sample, ctx): Promise<void> {
213
+ await storageRpc(
214
+ options,
215
+ "/internal/agent-serve/storage/abs/put-sample",
216
+ {
217
+ agentName: ctx.agentName,
218
+ sample,
219
+ }
220
+ );
221
+ },
222
+ async putSnapshot(snapshot, ctx): Promise<void> {
223
+ await storageRpc(
224
+ options,
225
+ "/internal/agent-serve/storage/abs/put-snapshot",
226
+ {
227
+ agentName: ctx.agentName,
228
+ snapshot,
229
+ }
230
+ );
231
+ },
232
+ async getSnapshot(ctx): Promise<ABSnapshot | undefined> {
233
+ const result = await storageRpc<{
234
+ found: boolean;
235
+ snapshot: ABSnapshot | null;
236
+ }>(options, "/internal/agent-serve/storage/abs/get-snapshot", {
237
+ agentName: ctx.agentName,
238
+ });
239
+ if (!result.found) {
240
+ return undefined;
241
+ }
242
+ return result.snapshot ?? undefined;
243
+ },
244
+ },
245
+ };
246
+ }