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.
Files changed (85) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/README.md +100 -28
  3. package/docs/cursor-live-smoke-checklist.md +7 -7
  4. package/docs/cursor-model-ux-spec.md +45 -38
  5. package/docs/cursor-native-tool-replay.md +2 -2
  6. package/docs/cursor-native-tool-visual-audit.md +1 -1
  7. package/docs/cursor-testing-lessons.md +20 -7
  8. package/docs/cursor-tool-surfaces.md +13 -2
  9. package/docs/platform-smoke-implementation.md +220 -0
  10. package/docs/platform-smoke.md +183 -247
  11. package/package.json +38 -6
  12. package/platform-smoke.config.mjs +5 -1
  13. package/scripts/cloud-runtime-smoke.d.mts +3 -0
  14. package/scripts/cloud-runtime-smoke.mjs +502 -0
  15. package/scripts/debug-provider-events.mjs +7 -2
  16. package/scripts/isolated-cursor-smoke.sh +4 -6
  17. package/scripts/lib/cursor-child-process.d.mts +1 -0
  18. package/scripts/lib/cursor-child-process.mjs +137 -7
  19. package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
  20. package/scripts/local-resume-cleanup-smoke.mjs +108 -0
  21. package/scripts/local-resume-smoke.d.mts +1 -0
  22. package/scripts/local-resume-smoke.mjs +642 -0
  23. package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
  24. package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
  25. package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
  26. package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
  27. package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
  28. package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
  29. package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
  30. package/scripts/platform-smoke/artifacts.mjs +293 -65
  31. package/scripts/platform-smoke/card-detect.mjs +16 -4
  32. package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
  33. package/scripts/platform-smoke/doctor.mjs +20 -10
  34. package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
  35. package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
  36. package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
  37. package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
  38. package/scripts/platform-smoke/scenarios.mjs +16 -2
  39. package/scripts/platform-smoke/target-runtime.mjs +206 -0
  40. package/scripts/platform-smoke/targets.mjs +33 -141
  41. package/scripts/platform-smoke/visual-evidence.mjs +6 -7
  42. package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
  43. package/scripts/platform-smoke.mjs +40 -27
  44. package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
  45. package/scripts/steering-rpc-smoke.mjs +12 -2
  46. package/scripts/tmux-live-smoke.sh +3 -5
  47. package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
  48. package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
  49. package/shared/cursor-sensitive-text.mjs +7 -1
  50. package/src/context.ts +5 -2
  51. package/src/cursor-agents-context-registration.ts +7 -0
  52. package/src/cursor-agents-context.ts +3 -1
  53. package/src/cursor-api-key.ts +15 -1
  54. package/src/cursor-bridge-contract.ts +3 -0
  55. package/src/cursor-cloud-lifecycle.ts +733 -0
  56. package/src/cursor-cloud-options.ts +206 -0
  57. package/src/cursor-cloud-reporting.ts +246 -0
  58. package/src/cursor-config.ts +659 -0
  59. package/src/cursor-display-only-trace.ts +14 -0
  60. package/src/cursor-display-text.ts +8 -2
  61. package/src/cursor-durable-fs.ts +49 -0
  62. package/src/cursor-fallback-models.generated.ts +2045 -485
  63. package/src/cursor-live-run-accounting.ts +7 -1
  64. package/src/cursor-live-run-coordinator.ts +1 -0
  65. package/src/cursor-pi-tool-bridge-run.ts +14 -4
  66. package/src/cursor-provider-live-run-drain.ts +5 -0
  67. package/src/cursor-provider-run-finalizer.ts +36 -28
  68. package/src/cursor-provider-turn-finalize.ts +72 -6
  69. package/src/cursor-provider-turn-prepare.ts +228 -12
  70. package/src/cursor-provider-turn-runner.ts +41 -13
  71. package/src/cursor-provider-turn-send.ts +59 -16
  72. package/src/cursor-provider-turn-types.ts +44 -10
  73. package/src/cursor-runtime-state.ts +478 -0
  74. package/src/cursor-sdk-event-debug.ts +46 -6
  75. package/src/cursor-sdk-process-error-guard.ts +101 -30
  76. package/src/cursor-session-agent-cleanup.ts +328 -0
  77. package/src/cursor-session-agent-resume.ts +439 -0
  78. package/src/cursor-session-agent.ts +109 -13
  79. package/src/cursor-session-scope.ts +35 -2
  80. package/src/cursor-session-send-policy.ts +1 -1
  81. package/src/cursor-skill-tool.ts +30 -11
  82. package/src/cursor-state.ts +112 -69
  83. package/src/cursor-usage-accounting.ts +14 -4
  84. package/src/index.ts +11 -2
  85. package/src/model-discovery.ts +10 -56
