pi-cursor-sdk 0.1.56 → 0.1.57
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/CHANGELOG.md +45 -1
- package/README.md +100 -28
- package/docs/cursor-live-smoke-checklist.md +7 -7
- package/docs/cursor-model-ux-spec.md +45 -38
- package/docs/cursor-native-tool-replay.md +2 -2
- package/docs/cursor-native-tool-visual-audit.md +1 -1
- package/docs/cursor-testing-lessons.md +20 -7
- package/docs/cursor-tool-surfaces.md +13 -2
- package/docs/platform-smoke-implementation.md +220 -0
- package/docs/platform-smoke.md +183 -247
- package/package.json +38 -6
- package/platform-smoke.config.mjs +5 -1
- package/scripts/cloud-runtime-smoke.d.mts +3 -0
- package/scripts/cloud-runtime-smoke.mjs +502 -0
- package/scripts/debug-provider-events.mjs +7 -2
- package/scripts/isolated-cursor-smoke.sh +4 -6
- package/scripts/lib/cursor-child-process.d.mts +1 -0
- package/scripts/lib/cursor-child-process.mjs +137 -7
- package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
- package/scripts/local-resume-cleanup-smoke.mjs +108 -0
- package/scripts/local-resume-smoke.d.mts +1 -0
- package/scripts/local-resume-smoke.mjs +642 -0
- package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
- package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
- package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
- package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
- package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
- package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
- package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
- package/scripts/platform-smoke/artifacts.mjs +293 -65
- package/scripts/platform-smoke/card-detect.mjs +16 -4
- package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
- package/scripts/platform-smoke/doctor.mjs +20 -10
- package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
- package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
- package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
- package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
- package/scripts/platform-smoke/scenarios.mjs +16 -2
- package/scripts/platform-smoke/target-runtime.mjs +206 -0
- package/scripts/platform-smoke/targets.mjs +33 -141
- package/scripts/platform-smoke/visual-evidence.mjs +6 -7
- package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
- package/scripts/platform-smoke.mjs +40 -27
- package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
- package/scripts/steering-rpc-smoke.mjs +12 -2
- package/scripts/tmux-live-smoke.sh +3 -5
- package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
- package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
- package/shared/cursor-sensitive-text.mjs +7 -1
- package/src/context.ts +5 -2
- package/src/cursor-agents-context-registration.ts +7 -0
- package/src/cursor-agents-context.ts +3 -1
- package/src/cursor-api-key.ts +15 -1
- package/src/cursor-bridge-contract.ts +3 -0
- package/src/cursor-cloud-lifecycle.ts +733 -0
- package/src/cursor-cloud-options.ts +206 -0
- package/src/cursor-cloud-reporting.ts +246 -0
- package/src/cursor-config.ts +659 -0
- package/src/cursor-display-only-trace.ts +14 -0
- package/src/cursor-display-text.ts +8 -2
- package/src/cursor-durable-fs.ts +49 -0
- package/src/cursor-fallback-models.generated.ts +2045 -485
- package/src/cursor-live-run-accounting.ts +7 -1
- package/src/cursor-live-run-coordinator.ts +1 -0
- package/src/cursor-pi-tool-bridge-run.ts +14 -4
- package/src/cursor-provider-live-run-drain.ts +5 -0
- package/src/cursor-provider-run-finalizer.ts +36 -28
- package/src/cursor-provider-turn-finalize.ts +72 -6
- package/src/cursor-provider-turn-prepare.ts +228 -12
- package/src/cursor-provider-turn-runner.ts +41 -13
- package/src/cursor-provider-turn-send.ts +59 -16
- package/src/cursor-provider-turn-types.ts +44 -10
- package/src/cursor-runtime-state.ts +478 -0
- package/src/cursor-sdk-event-debug.ts +46 -6
- package/src/cursor-sdk-process-error-guard.ts +101 -30
- package/src/cursor-session-agent-cleanup.ts +328 -0
- package/src/cursor-session-agent-resume.ts +439 -0
- package/src/cursor-session-agent.ts +109 -13
- package/src/cursor-session-scope.ts +35 -2
- package/src/cursor-session-send-policy.ts +1 -1
- package/src/cursor-skill-tool.ts +30 -11
- package/src/cursor-state.ts +112 -69
- package/src/cursor-usage-accounting.ts +14 -4
- package/src/index.ts +11 -2
- package/src/model-discovery.ts +10 -56
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, existsSync, fchmodSync, fstatSync, fsyncSync, lstatSync, openSync, readFileSync, readSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { resolveCursorApiKey } from "./cursor-api-key.js";
|
|
6
|
+
import {
|
|
7
|
+
MAX_CLOUD_REPORT_BRANCHES,
|
|
8
|
+
type CursorCloudRunReport,
|
|
9
|
+
} from "./cursor-cloud-reporting.js";
|
|
10
|
+
import {
|
|
11
|
+
CLOUD_AGENT_ID_PATTERN,
|
|
12
|
+
CLOUD_LIFECYCLE_ENTRY_TYPE,
|
|
13
|
+
CLOUD_LIFECYCLE_JOURNAL_PREFIX,
|
|
14
|
+
} from "../shared/cursor-cloud-lifecycle-constants.mjs";
|
|
15
|
+
import { fsyncExistingRegularFile, noFollowFlag, openExistingRegularFileNoFollow } from "./cursor-durable-fs.js";
|
|
16
|
+
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
17
|
+
import { asRecord, getString } from "./cursor-record-utils.js";
|
|
18
|
+
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
19
|
+
import { loadCursorSdk } from "./cursor-sdk-runtime.js";
|
|
20
|
+
|
|
21
|
+
export { CLOUD_LIFECYCLE_ENTRY_TYPE };
|
|
22
|
+
|
|
23
|
+
const MAX_LEDGER_AGENT_ID_LENGTH = 256;
|
|
24
|
+
const MAX_LEDGER_RUN_ID_LENGTH = 256;
|
|
25
|
+
const MAX_LEDGER_TIMESTAMP_LENGTH = 64;
|
|
26
|
+
const MAX_LEDGER_BRANCH_LENGTH = 160;
|
|
27
|
+
const MAX_LEDGER_PR_URL_LENGTH = 240;
|
|
28
|
+
const MAX_LIST_RECORDS = 25;
|
|
29
|
+
const MAX_CLOUD_AGENT_ID_ERROR_LENGTH = 256;
|
|
30
|
+
const DURABLE_LEDGER_VERSION = 1;
|
|
31
|
+
const DURABLE_LEDGER_PREFIX = CLOUD_LIFECYCLE_JOURNAL_PREFIX;
|
|
32
|
+
|
|
33
|
+
type CloudLifecycleAction = "record" | "archive_intent" | "delete_intent" | "archive" | "delete";
|
|
34
|
+
|
|
35
|
+
interface CursorCloudLifecycleBranchEntry {
|
|
36
|
+
branch?: string;
|
|
37
|
+
prUrl?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CursorCloudLifecycleEntryData {
|
|
41
|
+
action: CloudLifecycleAction;
|
|
42
|
+
runtime: "cloud";
|
|
43
|
+
agentId: string;
|
|
44
|
+
runId?: string;
|
|
45
|
+
timestamp: string;
|
|
46
|
+
branches?: CursorCloudLifecycleBranchEntry[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CursorCloudLifecycleAgentRecord {
|
|
50
|
+
agentId: string;
|
|
51
|
+
runId?: string;
|
|
52
|
+
timestamp: string;
|
|
53
|
+
archived: boolean;
|
|
54
|
+
deleted: boolean;
|
|
55
|
+
branches: CursorCloudLifecycleBranchEntry[];
|
|
56
|
+
pendingAction?: "archive" | "delete";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type CloudLifecycleApi = Pick<ExtensionAPI, "appendEntry" | "on">;
|
|
60
|
+
type CloudLifecycleCommandContext = Pick<ExtensionCommandContext, "modelRegistry" | "sessionManager" | "ui">;
|
|
61
|
+
type CloudLifecycleSessionContext = Pick<ExtensionContext, "sessionManager">;
|
|
62
|
+
type CloudLifecycleSdkOperations = {
|
|
63
|
+
archive(agentId: string, options?: { apiKey?: string }): Promise<void>;
|
|
64
|
+
delete(agentId: string, options?: { apiKey?: string }): Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
interface DurableCloudLifecycleEntry extends CursorCloudLifecycleEntryData {
|
|
68
|
+
version: 1;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
sessionFile: string;
|
|
71
|
+
anchorEntryId: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ParsedDurableCloudLifecycleEntry {
|
|
75
|
+
data: CursorCloudLifecycleEntryData;
|
|
76
|
+
sessionFile: string;
|
|
77
|
+
anchorEntryId: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface CloudLifecycleSessionState {
|
|
81
|
+
sessionFile?: string;
|
|
82
|
+
sessionId?: string;
|
|
83
|
+
getBranch?: () => SessionEntry[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let cloudLifecycleApi: CloudLifecycleApi | undefined;
|
|
87
|
+
let cloudLifecycleSession: CloudLifecycleSessionState = {};
|
|
88
|
+
let durableWriterForTests: ((data: CursorCloudLifecycleEntryData) => boolean) | undefined;
|
|
89
|
+
let sessionFsyncForTests: (() => boolean) | undefined;
|
|
90
|
+
let runtimeApiKeyResolverForTests: (() => Promise<string | undefined>) | undefined;
|
|
91
|
+
let sdkOperationsForTests: CloudLifecycleSdkOperations | undefined;
|
|
92
|
+
|
|
93
|
+
function isCloudLifecycleAction(value: unknown): value is CloudLifecycleAction {
|
|
94
|
+
return value === "record" ||
|
|
95
|
+
value === "archive_intent" ||
|
|
96
|
+
value === "delete_intent" ||
|
|
97
|
+
value === "archive" ||
|
|
98
|
+
value === "delete";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sanitizeLedgerString(value: string, maxLength: number, apiKey?: string): string {
|
|
102
|
+
return truncateCursorDisplayLine(scrubSensitiveText(value, apiKey), maxLength);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isValidExactCloudAgentId(value: unknown): value is string {
|
|
106
|
+
return typeof value === "string" && CLOUD_AGENT_ID_PATTERN.test(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function requiredBoundedString(record: Record<string, unknown>, key: string, maxLength: number): string | undefined {
|
|
110
|
+
const value = record[key];
|
|
111
|
+
if (typeof value !== "string") return undefined;
|
|
112
|
+
const sanitized = sanitizeLedgerString(value, maxLength);
|
|
113
|
+
return sanitized || undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function optionalString(
|
|
117
|
+
record: Record<string, unknown>,
|
|
118
|
+
key: string,
|
|
119
|
+
maxLength: number,
|
|
120
|
+
apiKey?: string,
|
|
121
|
+
): string | undefined | false {
|
|
122
|
+
const value = record[key];
|
|
123
|
+
if (value === undefined) return undefined;
|
|
124
|
+
if (typeof value !== "string") return false;
|
|
125
|
+
return sanitizeLedgerString(value, maxLength, apiKey) || undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseBranch(value: unknown, apiKey?: string): CursorCloudLifecycleBranchEntry | undefined {
|
|
129
|
+
const record = asRecord(value);
|
|
130
|
+
if (!record) return undefined;
|
|
131
|
+
const branch = optionalString(record, "branch", MAX_LEDGER_BRANCH_LENGTH, apiKey);
|
|
132
|
+
const prUrl = optionalString(record, "prUrl", MAX_LEDGER_PR_URL_LENGTH, apiKey);
|
|
133
|
+
if (branch === false || prUrl === false || (!branch && !prUrl)) return undefined;
|
|
134
|
+
return {
|
|
135
|
+
...(branch ? { branch } : {}),
|
|
136
|
+
...(prUrl ? { prUrl } : {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseArray<T>(value: unknown, parseItem: (item: unknown) => T | undefined): T[] | undefined {
|
|
141
|
+
if (value === undefined) return undefined;
|
|
142
|
+
if (!Array.isArray(value)) return undefined;
|
|
143
|
+
const parsed = value.map(parseItem).filter((item): item is T => item !== undefined);
|
|
144
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseCloudLifecycleEntryData(value: unknown): CursorCloudLifecycleEntryData | undefined {
|
|
148
|
+
const record = asRecord(value);
|
|
149
|
+
if (!record || !isCloudLifecycleAction(record.action) || record.runtime !== "cloud") return undefined;
|
|
150
|
+
const agentId = record.agentId;
|
|
151
|
+
const timestamp = requiredBoundedString(record, "timestamp", MAX_LEDGER_TIMESTAMP_LENGTH);
|
|
152
|
+
if (!isValidExactCloudAgentId(agentId) || !timestamp) return undefined;
|
|
153
|
+
const runId = optionalString(record, "runId", MAX_LEDGER_RUN_ID_LENGTH);
|
|
154
|
+
if (runId === false) return undefined;
|
|
155
|
+
const branches = parseArray(record.branches, (branch) => parseBranch(branch))?.slice(0, MAX_CLOUD_REPORT_BRANCHES);
|
|
156
|
+
return {
|
|
157
|
+
action: record.action,
|
|
158
|
+
runtime: "cloud",
|
|
159
|
+
agentId,
|
|
160
|
+
timestamp,
|
|
161
|
+
...(runId ? { runId } : {}),
|
|
162
|
+
...(branches ? { branches } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildBranchEntries(report: Pick<CursorCloudRunReport, "branches">, apiKey?: string): CursorCloudLifecycleBranchEntry[] | undefined {
|
|
167
|
+
if (!Array.isArray(report.branches)) return undefined;
|
|
168
|
+
return parseArray(report.branches.slice(0, MAX_CLOUD_REPORT_BRANCHES), (branch) => parseBranch(branch, apiKey));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildBaseEntry(agentId: string, action: CloudLifecycleAction): CursorCloudLifecycleEntryData | undefined {
|
|
172
|
+
if (!isValidExactCloudAgentId(agentId)) return undefined;
|
|
173
|
+
return {
|
|
174
|
+
action,
|
|
175
|
+
runtime: "cloud",
|
|
176
|
+
agentId,
|
|
177
|
+
timestamp: new Date().toISOString(),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function captureCloudLifecycleSession(ctx: CloudLifecycleSessionContext): void {
|
|
182
|
+
cloudLifecycleSession = {
|
|
183
|
+
sessionFile: ctx.sessionManager.getSessionFile?.() ?? undefined,
|
|
184
|
+
sessionId: ctx.sessionManager.getSessionId?.() ?? undefined,
|
|
185
|
+
getBranch: () => ctx.sessionManager.getBranch(),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function durableLedgerPath(sessionFile: string, sessionId: string): string {
|
|
190
|
+
const sessionHash = createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
|
|
191
|
+
return join(dirname(sessionFile), `${DURABLE_LEDGER_PREFIX}-${sessionHash}.journal`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function sameFileIdentity(left: { dev: number; ino: number }, right: { dev: number; ino: number }): boolean {
|
|
195
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function createRegularFileExclusive(path: string, flags: number, mode: number): number {
|
|
199
|
+
let fd: number | undefined;
|
|
200
|
+
try {
|
|
201
|
+
fd = openSync(path, flags | constants.O_CREAT | constants.O_EXCL | noFollowFlag(), mode);
|
|
202
|
+
const opened = fstatSync(fd);
|
|
203
|
+
const after = lstatSync(path);
|
|
204
|
+
if (!opened.isFile() || !after.isFile() || !sameFileIdentity(opened, after)) {
|
|
205
|
+
throw new Error("created journal path changed while opening");
|
|
206
|
+
}
|
|
207
|
+
return fd;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (fd !== undefined) {
|
|
210
|
+
try { closeSync(fd); } catch {}
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function fsyncCloudLifecycleSessionFile(): boolean {
|
|
217
|
+
if (sessionFsyncForTests) return sessionFsyncForTests();
|
|
218
|
+
const sessionFile = cloudLifecycleSession.sessionFile;
|
|
219
|
+
if (!sessionFile || !existsSync(sessionFile)) return true;
|
|
220
|
+
return fsyncExistingRegularFile(sessionFile);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function appendDurableCloudLifecycleEntry(data: CursorCloudLifecycleEntryData, anchorEntryId: string): boolean {
|
|
224
|
+
if (durableWriterForTests) return durableWriterForTests(data);
|
|
225
|
+
const { sessionFile, sessionId } = cloudLifecycleSession;
|
|
226
|
+
if (!sessionFile || !sessionId) return false;
|
|
227
|
+
const entry: DurableCloudLifecycleEntry = {
|
|
228
|
+
...data,
|
|
229
|
+
version: DURABLE_LEDGER_VERSION,
|
|
230
|
+
sessionId,
|
|
231
|
+
sessionFile,
|
|
232
|
+
anchorEntryId: existsSync(sessionFile) ? anchorEntryId : null,
|
|
233
|
+
};
|
|
234
|
+
const path = durableLedgerPath(sessionFile, sessionId);
|
|
235
|
+
let created = false;
|
|
236
|
+
let fd: number | undefined;
|
|
237
|
+
let directoryFd: number | undefined;
|
|
238
|
+
try {
|
|
239
|
+
const flags = constants.O_RDWR | constants.O_APPEND;
|
|
240
|
+
try {
|
|
241
|
+
fd = createRegularFileExclusive(path, flags, 0o600);
|
|
242
|
+
created = true;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
245
|
+
fd = openExistingRegularFileNoFollow(path, flags);
|
|
246
|
+
}
|
|
247
|
+
if (process.platform !== "win32") fchmodSync(fd, 0o600);
|
|
248
|
+
const { size } = fstatSync(fd);
|
|
249
|
+
const lastByte = Buffer.allocUnsafe(1);
|
|
250
|
+
const needsFrameBoundary = size > 0 && readSync(fd, lastByte, 0, 1, size - 1) === 1 && lastByte[0] !== 0x0a;
|
|
251
|
+
writeFileSync(fd, `${needsFrameBoundary ? "\n" : ""}${JSON.stringify(entry)}\n`);
|
|
252
|
+
fsyncSync(fd);
|
|
253
|
+
if (created && process.platform !== "win32") {
|
|
254
|
+
directoryFd = openSync(dirname(path), "r");
|
|
255
|
+
fsyncSync(directoryFd);
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
} finally {
|
|
261
|
+
for (const openFd of [fd, directoryFd]) {
|
|
262
|
+
if (openFd === undefined) continue;
|
|
263
|
+
try {
|
|
264
|
+
closeSync(openFd);
|
|
265
|
+
} catch {
|
|
266
|
+
// fsync already established the durability decision.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function appendCloudLifecycleEntry(pi: CloudLifecycleApi, data: CursorCloudLifecycleEntryData | undefined): boolean {
|
|
273
|
+
if (!data || (!durableWriterForTests && (!cloudLifecycleSession.sessionFile || !cloudLifecycleSession.sessionId))) return false;
|
|
274
|
+
let anchorEntryId: string | undefined;
|
|
275
|
+
try {
|
|
276
|
+
const previousEntryId = durableWriterForTests ? undefined : cloudLifecycleSession.getBranch?.().at(-1)?.id;
|
|
277
|
+
pi.appendEntry<CursorCloudLifecycleEntryData>(CLOUD_LIFECYCLE_ENTRY_TYPE, data);
|
|
278
|
+
const anchor = cloudLifecycleSession.getBranch?.().at(-1);
|
|
279
|
+
anchorEntryId = durableWriterForTests
|
|
280
|
+
? "test-cloud-lifecycle-entry"
|
|
281
|
+
: anchor?.type === "custom" &&
|
|
282
|
+
anchor.customType === CLOUD_LIFECYCLE_ENTRY_TYPE &&
|
|
283
|
+
anchor.id !== previousEntryId
|
|
284
|
+
? anchor.id
|
|
285
|
+
: undefined;
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
return anchorEntryId !== undefined &&
|
|
290
|
+
fsyncCloudLifecycleSessionFile() &&
|
|
291
|
+
appendDurableCloudLifecycleEntry(data, anchorEntryId);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function appendCloudLifecycleMutationEntry(
|
|
295
|
+
pi: CloudLifecycleApi,
|
|
296
|
+
ctx: CloudLifecycleCommandContext,
|
|
297
|
+
data: CursorCloudLifecycleEntryData | undefined,
|
|
298
|
+
): boolean {
|
|
299
|
+
const anchorEntryId = ctx.sessionManager.getBranch().at(-1)?.id;
|
|
300
|
+
if (!data || !anchorEntryId || !fsyncCloudLifecycleSessionFile() || !appendDurableCloudLifecycleEntry(data, anchorEntryId)) return false;
|
|
301
|
+
try {
|
|
302
|
+
pi.appendEntry<CursorCloudLifecycleEntryData>(CLOUD_LIFECYCLE_ENTRY_TYPE, data);
|
|
303
|
+
} catch {
|
|
304
|
+
// The fsynced mutation journal is authoritative.
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function registerCursorCloudLifecycleLedger(pi: CloudLifecycleApi): void {
|
|
310
|
+
cloudLifecycleApi = pi;
|
|
311
|
+
pi.on("session_start", (_event, ctx) => captureCloudLifecycleSession(ctx));
|
|
312
|
+
pi.on("before_agent_start", (_event, ctx) => captureCloudLifecycleSession(ctx));
|
|
313
|
+
pi.on("session_tree", (_event, ctx) => captureCloudLifecycleSession(ctx));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function recordCursorCloudLifecycleRun(
|
|
317
|
+
report: Omit<CursorCloudRunReport, "runId"> & { runId?: string },
|
|
318
|
+
options: { apiKey?: string } = {},
|
|
319
|
+
): boolean {
|
|
320
|
+
if (!cloudLifecycleApi) return false;
|
|
321
|
+
const baseEntry = buildBaseEntry(report.agentId, "record");
|
|
322
|
+
if (!baseEntry) return false;
|
|
323
|
+
const branches = buildBranchEntries(report, options.apiKey);
|
|
324
|
+
const runId = typeof report.runId === "string"
|
|
325
|
+
? sanitizeLedgerString(report.runId, MAX_LEDGER_RUN_ID_LENGTH, options.apiKey)
|
|
326
|
+
: "";
|
|
327
|
+
return appendCloudLifecycleEntry(cloudLifecycleApi, {
|
|
328
|
+
...baseEntry,
|
|
329
|
+
...(runId ? { runId } : {}),
|
|
330
|
+
...(branches ? { branches } : {}),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function recordCursorCloudLifecycleSafely(
|
|
335
|
+
report: { agentId: string; runId?: string },
|
|
336
|
+
apiKey: string | undefined,
|
|
337
|
+
): boolean {
|
|
338
|
+
try {
|
|
339
|
+
return recordCursorCloudLifecycleRun({ ...report, branches: [] }, { apiKey });
|
|
340
|
+
} catch {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function createCursorCloudLifecyclePersistenceError(
|
|
346
|
+
agentId: string,
|
|
347
|
+
phase: "intent" | "run",
|
|
348
|
+
cancellationConfirmed: boolean | undefined,
|
|
349
|
+
apiKey: string | undefined,
|
|
350
|
+
): Error {
|
|
351
|
+
const boundedAgentId = truncateCursorDisplayLine(scrubSensitiveText(agentId, apiKey), MAX_CLOUD_AGENT_ID_ERROR_LENGTH);
|
|
352
|
+
const cancellation = phase === "intent"
|
|
353
|
+
? "No run was started."
|
|
354
|
+
: cancellationConfirmed
|
|
355
|
+
? "Cancellation requested/confirmed."
|
|
356
|
+
: "Cancellation requested but unconfirmed.";
|
|
357
|
+
return new Error(scrubSensitiveText(
|
|
358
|
+
`Cursor Cloud ${phase === "intent" ? "send intent" : "run"} blocked because its lifecycle record could not be persisted for agent ${boundedAgentId}. Cloud requires a writable persisted pi session; do not use --no-session. ${cancellation} Open the Cursor Cloud dashboard and manually archive or delete agent ${boundedAgentId} before retrying.`,
|
|
359
|
+
apiKey,
|
|
360
|
+
));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function compareLifecycleTimestamps(left: string, right: string): number {
|
|
364
|
+
const leftTime = Date.parse(left);
|
|
365
|
+
const rightTime = Date.parse(right);
|
|
366
|
+
if (Number.isFinite(leftTime) && Number.isFinite(rightTime)) return leftTime - rightTime;
|
|
367
|
+
return left.localeCompare(right);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function pickNewerRunId(newer: boolean, dataRunId: string | undefined, existingRunId: string | undefined): string | undefined {
|
|
371
|
+
return newer ? dataRunId ?? existingRunId : existingRunId ?? dataRunId;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function mergeCloudLifecycleRecordBranches(
|
|
375
|
+
newer: boolean,
|
|
376
|
+
dataBranches: CursorCloudLifecycleBranchEntry[] | undefined,
|
|
377
|
+
existingBranches: CursorCloudLifecycleBranchEntry[] | undefined,
|
|
378
|
+
): CursorCloudLifecycleBranchEntry[] {
|
|
379
|
+
if (newer) return dataBranches ?? existingBranches ?? [];
|
|
380
|
+
return existingBranches?.length ? existingBranches : dataBranches ?? [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function reduceCloudLifecycleEntries(entries: readonly CursorCloudLifecycleEntryData[]): CursorCloudLifecycleAgentRecord[] {
|
|
384
|
+
type MutationState = { status: "intent" | "result"; timestamp: string };
|
|
385
|
+
const agents = new Map<string, CursorCloudLifecycleAgentRecord>();
|
|
386
|
+
const mutations = new Map<string, { archive?: MutationState; delete?: MutationState }>();
|
|
387
|
+
for (const data of entries) {
|
|
388
|
+
if (data.action === "record") {
|
|
389
|
+
const existing = agents.get(data.agentId);
|
|
390
|
+
const newer = !existing || compareLifecycleTimestamps(data.timestamp, existing.timestamp) >= 0;
|
|
391
|
+
agents.set(data.agentId, {
|
|
392
|
+
agentId: data.agentId,
|
|
393
|
+
runId: pickNewerRunId(newer, data.runId, existing?.runId),
|
|
394
|
+
timestamp: newer || !existing ? data.timestamp : existing.timestamp,
|
|
395
|
+
archived: false,
|
|
396
|
+
deleted: false,
|
|
397
|
+
branches: mergeCloudLifecycleRecordBranches(newer, data.branches, existing?.branches),
|
|
398
|
+
});
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const action = data.action.startsWith("archive") ? "archive" : "delete";
|
|
402
|
+
const status = data.action.endsWith("_intent") ? "intent" : "result";
|
|
403
|
+
const agentMutations = mutations.get(data.agentId) ?? {};
|
|
404
|
+
const current = agentMutations[action];
|
|
405
|
+
const comparison = current ? compareLifecycleTimestamps(data.timestamp, current.timestamp) : 1;
|
|
406
|
+
if (comparison > 0 || (comparison === 0 && status === "result" && current?.status === "intent")) {
|
|
407
|
+
agentMutations[action] = { status, timestamp: data.timestamp };
|
|
408
|
+
mutations.set(data.agentId, agentMutations);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
for (const [agentId, agent] of agents) {
|
|
412
|
+
const agentMutations = mutations.get(agentId);
|
|
413
|
+
const archive = agentMutations?.archive;
|
|
414
|
+
const deletion = agentMutations?.delete;
|
|
415
|
+
const pending = [
|
|
416
|
+
...(archive?.status === "intent" ? [{ action: "archive" as const, timestamp: archive.timestamp }] : []),
|
|
417
|
+
...(deletion?.status === "intent" ? [{ action: "delete" as const, timestamp: deletion.timestamp }] : []),
|
|
418
|
+
].sort((left, right) => compareLifecycleTimestamps(right.timestamp, left.timestamp))[0];
|
|
419
|
+
const mutationTimestamps = [archive?.timestamp, deletion?.timestamp].filter((value): value is string => value !== undefined);
|
|
420
|
+
const timestamp = mutationTimestamps.reduce(
|
|
421
|
+
(latest, value) => compareLifecycleTimestamps(value, latest) > 0 ? value : latest,
|
|
422
|
+
agent.timestamp,
|
|
423
|
+
);
|
|
424
|
+
agents.set(agentId, {
|
|
425
|
+
...agent,
|
|
426
|
+
timestamp,
|
|
427
|
+
archived: archive?.status === "result",
|
|
428
|
+
deleted: deletion?.status === "result",
|
|
429
|
+
...(pending ? { pendingAction: pending.action } : {}),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return [...agents.values()].filter((agent) => !agent.deleted);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function readCursorCloudLifecycleEntries(entries: readonly SessionEntry[]): CursorCloudLifecycleEntryData[] {
|
|
436
|
+
return entries.flatMap((entry) => {
|
|
437
|
+
if (entry.type !== "custom" || entry.customType !== CLOUD_LIFECYCLE_ENTRY_TYPE) return [];
|
|
438
|
+
const data = parseCloudLifecycleEntryData(entry.data);
|
|
439
|
+
return data ? [data] : [];
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function readCursorCloudLifecycleAgents(entries: readonly SessionEntry[]): CursorCloudLifecycleAgentRecord[] {
|
|
444
|
+
return reduceCloudLifecycleEntries(readCursorCloudLifecycleEntries(entries));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function readDurableCloudLifecycleJournal(ctx: CloudLifecycleCommandContext): ParsedDurableCloudLifecycleEntry[] {
|
|
448
|
+
const sessionFile = ctx.sessionManager.getSessionFile?.();
|
|
449
|
+
const sessionId = ctx.sessionManager.getSessionId?.();
|
|
450
|
+
if (!sessionFile || !sessionId) return [];
|
|
451
|
+
let fd: number | undefined;
|
|
452
|
+
let lines: string[];
|
|
453
|
+
try {
|
|
454
|
+
fd = openExistingRegularFileNoFollow(durableLedgerPath(sessionFile, sessionId), constants.O_RDONLY);
|
|
455
|
+
lines = readFileSync(fd, "utf8").split(/\r?\n/);
|
|
456
|
+
} catch {
|
|
457
|
+
return [];
|
|
458
|
+
} finally {
|
|
459
|
+
if (fd !== undefined) {
|
|
460
|
+
try { closeSync(fd); } catch { return []; }
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const entries: ParsedDurableCloudLifecycleEntry[] = [];
|
|
464
|
+
for (const line of lines) {
|
|
465
|
+
if (!line) continue;
|
|
466
|
+
let raw: Record<string, unknown> | undefined;
|
|
467
|
+
try {
|
|
468
|
+
raw = asRecord(JSON.parse(line));
|
|
469
|
+
} catch {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (raw?.version !== DURABLE_LEDGER_VERSION || raw.sessionId !== sessionId || typeof raw.sessionFile !== "string") continue;
|
|
473
|
+
const anchorEntryId = raw.anchorEntryId;
|
|
474
|
+
if (anchorEntryId !== null && typeof anchorEntryId !== "string") continue;
|
|
475
|
+
const data = parseCloudLifecycleEntryData(raw);
|
|
476
|
+
if (data) entries.push({ data, sessionFile: raw.sessionFile, anchorEntryId });
|
|
477
|
+
}
|
|
478
|
+
return entries;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function readDurableCloudLifecycleEntries(ctx: CloudLifecycleCommandContext): CursorCloudLifecycleEntryData[] {
|
|
482
|
+
const sessionFile = ctx.sessionManager.getSessionFile?.();
|
|
483
|
+
if (!sessionFile) return [];
|
|
484
|
+
const fileless = !existsSync(sessionFile);
|
|
485
|
+
const branchIds = new Set(ctx.sessionManager.getBranch().map((entry) => entry.id));
|
|
486
|
+
return readDurableCloudLifecycleJournal(ctx).flatMap((entry) => {
|
|
487
|
+
if (entry.data.action !== "record") return [entry.data];
|
|
488
|
+
if (fileless) return entry.anchorEntryId === null ? [entry.data] : [];
|
|
489
|
+
return entry.anchorEntryId !== null && branchIds.has(entry.anchorEntryId) ? [entry.data] : [];
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function cloudLifecycleEntryKey(data: CursorCloudLifecycleEntryData): string {
|
|
494
|
+
return JSON.stringify([data.action, data.agentId, data.runId, data.timestamp, data.branches]);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function reconcileDurableCloudLifecycleOrphans(pi: CloudLifecycleApi, ctx: CloudLifecycleCommandContext): boolean {
|
|
498
|
+
const sessionFile = ctx.sessionManager.getSessionFile?.();
|
|
499
|
+
if (!sessionFile || !existsSync(sessionFile)) return true;
|
|
500
|
+
const journal = readDurableCloudLifecycleJournal(ctx);
|
|
501
|
+
const claimedAgentIds = new Set(journal.flatMap((entry) =>
|
|
502
|
+
entry.data.action === "record" && entry.anchorEntryId !== null ? [entry.data.agentId] : []));
|
|
503
|
+
const latestOrphanByAgent = new Map<string, ParsedDurableCloudLifecycleEntry>();
|
|
504
|
+
for (const entry of journal) {
|
|
505
|
+
if (entry.data.action === "record" && entry.anchorEntryId === null) latestOrphanByAgent.set(entry.data.agentId, entry);
|
|
506
|
+
}
|
|
507
|
+
const branch = ctx.sessionManager.getBranch();
|
|
508
|
+
for (const [agentId, orphan] of latestOrphanByAgent) {
|
|
509
|
+
if (claimedAgentIds.has(agentId)) continue;
|
|
510
|
+
let matchingEntry: SessionEntry | undefined;
|
|
511
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
512
|
+
const entry = branch[index];
|
|
513
|
+
if (entry.type !== "custom" || entry.customType !== CLOUD_LIFECYCLE_ENTRY_TYPE) continue;
|
|
514
|
+
const data = parseCloudLifecycleEntryData(entry.data);
|
|
515
|
+
if (data !== undefined && cloudLifecycleEntryKey(data) === cloudLifecycleEntryKey(orphan.data)) {
|
|
516
|
+
matchingEntry = entry;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (matchingEntry) {
|
|
521
|
+
if (!fsyncCloudLifecycleSessionFile() || !appendDurableCloudLifecycleEntry(orphan.data, matchingEntry.id)) return false;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (orphan.sessionFile !== sessionFile && !appendCloudLifecycleEntry(pi, orphan.data)) return false;
|
|
525
|
+
}
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function reconcileDurableCloudLifecycleOrphansForCommand(pi: CloudLifecycleApi, ctx: CloudLifecycleCommandContext): boolean {
|
|
530
|
+
if (reconcileDurableCloudLifecycleOrphans(pi, ctx)) return true;
|
|
531
|
+
ctx.ui.notify("Unable to reconcile the durable Cursor cloud lifecycle journal with this session branch.", "error");
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function readRecordedCloudAgents(ctx: CloudLifecycleCommandContext): CursorCloudLifecycleAgentRecord[] {
|
|
536
|
+
const durableEntries = readDurableCloudLifecycleEntries(ctx);
|
|
537
|
+
const durableKeys = new Set(durableEntries.map(cloudLifecycleEntryKey));
|
|
538
|
+
const branchOnlyEntries = readCursorCloudLifecycleEntries(ctx.sessionManager.getBranch())
|
|
539
|
+
.filter((entry) => !durableKeys.has(cloudLifecycleEntryKey(entry)));
|
|
540
|
+
return reduceCloudLifecycleEntries([...durableEntries, ...branchOnlyEntries]);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function validateCloudAgentIdSyntax(agentId: string): string | undefined {
|
|
544
|
+
if (!agentId) return "Missing Cursor cloud agent ID.";
|
|
545
|
+
if (!agentId.startsWith("bc-")) return "Cursor cloud agent IDs must start with bc-.";
|
|
546
|
+
if (agentId.length > MAX_LEDGER_AGENT_ID_LENGTH) return "Cursor cloud agent ID is too long.";
|
|
547
|
+
if (!isValidExactCloudAgentId(agentId)) return "Pass exactly one valid recorded Cursor cloud agent ID.";
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function validateRecordedCloudAgentId(
|
|
552
|
+
agentId: string,
|
|
553
|
+
records: readonly CursorCloudLifecycleAgentRecord[],
|
|
554
|
+
action?: "archive" | "delete",
|
|
555
|
+
): string | undefined {
|
|
556
|
+
const syntaxError = validateCloudAgentIdSyntax(agentId);
|
|
557
|
+
if (syntaxError) return syntaxError;
|
|
558
|
+
const record = records.find((candidate) => candidate.agentId === agentId);
|
|
559
|
+
if (!record) return `Cursor cloud agent ${agentId} is not recorded in this session branch.`;
|
|
560
|
+
if (record.pendingAction) {
|
|
561
|
+
return `Cursor cloud agent ${agentId} has an unresolved ${record.pendingAction} request; inspect it in the Cursor Cloud dashboard before retrying.`;
|
|
562
|
+
}
|
|
563
|
+
if (action === "archive" && record.archived) return `Cursor cloud agent ${agentId} is already archived.`;
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function tokenizeArgs(args: string): string[] {
|
|
568
|
+
return args.trim().split(/\s+/).filter(Boolean);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function formatCloudAgentRecord(record: CursorCloudLifecycleAgentRecord): string {
|
|
572
|
+
const agentId = sanitizeLedgerString(record.agentId, MAX_LEDGER_AGENT_ID_LENGTH);
|
|
573
|
+
const status = [record.archived ? "archived" : undefined, record.pendingAction ? `${record.pendingAction} pending` : undefined]
|
|
574
|
+
.filter(Boolean)
|
|
575
|
+
.join(", ");
|
|
576
|
+
const parts = [`- ${agentId}${status ? ` (${status})` : ""}`];
|
|
577
|
+
if (record.runId) parts.push(`run ${sanitizeLedgerString(record.runId, MAX_LEDGER_RUN_ID_LENGTH)}`);
|
|
578
|
+
const branch = record.branches.find((candidate) => candidate.branch || candidate.prUrl);
|
|
579
|
+
if (branch?.branch) parts.push(`branch ${sanitizeLedgerString(branch.branch, MAX_LEDGER_BRANCH_LENGTH)}`);
|
|
580
|
+
if (branch?.prUrl) parts.push(`PR ${sanitizeLedgerString(branch.prUrl, MAX_LEDGER_PR_URL_LENGTH)}`);
|
|
581
|
+
return parts.join(" · ");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function formatCloudLifecycleRecords(records: readonly CursorCloudLifecycleAgentRecord[]): string {
|
|
585
|
+
if (records.length === 0) return "No recorded Cursor cloud agents for this session branch.";
|
|
586
|
+
const visibleRecords = records.slice(0, MAX_LIST_RECORDS);
|
|
587
|
+
const lines = ["Recorded Cursor cloud agents for this session branch:", ...visibleRecords.map(formatCloudAgentRecord)];
|
|
588
|
+
if (records.length > MAX_LIST_RECORDS) lines.push(`- +${records.length - MAX_LIST_RECORDS} more recorded agents`);
|
|
589
|
+
return lines.join("\n");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function formatCursorCloudLifecycleList(entries: readonly SessionEntry[]): string {
|
|
593
|
+
return formatCloudLifecycleRecords(readCursorCloudLifecycleAgents(entries));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function getSdkOperations(): Promise<CloudLifecycleSdkOperations> {
|
|
597
|
+
if (sdkOperationsForTests) return sdkOperationsForTests;
|
|
598
|
+
const { Agent } = await loadCursorSdk();
|
|
599
|
+
return {
|
|
600
|
+
archive: (agentId, options) => Agent.archive(agentId, options),
|
|
601
|
+
delete: (agentId, options) => Agent.delete(agentId, options),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function formatCloudLifecycleError(error: unknown, apiKey: string | undefined): string {
|
|
606
|
+
return truncateCursorDisplayLine(scrubSensitiveText(getString(asRecord(error), "message") ?? String(error), apiKey));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function resolveCloudLifecycleMutationApiKey(ctx: CloudLifecycleCommandContext): Promise<string | undefined> {
|
|
610
|
+
const apiKey = resolveCursorApiKey(await (runtimeApiKeyResolverForTests?.() ?? ctx.modelRegistry.getApiKeyForProvider("cursor")));
|
|
611
|
+
if (apiKey) return apiKey;
|
|
612
|
+
ctx.ui.notify("Cursor cloud lifecycle mutations require a Cursor API key; run /login or set CURSOR_API_KEY, then retry.", "error");
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function mutateRecordedCloudAgent(params: {
|
|
617
|
+
pi: CloudLifecycleApi;
|
|
618
|
+
ctx: CloudLifecycleCommandContext;
|
|
619
|
+
agentId: string;
|
|
620
|
+
action: "archive" | "delete";
|
|
621
|
+
apiKey: string;
|
|
622
|
+
}): Promise<void> {
|
|
623
|
+
const records = readRecordedCloudAgents(params.ctx);
|
|
624
|
+
const validationError = validateRecordedCloudAgentId(params.agentId, records, params.action);
|
|
625
|
+
if (validationError) {
|
|
626
|
+
params.ctx.ui.notify(validationError, "error");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const apiKey = params.apiKey;
|
|
630
|
+
let operations: CloudLifecycleSdkOperations;
|
|
631
|
+
try {
|
|
632
|
+
operations = await getSdkOperations();
|
|
633
|
+
} catch (error) {
|
|
634
|
+
params.ctx.ui.notify(`Failed to prepare Cursor cloud ${params.action}: ${formatCloudLifecycleError(error, apiKey)}`, "error");
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (!appendCloudLifecycleMutationEntry(
|
|
638
|
+
params.pi,
|
|
639
|
+
params.ctx,
|
|
640
|
+
buildBaseEntry(params.agentId, params.action === "archive" ? "archive_intent" : "delete_intent"),
|
|
641
|
+
)) {
|
|
642
|
+
params.ctx.ui.notify(`Cursor cloud ${params.action} was not started because its durable intent could not be recorded.`, "error");
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
await operations[params.action](params.agentId, { apiKey });
|
|
647
|
+
} catch (error) {
|
|
648
|
+
params.ctx.ui.notify(
|
|
649
|
+
`Cursor cloud ${params.action} for agent ${params.agentId} is unresolved: ${formatCloudLifecycleError(error, apiKey)} Inspect it in the Cursor Cloud dashboard before retrying.`,
|
|
650
|
+
"error",
|
|
651
|
+
);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (!appendCloudLifecycleMutationEntry(params.pi, params.ctx, buildBaseEntry(params.agentId, params.action))) {
|
|
655
|
+
params.ctx.ui.notify(
|
|
656
|
+
`Cursor cloud agent ${params.agentId} ${params.action === "archive" ? "archived" : "deleted"}, but its durable result could not be recorded. Inspect it in the Cursor Cloud dashboard before retrying.`,
|
|
657
|
+
"error",
|
|
658
|
+
);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
params.ctx.ui.notify(`Cursor cloud agent ${params.agentId} ${params.action === "archive" ? "archived" : "deleted"}.`, "info");
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export async function runCursorCloudLifecycleCommand(pi: CloudLifecycleApi, args: string, ctx: CloudLifecycleCommandContext): Promise<void> {
|
|
665
|
+
captureCloudLifecycleSession(ctx);
|
|
666
|
+
const usage = "Usage: /cursor-cloud list | archive <bc-agentId> | delete <bc-agentId> --yes";
|
|
667
|
+
const tokens = tokenizeArgs(args);
|
|
668
|
+
const [subcommand, agentId, ...rest] = tokens;
|
|
669
|
+
if (!subcommand || subcommand === "list") {
|
|
670
|
+
if (tokens.length > (subcommand ? 1 : 0)) {
|
|
671
|
+
ctx.ui.notify(`Invalid Cursor cloud arguments. ${usage}`, "error");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (!reconcileDurableCloudLifecycleOrphansForCommand(pi, ctx)) return;
|
|
675
|
+
ctx.ui.notify(formatCloudLifecycleRecords(readRecordedCloudAgents(ctx)), "info");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (subcommand === "archive") {
|
|
679
|
+
if (!agentId || rest.length > 0) {
|
|
680
|
+
ctx.ui.notify(`Invalid Cursor cloud archive arguments. ${usage}`, "error");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const validationError = validateCloudAgentIdSyntax(agentId);
|
|
684
|
+
if (validationError) {
|
|
685
|
+
ctx.ui.notify(validationError, "error");
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const apiKey = await resolveCloudLifecycleMutationApiKey(ctx);
|
|
689
|
+
if (!apiKey || !reconcileDurableCloudLifecycleOrphansForCommand(pi, ctx)) return;
|
|
690
|
+
await mutateRecordedCloudAgent({ pi, ctx, agentId, action: "archive", apiKey });
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (subcommand === "delete") {
|
|
694
|
+
if (!agentId || rest.length !== 1 || rest[0] !== "--yes") {
|
|
695
|
+
ctx.ui.notify(`Delete requires exactly one recorded bc- cloud agent ID and --yes. ${usage}`, "error");
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const validationError = validateCloudAgentIdSyntax(agentId);
|
|
699
|
+
if (validationError) {
|
|
700
|
+
ctx.ui.notify(validationError, "error");
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const apiKey = await resolveCloudLifecycleMutationApiKey(ctx);
|
|
704
|
+
if (!apiKey || !reconcileDurableCloudLifecycleOrphansForCommand(pi, ctx)) return;
|
|
705
|
+
await mutateRecordedCloudAgent({ pi, ctx, agentId, action: "delete", apiKey });
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
ctx.ui.notify(`Invalid Cursor cloud command. ${usage}`, "error");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export const __testUtils = {
|
|
712
|
+
reset: () => {
|
|
713
|
+
cloudLifecycleApi = undefined;
|
|
714
|
+
cloudLifecycleSession = {};
|
|
715
|
+
durableWriterForTests = undefined;
|
|
716
|
+
sessionFsyncForTests = undefined;
|
|
717
|
+
runtimeApiKeyResolverForTests = undefined;
|
|
718
|
+
sdkOperationsForTests = undefined;
|
|
719
|
+
},
|
|
720
|
+
setDurableWriter: (writer: ((data: CursorCloudLifecycleEntryData) => boolean) | undefined) => {
|
|
721
|
+
durableWriterForTests = writer;
|
|
722
|
+
},
|
|
723
|
+
setSessionFsync: (fsync: (() => boolean) | undefined) => {
|
|
724
|
+
sessionFsyncForTests = fsync;
|
|
725
|
+
},
|
|
726
|
+
setRuntimeApiKeyResolver: (resolver: (() => Promise<string | undefined>) | undefined) => {
|
|
727
|
+
runtimeApiKeyResolverForTests = resolver;
|
|
728
|
+
},
|
|
729
|
+
durableLedgerPath,
|
|
730
|
+
setSdkOperations: (operations: CloudLifecycleSdkOperations | undefined) => {
|
|
731
|
+
sdkOperationsForTests = operations;
|
|
732
|
+
},
|
|
733
|
+
};
|