@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.
- package/dist/bin/agent-serve.js +0 -0
- package/dist/channels/slack/post-update-delivery.d.ts +85 -0
- package/dist/channels/slack/post-update-delivery.d.ts.map +1 -0
- package/dist/channels/slack/post-update-delivery.js +318 -0
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.BP8mAVMW.js → app.Ciosf24M.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.1FXIZbz8.js → VPLocalSearchBox.BcRDly7n.js} +1 -1
- package/dist/docs/assets/chunks/{theme.Dm1R02v9.js → theme.CMpbjdfx.js} +2 -2
- package/dist/docs/assets/{deployment.md.BtfEsc9S.js → deployment.md.DC5-ImcM.js} +1 -1
- package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.js → example-agents_approval-buddy.md.DFGBYLcc.js} +2 -2
- package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.lean.js → example-agents_approval-buddy.md.DFGBYLcc.lean.js} +1 -1
- package/dist/docs/assets/{example-agents_index.md.CwtWu5ix.js → example-agents_index.md.BPXjUtRB.js} +2 -2
- package/dist/docs/assets/{example-agents_index.md.CwtWu5ix.lean.js → example-agents_index.md.BPXjUtRB.lean.js} +1 -1
- package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.js → example-agents_security-reviewer.md.CE9v-aKm.js} +2 -2
- package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.lean.js → example-agents_security-reviewer.md.CE9v-aKm.lean.js} +1 -1
- package/dist/docs/assets/{storage.md.Cu3MwM2a.js → storage.md.DRTdnFvd.js} +7 -1
- package/dist/docs/assets/{storage.md.Cu3MwM2a.lean.js → storage.md.DRTdnFvd.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +5 -5
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +5 -5
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +5 -5
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +6 -6
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +11 -5
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +45 -18
- package/dist/internal/deploy-client.d.ts +7 -1
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +3 -0
- package/dist/internal/deploy-source.d.ts +3 -0
- package/dist/internal/deploy-source.d.ts.map +1 -1
- package/dist/internal/deploy-source.js +25 -7
- package/dist/internal/discovery.js +1 -1
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/hosting.d.ts +12 -2
- package/dist/internal/hosting.d.ts.map +1 -1
- package/dist/internal/hosting.js +21 -4
- package/dist/internal/json-dir-store.d.ts +32 -0
- package/dist/internal/json-dir-store.d.ts.map +1 -0
- package/dist/internal/json-dir-store.js +100 -0
- package/dist/internal/logs-client.d.ts +16 -2
- package/dist/internal/logs-client.d.ts.map +1 -1
- package/dist/internal/logs-client.js +51 -16
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +29 -15
- package/dist/internal/workspace.d.ts +19 -0
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +47 -13
- package/dist/playground/assets/index-C0-QEG4O.js +86 -0
- package/dist/playground/assets/index-z3JY9O2x.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +65 -0
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -0
- package/dist/storage-backends/cursor-hosted.js +203 -0
- package/dist/storage-backends/postgres-kv.d.ts +5 -0
- package/dist/storage-backends/postgres-kv.d.ts.map +1 -1
- package/dist/storage-backends/postgres-kv.js +5 -0
- package/docs/.vitepress/cache/deps/package.json +3 -0
- package/docs/deployment.md +7 -3
- package/docs/example-agents/approval-buddy.md +1 -1
- package/docs/example-agents/index.md +1 -1
- package/docs/example-agents/security-reviewer.md +5 -5
- package/docs/storage.md +18 -4
- package/package.json +8 -1
- package/src/internal/cli-deploy.ts +58 -20
- package/src/internal/deploy-client.ts +11 -0
- package/src/internal/deploy-source.ts +24 -6
- package/src/internal/discovery.ts +1 -1
- package/src/internal/distribution.ts +1 -0
- package/src/internal/hosting.ts +24 -4
- package/src/internal/logs-client.ts +67 -21
- package/src/internal/session-engine.ts +28 -11
- package/src/internal/workspace.ts +48 -11
- package/src/storage-backends/cursor-hosted.ts +246 -0
- package/src/storage-backends/postgres-kv.ts +5 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.DevOabKJ.js +0 -1
- package/dist/playground/assets/index-7GJIUFQj.css +0 -1
- package/dist/playground/assets/index-Bpr4Th4M.js +0 -319
- /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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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-
|
|
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
|
];
|
package/src/internal/hosting.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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" &&
|
|
283
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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 (
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
.
|
|
1158
|
-
|
|
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
|
|
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
|
+
}
|