@@ -0,0 +1,478 @@
1
+ import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ registerCursorCloudLifecycleLedger,
4
+ runCursorCloudLifecycleCommand,
5
+ } from "./cursor-cloud-lifecycle.js";
6
+ import {
7
+ CURSOR_AUTO_REVIEW_ENV,
8
+ CURSOR_CLOUD_ACK_ENV,
9
+ CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV,
10
+ CURSOR_CLOUD_BRANCH_ENV,
11
+ CURSOR_CLOUD_CONTEXT_ENV,
12
+ CURSOR_CLOUD_DIRECT_PUSH_ENV,
13
+ CURSOR_CLOUD_ENV_ENV,
14
+ CURSOR_CLOUD_ENV_FROM_FILES_ENV,
15
+ CURSOR_CLOUD_ENV_NAME_ENV,
16
+ CURSOR_CLOUD_ENV_TYPE_ENV,
17
+ CURSOR_CLOUD_REPO_ENV,
18
+ CURSOR_LOCAL_FORCE_ENV,
19
+ CURSOR_LOCAL_RESUME_ENV,
20
+ CURSOR_RUNTIME_ENV,
21
+ CURSOR_SANDBOX_ENV,
22
+ loadCursorSdkConfig,
23
+ loadCursorSdkProjectConfig,
24
+ loadCursorSdkUserConfig,
25
+ mergeCursorSdkConfig,
26
+ parseCursorSdkConfig,
27
+ parseExplicitCursorCloudEnvNames,
28
+ resolveCursorSdkConfig,
29
+ saveCursorSdkProjectConfig,
30
+ saveCursorSdkUserConfig,
31
+ type CursorExplicitSdkConfig,
32
+ type CursorResolvedSdkConfig,
33
+ type CursorResolvedSetting,
34
+ type CursorRuntime,
35
+ type CursorSdkConfig,
36
+ } from "./cursor-config.js";
37
+ import { asRecord } from "./cursor-record-utils.js";
38
+
39
+ export const CURSOR_RUNTIME_ENTRY_TYPE = "cursor-runtime-state";
40
+
41
+ export const CURSOR_CLOUD_ACK_DISCLOSURE = [
42
+ "Cursor Cloud executes this work remotely.",
43
+ "Fresh context is used by default; prior Pi context is included only with explicit bootstrap opt-in.",
44
+ "Pi-local tools and the Pi bridge are unavailable, and Pi environment variables are not forwarded.",
45
+ "Cursor may create branches, commit, push, and open pull requests.",
46
+ "Cloud agents remain until you archive or delete them.",
47
+ "Cloud Agents run in Max Mode, are billed at Cursor API pricing, and may require spend-limit setup.",
48
+ ].join("\n\n");
49
+
50
+ interface CursorRuntimeEntryData {
51
+ runtime: CursorRuntime;
52
+ cloudAcknowledged?: boolean;
53
+ }
54
+
55
+ export type CursorRuntimeStateExtensionApi = Pick<
56
+ ExtensionAPI,
57
+ "appendEntry" | "getFlag" | "registerFlag" | "registerCommand" | "on"
58
+ >;
59
+
60
+ type CursorRuntimeContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "isProjectTrusted">>;
61
+
62
+ type CursorStatusRefresh = (ctx: ExtensionContext) => void;
63
+
64
+ let cliAutoReview = false;
65
+ let cliSandbox = false;
66
+ let cliLocalForce = false;
67
+ let cliLocalForceConsumed = false;
68
+ let cliLocalResume = false;
69
+ let cliNoLocalResume = false;
70
+ let envLocalForceConsumed = false;
71
+ let cliCursorRuntime: string | undefined;
72
+ let cliCursorCloudRepo: string | undefined;
73
+ let cliCursorCloudBranch: string | undefined;
74
+ let cliCursorCloudContext: string | undefined;
75
+ let cliCursorCloudDirectPush = false;
76
+ let cliCursorCloudAllowLocalState = false;
77
+ let cliCursorCloudEnv: string | undefined;
78
+ let cliCursorCloudEnvFromFiles = false;
79
+ let cliCursorCloudEnvType: string | undefined;
80
+ let cliCursorCloudEnvName: string | undefined;
81
+ let cliCursorCloudAck = false;
82
+ let sessionCursorRuntime: CursorRuntime | undefined;
83
+ let sessionCursorCloudAcknowledged = false;
84
+
85
+ function isCursorRuntimeEntryData(value: unknown): value is CursorRuntimeEntryData {
86
+ const record = asRecord(value);
87
+ if (!record) return false;
88
+ return (record.runtime === "local" || record.runtime === "cloud")
89
+ && (record.cloudAcknowledged === undefined || typeof record.cloudAcknowledged === "boolean");
90
+ }
91
+
92
+ function stringFlagValue(value: boolean | string | undefined): string | undefined {
93
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
94
+ }
95
+
96
+ export function getCursorCliConfig(): CursorExplicitSdkConfig {
97
+ const parsed = parseCursorSdkConfig({
98
+ cloud: {
99
+ repo: cliCursorCloudRepo,
100
+ branch: cliCursorCloudBranch,
101
+ ...(cliCursorCloudDirectPush ? { directPush: true } : {}),
102
+ ...(cliCursorCloudAllowLocalState ? { allowLocalState: true } : {}),
103
+ envNames: parseExplicitCursorCloudEnvNames(cliCursorCloudEnv, "--cursor-cloud-env"),
104
+ ...(cliCursorCloudEnvFromFiles ? { envFromFiles: true } : {}),
105
+ environment: {
106
+ type: cliCursorCloudEnvType,
107
+ name: cliCursorCloudEnvName,
108
+ },
109
+ ...(cliCursorCloudAck ? { acknowledged: true } : {}),
110
+ },
111
+ local: {
112
+ ...(cliAutoReview ? { autoReview: true } : {}),
113
+ ...(cliSandbox ? { sandboxOptions: { enabled: true } } : {}),
114
+ ...(cliLocalForce ? { force: true } : {}),
115
+ ...(cliNoLocalResume ? { resume: false } : cliLocalResume ? { resume: true } : {}),
116
+ },
117
+ }) ?? {};
118
+ return {
119
+ ...parsed,
120
+ ...(cliCursorRuntime ? { runtime: cliCursorRuntime } : {}),
121
+ ...(cliCursorCloudContext
122
+ ? { cloud: { ...parsed.cloud, contextHandoff: cliCursorCloudContext } }
123
+ : {}),
124
+ };
125
+ }
126
+
127
+ export function getCursorSessionConfig(): CursorSdkConfig {
128
+ return sessionCursorRuntime
129
+ ? {
130
+ runtime: sessionCursorRuntime,
131
+ ...(sessionCursorCloudAcknowledged ? { cloud: { acknowledged: true } } : {}),
132
+ }
133
+ : {};
134
+ }
135
+
136
+ export function resolveEffectiveCursorConfig(options: {
137
+ cwd: string;
138
+ projectTrusted?: boolean;
139
+ }): CursorResolvedSdkConfig {
140
+ const loadedConfig = loadCursorSdkConfig({ cwd: options.cwd, projectTrusted: options.projectTrusted === true });
141
+ return resolveCursorSdkConfig({
142
+ cli: getCursorCliConfig(),
143
+ session: getCursorSessionConfig(),
144
+ user: loadedConfig.user,
145
+ project: loadedConfig.project,
146
+ });
147
+ }
148
+
149
+ export function resolveEffectiveCursorConfigForContext(ctx: CursorRuntimeContext): CursorResolvedSdkConfig {
150
+ return resolveEffectiveCursorConfig({ cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted?.() === true });
151
+ }
152
+
153
+ export type CursorRuntimeResolution =
154
+ | { kind: "valid"; runtime: CursorResolvedSetting<CursorRuntime> }
155
+ | { kind: "invalid"; message: string };
156
+
157
+ export function resolveCursorStatusRuntime(ctx: CursorRuntimeContext): CursorRuntimeResolution {
158
+ try {
159
+ return { kind: "valid", runtime: resolveEffectiveCursorConfigForContext(ctx).runtime };
160
+ } catch (error) {
161
+ return { kind: "invalid", message: error instanceof Error ? error.message : String(error) };
162
+ }
163
+ }
164
+
165
+ export function formatResolvedCursorRuntime(runtime: CursorResolvedSetting<CursorRuntime>): string {
166
+ const cap = runtime.cappedBy;
167
+ return cap
168
+ ? `${runtime.value} (source: ${runtime.source} safety cap over ${cap.cappedSource} ${cap.cappedValue})`
169
+ : `${runtime.value} (source: ${runtime.source})`;
170
+ }
171
+
172
+ export function formatCursorStatus(
173
+ runtime: CursorRuntime | "invalid",
174
+ fast: boolean | undefined,
175
+ mode: "agent" | "plan" | "invalid",
176
+ ): string {
177
+ const parts = [`cursor:${runtime}`, fast === true ? "fast:on" : fast === false ? "fast:off" : "fast:n/a"];
178
+ if (mode === "invalid") parts.push("mode invalid");
179
+ else if (mode === "plan") parts.push("plan");
180
+ return parts.join(" · ");
181
+ }
182
+
183
+ export function consumeCursorLocalForceOverride(resolved: { value: boolean; source: string }): boolean {
184
+ if (!resolved.value) return false;
185
+ if (resolved.source === "cli" && !cliLocalForceConsumed) {
186
+ cliLocalForce = false;
187
+ cliLocalForceConsumed = true;
188
+ return true;
189
+ }
190
+ if (resolved.source === "environment" && !envLocalForceConsumed) {
191
+ envLocalForceConsumed = true;
192
+ return true;
193
+ }
194
+ return false;
195
+ }
196
+
197
+ export function restoreSessionCursorRuntimeState(branch: readonly SessionEntry[]): void {
198
+ sessionCursorRuntime = undefined;
199
+ sessionCursorCloudAcknowledged = false;
200
+ for (const entry of branch) {
201
+ if (entry.type !== "custom" || entry.customType !== CURSOR_RUNTIME_ENTRY_TYPE) continue;
202
+ if (isCursorRuntimeEntryData(entry.data)) {
203
+ sessionCursorRuntime = entry.data.runtime;
204
+ sessionCursorCloudAcknowledged ||= entry.data.cloudAcknowledged === true;
205
+ }
206
+ }
207
+ }
208
+
209
+ export function restoreCursorCliState(pi: Pick<ExtensionAPI, "getFlag">): void {
210
+ cliAutoReview = pi.getFlag("cursor-auto-review") === true;
211
+ cliSandbox = pi.getFlag("cursor-sandbox") === true;
212
+ cliLocalForce = !cliLocalForceConsumed && pi.getFlag("cursor-local-force") === true;
213
+ cliLocalResume = pi.getFlag("cursor-local-resume") === true;
214
+ cliNoLocalResume = pi.getFlag("cursor-no-local-resume") === true;
215
+ cliCursorRuntime = stringFlagValue(pi.getFlag("cursor-runtime"));
216
+ cliCursorCloudRepo = stringFlagValue(pi.getFlag("cursor-cloud-repo"));
217
+ cliCursorCloudBranch = stringFlagValue(pi.getFlag("cursor-cloud-branch"));
218
+ cliCursorCloudContext = stringFlagValue(pi.getFlag("cursor-cloud-context"));
219
+ cliCursorCloudDirectPush = pi.getFlag("cursor-cloud-direct-push") === true;
220
+ cliCursorCloudAllowLocalState = pi.getFlag("cursor-cloud-allow-local-state") === true;
221
+ cliCursorCloudEnv = stringFlagValue(pi.getFlag("cursor-cloud-env"));
222
+ cliCursorCloudEnvFromFiles = pi.getFlag("cursor-cloud-env-from-files") === true;
223
+ cliCursorCloudEnvType = stringFlagValue(pi.getFlag("cursor-cloud-env-type"));
224
+ cliCursorCloudEnvName = stringFlagValue(pi.getFlag("cursor-cloud-env-name"));
225
+ cliCursorCloudAck = pi.getFlag("cursor-cloud-ack") === true;
226
+ }
227
+
228
+ function persistCursorRuntimePreference(
229
+ pi: Pick<ExtensionAPI, "appendEntry">,
230
+ runtime: CursorRuntime,
231
+ cloudAcknowledged = false,
232
+ ): void {
233
+ const acknowledged = sessionCursorCloudAcknowledged || cloudAcknowledged;
234
+ pi.appendEntry<CursorRuntimeEntryData>(CURSOR_RUNTIME_ENTRY_TYPE, {
235
+ runtime,
236
+ ...(acknowledged ? { cloudAcknowledged: true } : {}),
237
+ });
238
+ sessionCursorRuntime = runtime;
239
+ sessionCursorCloudAcknowledged = acknowledged;
240
+ }
241
+
242
+ function registerCursorRuntimeFlags(pi: Pick<ExtensionAPI, "registerFlag">): void {
243
+ pi.registerFlag("cursor-runtime", {
244
+ description: `Select Cursor runtime for this run: local or cloud (or set ${CURSOR_RUNTIME_ENV})`,
245
+ type: "string",
246
+ default: "",
247
+ });
248
+ pi.registerFlag("cursor-cloud-repo", {
249
+ description: `Set Cursor cloud repository URL for this run (or set ${CURSOR_CLOUD_REPO_ENV})`,
250
+ type: "string",
251
+ default: "",
252
+ });
253
+ pi.registerFlag("cursor-cloud-branch", {
254
+ description: `Set Cursor cloud branch/ref for this run (or set ${CURSOR_CLOUD_BRANCH_ENV})`,
255
+ type: "string",
256
+ default: "",
257
+ });
258
+ pi.registerFlag("cursor-cloud-context", {
259
+ description: `Set Cursor cloud context handoff: never, fresh, or bootstrap (or set ${CURSOR_CLOUD_CONTEXT_ENV})`,
260
+ type: "string",
261
+ default: "",
262
+ });
263
+ pi.registerFlag("cursor-cloud-direct-push", {
264
+ description: `Allow Cursor cloud direct push for this run (or set ${CURSOR_CLOUD_DIRECT_PUSH_ENV}=1)`,
265
+ type: "boolean",
266
+ default: false,
267
+ });
268
+ pi.registerFlag("cursor-cloud-allow-local-state", {
269
+ description: `Allow Cursor cloud to proceed with local-only state for this run (or set ${CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV}=1)`,
270
+ type: "boolean",
271
+ default: false,
272
+ });
273
+ pi.registerFlag("cursor-cloud-env", {
274
+ description: `Reserved Cursor cloud env var names; env forwarding is not implemented yet (or set ${CURSOR_CLOUD_ENV_ENV})`,
275
+ type: "string",
276
+ default: "",
277
+ });
278
+ pi.registerFlag("cursor-cloud-env-from-files", {
279
+ description: `Reserved Cursor cloud env-file forwarding flag; not implemented yet (or set ${CURSOR_CLOUD_ENV_FROM_FILES_ENV}=1)`,
280
+ type: "boolean",
281
+ default: false,
282
+ });
283
+ pi.registerFlag("cursor-cloud-env-type", {
284
+ description: `Select Cursor-managed cloud environment type: cloud, pool, or machine (or set ${CURSOR_CLOUD_ENV_TYPE_ENV})`,
285
+ type: "string",
286
+ default: "",
287
+ });
288
+ pi.registerFlag("cursor-cloud-env-name", {
289
+ description: `Select Cursor-managed cloud environment name (or set ${CURSOR_CLOUD_ENV_NAME_ENV})`,
290
+ type: "string",
291
+ default: "",
292
+ });
293
+ pi.registerFlag("cursor-cloud-ack", {
294
+ description: `Acknowledge first-use Cursor cloud runtime risks for this run (or set ${CURSOR_CLOUD_ACK_ENV}=1)`,
295
+ type: "boolean",
296
+ default: false,
297
+ });
298
+ pi.registerFlag("cursor-auto-review", {
299
+ description: `Enable Cursor SDK local Auto-review for this run (or set ${CURSOR_AUTO_REVIEW_ENV}=1)`,
300
+ type: "boolean",
301
+ default: false,
302
+ });
303
+ pi.registerFlag("cursor-sandbox", {
304
+ description: `Enable Cursor SDK local sandboxing for this run (or set ${CURSOR_SANDBOX_ENV}=1)`,
305
+ type: "boolean",
306
+ default: false,
307
+ });
308
+ pi.registerFlag("cursor-local-force", {
309
+ description: `Force-expire a stuck local Cursor SDK run before sending this run (or set ${CURSOR_LOCAL_FORCE_ENV}=1)`,
310
+ type: "boolean",
311
+ default: false,
312
+ });
313
+ pi.registerFlag("cursor-local-resume", {
314
+ description: `Resume recorded local Cursor SDK agents for matching pi session branches (default; or set ${CURSOR_LOCAL_RESUME_ENV}=1)`,
315
+ type: "boolean",
316
+ default: false,
317
+ });
318
+ pi.registerFlag("cursor-no-local-resume", {
319
+ description: `Disable local Cursor SDK agent resume for this run (or set ${CURSOR_LOCAL_RESUME_ENV}=0)`,
320
+ type: "boolean",
321
+ default: false,
322
+ });
323
+ }
324
+
325
+ async function confirmCloudAcknowledgement(
326
+ ctx: Pick<ExtensionContext, "hasUI" | "ui">,
327
+ alreadyAcknowledged: boolean,
328
+ ): Promise<boolean> {
329
+ if (alreadyAcknowledged) return true;
330
+ if (!ctx.hasUI) {
331
+ ctx.ui.notify(
332
+ `Cursor cloud runtime requires first-use acknowledgement. Pass --cursor-cloud-ack or set ${CURSOR_CLOUD_ACK_ENV}=1, then retry.`,
333
+ "error",
334
+ );
335
+ return false;
336
+ }
337
+ const confirmed = await ctx.ui.confirm("Enable Cursor Cloud runtime?", CURSOR_CLOUD_ACK_DISCLOSURE);
338
+ if (!confirmed) ctx.ui.notify("Cursor cloud runtime change cancelled; no settings were written.", "info");
339
+ return confirmed;
340
+ }
341
+
342
+ function registerCursorRuntimeCommand(
343
+ pi: Pick<ExtensionAPI, "appendEntry" | "registerCommand">,
344
+ refreshStatus: CursorStatusRefresh,
345
+ ): void {
346
+ pi.registerCommand("cursor-runtime", {
347
+ description: "Set Cursor runtime for this session: local or cloud",
348
+ handler: async (args, ctx) => {
349
+ const usage = "Usage: /cursor-runtime local|cloud [--save-user|--save-project]";
350
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
351
+ const raw = tokens[0];
352
+ const saveUser = tokens.includes("--save-user");
353
+ const saveProject = tokens.includes("--save-project");
354
+ const extra = tokens.slice(1).filter((token) => token !== "--save-user" && token !== "--save-project");
355
+ const currentResolution = resolveCursorStatusRuntime(ctx);
356
+ if (!raw) {
357
+ ctx.ui.notify(
358
+ currentResolution.kind === "invalid"
359
+ ? `${currentResolution.message} ${usage}`
360
+ : `Cursor runtime is ${formatResolvedCursorRuntime(currentResolution.runtime)}. ${usage}`,
361
+ currentResolution.kind === "invalid" ? "error" : "info",
362
+ );
363
+ return;
364
+ }
365
+ if ((raw !== "local" && raw !== "cloud") || extra.length > 0 || (saveUser && saveProject)) {
366
+ ctx.ui.notify(`Invalid Cursor runtime arguments. ${usage}`, "error");
367
+ return;
368
+ }
369
+ if (currentResolution.kind === "invalid") {
370
+ ctx.ui.notify(`${currentResolution.message} Fix the explicit override before changing the session runtime.`, "error");
371
+ return;
372
+ }
373
+ const cloudAcknowledged = raw === "cloud" && await confirmCloudAcknowledgement(
374
+ ctx,
375
+ resolveEffectiveCursorConfigForContext(ctx).cloud.acknowledged.value,
376
+ );
377
+ if (raw === "cloud" && !cloudAcknowledged) return;
378
+ if (saveUser || saveProject) {
379
+ try {
380
+ if (saveUser) {
381
+ const current = loadCursorSdkUserConfig();
382
+ saveCursorSdkUserConfig(mergeCursorSdkConfig(current, {
383
+ runtime: raw,
384
+ ...(cloudAcknowledged ? { cloud: { acknowledged: true } } : {}),
385
+ }));
386
+ } else {
387
+ const current = loadCursorSdkProjectConfig(ctx.cwd, true) ?? {};
388
+ saveCursorSdkProjectConfig(ctx.cwd, mergeCursorSdkConfig(current, { runtime: raw }));
389
+ }
390
+ } catch (error) {
391
+ const effectiveResolution = resolveCursorStatusRuntime(ctx);
392
+ refreshStatus(ctx);
393
+ const effective = effectiveResolution.kind === "valid"
394
+ ? ` Effective runtime remains ${formatResolvedCursorRuntime(effectiveResolution.runtime)}.`
395
+ : ` ${effectiveResolution.message}`;
396
+ ctx.ui.notify(
397
+ `Failed to save Cursor runtime preference to ${saveUser ? "user" : "project"} config: ${error instanceof Error ? error.message : String(error)}.${effective}`,
398
+ "error",
399
+ );
400
+ return;
401
+ }
402
+ }
403
+ try {
404
+ persistCursorRuntimePreference(pi, raw, cloudAcknowledged);
405
+ } catch (error) {
406
+ const effectiveResolution = resolveCursorStatusRuntime(ctx);
407
+ refreshStatus(ctx);
408
+ const effective = effectiveResolution.kind === "valid"
409
+ ? ` Effective runtime is ${formatResolvedCursorRuntime(effectiveResolution.runtime)}.`
410
+ : ` ${effectiveResolution.message}`;
411
+ const persisted = saveUser
412
+ ? "User config was saved, but persisting the session runtime entry failed."
413
+ : saveProject
414
+ ? "Project config was saved, but persisting the session runtime entry failed."
415
+ : "Persisting the session runtime entry failed.";
416
+ ctx.ui.notify(
417
+ `${persisted} ${error instanceof Error ? error.message : String(error)}.${effective}`,
418
+ "error",
419
+ );
420
+ return;
421
+ }
422
+ const effectiveResolution = resolveCursorStatusRuntime(ctx);
423
+ refreshStatus(ctx);
424
+ const saved = saveUser
425
+ ? " Saved to user config."
426
+ : saveProject
427
+ ? " Saved to project config; cloud acknowledgement remains session/user-scoped."
428
+ : "";
429
+ const effective = effectiveResolution.kind === "valid"
430
+ ? ` Effective runtime is ${formatResolvedCursorRuntime(effectiveResolution.runtime)}.`
431
+ : ` ${effectiveResolution.message}`;
432
+ ctx.ui.notify(
433
+ raw === "cloud"
434
+ ? `Cursor runtime request saved for this session and cloud risk acknowledged.${effective}${saved}`
435
+ : `Cursor runtime request saved for this session.${effective}${saved}`,
436
+ "info",
437
+ );
438
+ },
439
+ });
440
+ }
441
+
442
+ export function registerCursorCloudRuntimeControls(
443
+ pi: CursorRuntimeStateExtensionApi,
444
+ options: { refreshStatus: CursorStatusRefresh },
445
+ ): void {
446
+ registerCursorCloudLifecycleLedger(pi);
447
+ registerCursorRuntimeFlags(pi);
448
+ registerCursorRuntimeCommand(pi, options.refreshStatus);
449
+ pi.registerCommand("cursor-cloud", {
450
+ description: "List, archive, or delete recorded Cursor cloud agents for this session branch",
451
+ handler: async (args, ctx) => {
452
+ await runCursorCloudLifecycleCommand(pi, args, ctx);
453
+ },
454
+ });
455
+ }
456
+
457
+ export function resetCursorRuntimeStateForTests(): void {
458
+ cliAutoReview = false;
459
+ cliSandbox = false;
460
+ cliLocalForce = false;
461
+ cliLocalForceConsumed = false;
462
+ cliLocalResume = false;
463
+ cliNoLocalResume = false;
464
+ envLocalForceConsumed = false;
465
+ cliCursorRuntime = undefined;
466
+ cliCursorCloudRepo = undefined;
467
+ cliCursorCloudBranch = undefined;
468
+ cliCursorCloudContext = undefined;
469
+ cliCursorCloudDirectPush = false;
470
+ cliCursorCloudAllowLocalState = false;
471
+ cliCursorCloudEnv = undefined;
472
+ cliCursorCloudEnvFromFiles = false;
473
+ cliCursorCloudEnvType = undefined;
474
+ cliCursorCloudEnvName = undefined;
475
+ cliCursorCloudAck = false;
476
+ sessionCursorRuntime = undefined;
477
+ sessionCursorCloudAcknowledged = false;
478
+ }
@@ -36,6 +36,8 @@ export {
36
36
  resolveCursorSdkEventDebugBaseDir,
37
37
  } from "./cursor-sdk-event-debug-constants.js";
38
38
 
39
+ const MAX_CURSOR_SDK_EVENT_DEBUG_JSONL_BYTES = 2 * 1024 * 1024;
40
+
39
41
  export type CursorSdkDisplayDecisionAction =
40
42
  | "skip-duplicate"
41
43
  | "skip-incomplete-fast-local"
@@ -123,6 +125,22 @@ function snapshotCursorSdkEventDebugRecord(record: unknown): unknown {
123
125
  }
124
126
  }
125
127
 
128
+ function serializeCursorSdkEventDebugRecord(record: unknown): string {
129
+ try {
130
+ const seen = new WeakSet<object>();
131
+ return JSON.stringify(snapshotCursorSdkEventDebugRecord(record), (_key, value: unknown) => {
132
+ if (typeof value === "bigint") return value.toString();
133
+ if (value && typeof value === "object") {
134
+ if (seen.has(value)) return "[Circular]";
135
+ seen.add(value);
136
+ }
137
+ return value;
138
+ }) ?? JSON.stringify({ type: "artifact_serialization_error" });
139
+ } catch {
140
+ return JSON.stringify({ type: "artifact_serialization_error" });
141
+ }
142
+ }
143
+
126
144
  export function resolveCursorSdkEventDebugEnabled(env: Record<string, string | undefined> = process.env): boolean {
127
145
  return parseEnvBoolean(env[CURSOR_SDK_EVENT_DEBUG_ENV], false);
128
146
  }
@@ -200,7 +218,11 @@ export function attachCursorSdkEventDebugPiStreamTap(
200
218
  if (!resolveCursorSdkEventDebugEnabled()) return;
201
219
  const originalPush = stream.push.bind(stream);
202
220
  stream.push = (event) => {
203
- sinkRef.current?.recordPiStreamEvent(event);
221
+ try {
222
+ sinkRef.current?.recordPiStreamEvent(event);
223
+ } catch {
224
+ // Debug capture must never block the underlying stream.
225
+ }
204
226
  return originalPush(event);
205
227
  };
206
228
  }
@@ -229,7 +251,9 @@ export class CursorSdkEventDebugSink {
229
251
  errors: 0,
230
252
  };
231
253
  private metadata: Record<string, unknown>;
232
- private readonly jsonlBuffers = new Map<string, unknown[]>();
254
+ private readonly jsonlBuffers = new Map<string, string[]>();
255
+ private readonly jsonlBufferBytes = new Map<string, number>();
256
+ private readonly truncatedJsonlFiles = new Set<string>();
233
257
  private finalized = false;
234
258
  private finalizationPromise: Promise<void> | undefined;
235
259
  private waitResultRecorded = false;
@@ -511,6 +535,8 @@ export class CursorSdkEventDebugSink {
511
535
  streamCaptureErrors: this.streamCaptureErrors.map((error) =>
512
536
  error instanceof Error ? error.message : String(error),
513
537
  ),
538
+ jsonlByteLimit: MAX_CURSOR_SDK_EVENT_DEBUG_JSONL_BYTES,
539
+ truncatedJsonlFiles: [...this.truncatedJsonlFiles].sort(),
514
540
  };
515
541
  this.flushJsonlBuffers();
516
542
  writeFileSync(join(this.artifactDir, ARTIFACTS.summary), `${JSON.stringify(summary, null, 2)}\n`);
@@ -582,24 +608,38 @@ export class CursorSdkEventDebugSink {
582
608
  }
583
609
 
584
610
  private bufferJsonl(fileName: string, record: unknown): void {
585
- if (this.finalized) return;
611
+ if (this.finalized || this.truncatedJsonlFiles.has(fileName)) return;
612
+ const line = `${serializeCursorSdkEventDebugRecord(record)}\n`;
613
+ const marker = `${JSON.stringify({ type: "artifact_truncated", limitBytes: MAX_CURSOR_SDK_EVENT_DEBUG_JSONL_BYTES })}\n`;
614
+ const usedBytes = this.jsonlBufferBytes.get(fileName) ?? 0;
615
+ const lineBytes = Buffer.byteLength(line);
616
+ const markerBytes = Buffer.byteLength(marker);
586
617
  const records = this.jsonlBuffers.get(fileName) ?? [];
587
- records.push(snapshotCursorSdkEventDebugRecord(record));
618
+ if (usedBytes + lineBytes > MAX_CURSOR_SDK_EVENT_DEBUG_JSONL_BYTES - markerBytes) {
619
+ records.push(marker);
620
+ this.jsonlBuffers.set(fileName, records);
621
+ this.jsonlBufferBytes.set(fileName, usedBytes + markerBytes);
622
+ this.truncatedJsonlFiles.add(fileName);
623
+ return;
624
+ }
625
+ records.push(line);
588
626
  this.jsonlBuffers.set(fileName, records);
627
+ this.jsonlBufferBytes.set(fileName, usedBytes + lineBytes);
589
628
  }
590
629
 
591
630
  private flushJsonlBuffers(): void {
592
631
  for (const [fileName, records] of this.jsonlBuffers) {
593
- const lines = records.map((record) => `${JSON.stringify(record)}\n`).join("");
594
- writeFileSync(join(this.artifactDir, fileName), lines);
632
+ writeFileSync(join(this.artifactDir, fileName), records.join(""));
595
633
  }
596
634
  this.jsonlBuffers.clear();
635
+ this.jsonlBufferBytes.clear();
597
636
  }
598
637
  }
599
638
 
600
639
  export const __testUtils = {
601
640
  ARTIFACTS,
602
641
  SESSION_MANIFEST,
642
+ MAX_CURSOR_SDK_EVENT_DEBUG_JSONL_BYTES,
603
643
  slugSessionKey,
604
644
  resetSessionDebugState: resetCursorSdkEventDebugSessionStateForTests,
605
645
  };