@emmaneugene/pi-cursor-sdk 0.3.7 → 0.4.1

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 (91) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +99 -126
  3. package/dist/cursor-agents-context-registration.js +1 -3
  4. package/dist/cursor-agents-context.js +2 -2
  5. package/dist/cursor-config.js +2 -290
  6. package/dist/cursor-extension-factory-guard.js +31 -0
  7. package/dist/cursor-pi-tool-bridge-server.js +3 -0
  8. package/dist/cursor-pi-tool-bridge.js +5 -0
  9. package/dist/cursor-provider-errors.js +3 -58
  10. package/dist/cursor-provider-live-run-drain.js +0 -3
  11. package/dist/cursor-provider-run-finalizer.js +4 -7
  12. package/dist/cursor-provider-run-outcome.js +1 -1
  13. package/dist/cursor-provider-turn-finalize.js +3 -55
  14. package/dist/cursor-provider-turn-prepare.js +8 -144
  15. package/dist/cursor-provider-turn-runner.js +9 -23
  16. package/dist/cursor-provider-turn-send.js +8 -35
  17. package/dist/cursor-runtime-state.js +6 -268
  18. package/dist/cursor-sdk-billed-usage.js +3 -18
  19. package/dist/cursor-sdk-platform-package.js +88 -0
  20. package/dist/cursor-session-agent.js +18 -14
  21. package/dist/cursor-skill-tool.js +12 -22
  22. package/dist/cursor-state.js +5 -8
  23. package/dist/cursor-usage-accounting.js +1 -1
  24. package/dist/index.js +65 -49
  25. package/docs/cursor-dogfood-checklist.md +4 -4
  26. package/docs/cursor-live-smoke-checklist.md +24 -24
  27. package/docs/cursor-model-ux-spec.md +27 -30
  28. package/docs/cursor-native-tool-replay.md +4 -4
  29. package/docs/cursor-native-tool-visual-audit.md +10 -10
  30. package/docs/cursor-testing-lessons.md +38 -16
  31. package/docs/cursor-tool-surfaces.md +2 -4
  32. package/docs/platform-smoke-implementation.md +5 -5
  33. package/docs/platform-smoke.md +28 -73
  34. package/node_modules/cross-spawn/node_modules/which/CHANGELOG.md +166 -0
  35. package/package.json +3 -17
  36. package/platform-smoke.config.mjs +2 -1
  37. package/scripts/lib/cursor-visual-render.mjs +12 -2
  38. package/scripts/lib/local-resume-smoke-harness.mjs +0 -18
  39. package/scripts/platform-smoke/card-detect.mjs +1 -1
  40. package/scripts/platform-smoke/local-resume-runner.mjs +1 -1
  41. package/scripts/platform-smoke/scenarios.mjs +1 -1
  42. package/scripts/platform-smoke/targets.mjs +14 -5
  43. package/scripts/visual-tui-smoke-self-test.mjs +9 -33
  44. package/scripts/visual-tui-smoke.mjs +54 -11
  45. package/src/cursor-agents-context-registration.ts +0 -5
  46. package/src/cursor-agents-context.ts +1 -3
  47. package/src/cursor-config.ts +8 -379
  48. package/src/cursor-extension-factory-guard.ts +57 -0
  49. package/src/cursor-pi-tool-bridge-server.ts +4 -0
  50. package/src/cursor-pi-tool-bridge.ts +5 -0
  51. package/src/cursor-provider-errors.ts +2 -62
  52. package/src/cursor-provider-live-run-drain.ts +0 -3
  53. package/src/cursor-provider-run-finalizer.ts +4 -12
  54. package/src/cursor-provider-run-outcome.ts +0 -3
  55. package/src/cursor-provider-turn-finalize.ts +3 -61
  56. package/src/cursor-provider-turn-prepare.ts +8 -165
  57. package/src/cursor-provider-turn-runner.ts +15 -31
  58. package/src/cursor-provider-turn-send.ts +7 -38
  59. package/src/cursor-provider-turn-types.ts +9 -30
  60. package/src/cursor-runtime-state.ts +6 -345
  61. package/src/cursor-sdk-billed-usage.ts +3 -24
  62. package/src/cursor-sdk-platform-package.ts +106 -0
  63. package/src/cursor-session-agent.ts +16 -12
  64. package/src/cursor-skill-tool.ts +10 -26
  65. package/src/cursor-state.ts +5 -10
  66. package/src/cursor-usage-accounting.ts +1 -3
  67. package/src/index.ts +70 -49
  68. package/dist/cursor-cloud-lifecycle.js +0 -650
  69. package/dist/cursor-cloud-local-state.js +0 -460
  70. package/dist/cursor-cloud-options.js +0 -145
  71. package/dist/cursor-cloud-reporting.js +0 -222
  72. package/dist/cursor-ripgrep-path.js +0 -27
  73. package/scripts/cloud-runtime-smoke.d.mts +0 -42
  74. package/scripts/cloud-runtime-smoke.mjs +0 -623
  75. package/scripts/lib/cloud-smoke-artifacts.d.mts +0 -16
  76. package/scripts/lib/cloud-smoke-artifacts.mjs +0 -94
  77. package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +0 -209
  78. package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +0 -585
  79. package/scripts/lib/cloud-smoke-github.d.mts +0 -78
  80. package/scripts/lib/cloud-smoke-github.mjs +0 -331
  81. package/scripts/lib/cloud-smoke-pi-runner.d.mts +0 -42
  82. package/scripts/lib/cloud-smoke-pi-runner.mjs +0 -214
  83. package/scripts/lib/cloud-smoke-shutdown.d.mts +0 -67
  84. package/scripts/lib/cloud-smoke-shutdown.mjs +0 -133
  85. package/shared/cursor-cloud-lifecycle-constants.d.mts +0 -3
  86. package/shared/cursor-cloud-lifecycle-constants.mjs +0 -7
  87. package/src/cursor-cloud-lifecycle.ts +0 -733
  88. package/src/cursor-cloud-local-state.ts +0 -536
  89. package/src/cursor-cloud-options.ts +0 -182
  90. package/src/cursor-cloud-reporting.ts +0 -267
  91. package/src/cursor-ripgrep-path.ts +0 -32
@@ -1,106 +1,38 @@
1
- import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
2
- import {
3
- registerCursorCloudLifecycleLedger,
4
- runCursorCloudLifecycleCommand,
5
- } from "./cursor-cloud-lifecycle.js";
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
6
2
  import {
7
3
  CURSOR_AUTO_REVIEW_ENV,
8
- CURSOR_CLOUD_ACK_ENV,
9
- CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV,
10
- CURSOR_CLOUD_AUTO_CREATE_PR_ENV,
11
- CURSOR_CLOUD_BRANCH_ENV,
12
- CURSOR_CLOUD_CONTEXT_ENV,
13
- CURSOR_CLOUD_DIRECT_PUSH_ENV,
14
- CURSOR_CLOUD_ENV_ENV,
15
- CURSOR_CLOUD_ENV_FROM_FILES_ENV,
16
- CURSOR_CLOUD_ENV_NAME_ENV,
17
- CURSOR_CLOUD_ENV_TYPE_ENV,
18
- CURSOR_CLOUD_REPO_ENV,
19
- CURSOR_CLOUD_SKIP_REVIEWER_REQUEST_ENV,
20
4
  CURSOR_LOCAL_FORCE_ENV,
21
5
  CURSOR_LOCAL_RESUME_ENV,
22
- CURSOR_RUNTIME_ENV,
23
6
  CURSOR_SANDBOX_ENV,
24
- getCursorSdkProjectConfigPath,
25
- getCursorSdkUserConfigPath,
26
7
  loadCursorSdkConfig,
27
- mergeCursorSdkConfigForUpdate,
28
- parseExplicitCursorCloudEnvNames,
29
8
  resolveCursorSdkConfig,
30
- updateCursorSdkConfig,
31
9
  type CursorExplicitSdkConfig,
32
10
  type CursorResolvedSdkConfig,
33
11
  type CursorResolvedSetting,
34
- type CursorRuntime,
35
12
  type CursorSdkConfig,
36
13
  } from "./cursor-config.js";
37
- import { asRecord } from "./cursor-record-utils.js";
38
14
  import { getResolvedSessionCursorHttp1Enabled } from "./cursor-http1.js";
39
15
  import { getCursorSessionCwd, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
40
16
 
41
- export const CURSOR_RUNTIME_ENTRY_TYPE = "cursor-runtime-state";
42
-
43
- export const CURSOR_CLOUD_ACK_DISCLOSURE = [
44
- "Cursor Cloud executes this work remotely.",
45
- "Fresh context is used by default; prior Pi context is included only with explicit bootstrap opt-in.",
46
- "Pi-local tools and the Pi bridge are unavailable, and Pi environment variables are not forwarded.",
47
- "Cursor may create branches, commit, push, and open pull requests.",
48
- "Cloud agents remain until you archive or delete them.",
49
- "Cloud Agents run in Max Mode, are billed at Cursor API pricing, and may require spend-limit setup.",
50
- ].join("\n\n");
51
-
52
- interface CursorRuntimeEntryData {
53
- runtime: CursorRuntime;
54
- cloudAcknowledged?: boolean;
55
- }
56
-
57
- export type CursorRuntimeStateExtensionApi = Pick<
58
- ExtensionAPI,
59
- "appendEntry" | "getFlag" | "registerFlag" | "registerCommand" | "on"
60
- >;
17
+ export type CursorRuntimeStateExtensionApi = Pick<ExtensionAPI, "getFlag" | "registerFlag">;
61
18
 
62
19
  type CursorRuntimeContext = Pick<ExtensionContext, "cwd">;
63
20
 
64
- type CursorStatusRefresh = (ctx: ExtensionContext) => void;
65
-
66
21
  interface CursorCliConfigSnapshot {
67
22
  config: CursorExplicitSdkConfig;
68
- cloudEnvNames?: string;
69
23
  }
70
24
 
71
25
  let cliCursorSnapshot: CursorCliConfigSnapshot = { config: {} };
72
26
  let cliLocalForceConsumed = false;
73
27
  let envLocalForceConsumed = false;
74
- let sessionCursorRuntime: CursorRuntime | undefined;
75
- let sessionCursorCloudAcknowledged = false;
76
-
77
- function isCursorRuntimeEntryData(value: unknown): value is CursorRuntimeEntryData {
78
- const record = asRecord(value);
79
- if (!record) return false;
80
- return (record.runtime === "local" || record.runtime === "cloud")
81
- && (record.cloudAcknowledged === undefined || typeof record.cloudAcknowledged === "boolean");
82
- }
83
-
84
- function stringFlagValue(value: boolean | string | undefined): string | undefined {
85
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
86
- }
87
28
 
88
29
  export function getCursorCliConfig(): CursorExplicitSdkConfig {
89
- const config = structuredClone(cliCursorSnapshot.config);
90
- const envNames = parseExplicitCursorCloudEnvNames(cliCursorSnapshot.cloudEnvNames, "--cursor-cloud-env");
91
- if (envNames) config.cloud = { ...config.cloud, envNames };
92
- return config;
30
+ return structuredClone(cliCursorSnapshot.config);
93
31
  }
94
32
 
95
33
  export function getCursorSessionConfig(): CursorSdkConfig {
96
34
  const useHttp1ForAgent = getResolvedSessionCursorHttp1Enabled();
97
35
  return {
98
- ...(sessionCursorRuntime
99
- ? {
100
- runtime: sessionCursorRuntime,
101
- ...(sessionCursorCloudAcknowledged ? { cloud: { acknowledged: true } } : {}),
102
- }
103
- : {}),
104
36
  ...(useHttp1ForAgent === undefined ? {} : { local: { useHttp1ForAgent } }),
105
37
  };
106
38
  }
@@ -128,7 +60,6 @@ export function resolveEffectiveCursorConfigForContext(ctx: CursorRuntimeContext
128
60
  export type CursorRuntimeResolution =
129
61
  | {
130
62
  kind: "valid";
131
- runtime: CursorResolvedSetting<CursorRuntime>;
132
63
  useHttp1ForAgent: CursorResolvedSetting<boolean>;
133
64
  }
134
65
  | { kind: "invalid"; message: string };
@@ -138,7 +69,6 @@ export function resolveCursorStatusRuntime(ctx: CursorRuntimeContext): CursorRun
138
69
  const config = resolveEffectiveCursorConfigForContext(ctx);
139
70
  return {
140
71
  kind: "valid",
141
- runtime: config.runtime,
142
72
  useHttp1ForAgent: config.local.useHttp1ForAgent,
143
73
  };
144
74
  } catch (error) {
@@ -146,21 +76,13 @@ export function resolveCursorStatusRuntime(ctx: CursorRuntimeContext): CursorRun
146
76
  }
147
77
  }
148
78
 
149
- export function formatResolvedCursorRuntime(runtime: CursorResolvedSetting<CursorRuntime>): string {
150
- const cap = runtime.cappedBy;
151
- return cap
152
- ? `${runtime.value} (source: ${runtime.source} safety cap over ${cap.cappedSource} ${cap.cappedValue})`
153
- : `${runtime.value} (source: ${runtime.source})`;
154
- }
155
-
156
79
  export function formatCursorStatus(
157
- runtime: CursorRuntime | "invalid",
158
80
  fast: boolean | undefined,
159
81
  mode: "agent" | "plan" | "invalid",
160
82
  useHttp1ForAgent = false,
161
83
  ): string {
162
- const parts = [`cursor:${runtime}`, fast === true ? "fast:on" : fast === false ? "fast:off" : "fast:n/a"];
163
- if (runtime === "local" && useHttp1ForAgent) parts.push("http1");
84
+ const parts = ["cursor", fast === true ? "fast:on" : fast === false ? "fast:off" : "fast:n/a"];
85
+ if (useHttp1ForAgent) parts.push("http1");
164
86
  if (mode === "invalid") parts.push("mode invalid");
165
87
  else if (mode === "plan") parts.push("plan");
166
88
  return parts.join(" · ");
@@ -183,40 +105,7 @@ export function consumeCursorLocalForceOverride(resolved: { value: boolean; sour
183
105
  return false;
184
106
  }
185
107
 
186
- export function restoreSessionCursorRuntimeState(branch: readonly SessionEntry[]): void {
187
- sessionCursorRuntime = undefined;
188
- sessionCursorCloudAcknowledged = false;
189
- for (const entry of branch) {
190
- if (entry.type !== "custom" || entry.customType !== CURSOR_RUNTIME_ENTRY_TYPE) continue;
191
- if (isCursorRuntimeEntryData(entry.data)) {
192
- sessionCursorRuntime = entry.data.runtime;
193
- sessionCursorCloudAcknowledged ||= entry.data.cloudAcknowledged === true;
194
- }
195
- }
196
- }
197
-
198
108
  export function restoreCursorCliState(pi: Pick<ExtensionAPI, "getFlag">): void {
199
- const runtime = stringFlagValue(pi.getFlag("cursor-runtime"));
200
- const repo = stringFlagValue(pi.getFlag("cursor-cloud-repo"));
201
- const branch = stringFlagValue(pi.getFlag("cursor-cloud-branch"));
202
- const contextHandoff = stringFlagValue(pi.getFlag("cursor-cloud-context"));
203
- const environmentType = stringFlagValue(pi.getFlag("cursor-cloud-env-type"));
204
- const environmentName = stringFlagValue(pi.getFlag("cursor-cloud-env-name"));
205
- const cloudEnvNames = stringFlagValue(pi.getFlag("cursor-cloud-env"));
206
- const cloud: NonNullable<CursorExplicitSdkConfig["cloud"]> = {
207
- ...(repo ? { repo } : {}),
208
- ...(branch ? { branch } : {}),
209
- ...(contextHandoff ? { contextHandoff } : {}),
210
- ...(pi.getFlag("cursor-cloud-direct-push") === true ? { directPush: true } : {}),
211
- ...(pi.getFlag("cursor-cloud-auto-create-pr") === true ? { autoCreatePR: true } : {}),
212
- ...(pi.getFlag("cursor-cloud-skip-reviewer-request") === true ? { skipReviewerRequest: true } : {}),
213
- ...(pi.getFlag("cursor-cloud-allow-local-state") === true ? { allowLocalState: true } : {}),
214
- ...(pi.getFlag("cursor-cloud-env-from-files") === true ? { envFromFiles: true } : {}),
215
- ...(environmentType || environmentName
216
- ? { environment: { ...(environmentType ? { type: environmentType } : {}), ...(environmentName ? { name: environmentName } : {}) } }
217
- : {}),
218
- ...(pi.getFlag("cursor-cloud-ack") === true ? { acknowledged: true } : {}),
219
- };
220
109
  const local: NonNullable<CursorExplicitSdkConfig["local"]> = {
221
110
  ...(pi.getFlag("cursor-auto-review") === true ? { autoReview: true } : {}),
222
111
  ...(pi.getFlag("cursor-sandbox") === true ? { sandboxOptions: { enabled: true } } : {}),
@@ -229,94 +118,12 @@ export function restoreCursorCliState(pi: Pick<ExtensionAPI, "getFlag">): void {
229
118
  };
230
119
  cliCursorSnapshot = {
231
120
  config: {
232
- ...(runtime ? { runtime } : {}),
233
- ...(Object.keys(cloud).length ? { cloud } : {}),
234
121
  ...(Object.keys(local).length ? { local } : {}),
235
122
  },
236
- ...(cloudEnvNames ? { cloudEnvNames } : {}),
237
123
  };
238
124
  }
239
125
 
240
- function persistCursorRuntimePreference(
241
- pi: Pick<ExtensionAPI, "appendEntry">,
242
- runtime: CursorRuntime,
243
- cloudAcknowledged = false,
244
- ): void {
245
- const acknowledged = sessionCursorCloudAcknowledged || cloudAcknowledged;
246
- pi.appendEntry<CursorRuntimeEntryData>(CURSOR_RUNTIME_ENTRY_TYPE, {
247
- runtime,
248
- ...(acknowledged ? { cloudAcknowledged: true } : {}),
249
- });
250
- sessionCursorRuntime = runtime;
251
- sessionCursorCloudAcknowledged = acknowledged;
252
- }
253
-
254
- function registerCursorRuntimeFlags(pi: Pick<ExtensionAPI, "registerFlag">): void {
255
- pi.registerFlag("cursor-runtime", {
256
- description: `Select Cursor runtime for this run: local or cloud (or set ${CURSOR_RUNTIME_ENV})`,
257
- type: "string",
258
- default: "",
259
- });
260
- pi.registerFlag("cursor-cloud-repo", {
261
- description: `Set Cursor cloud repository URL for this run (or set ${CURSOR_CLOUD_REPO_ENV})`,
262
- type: "string",
263
- default: "",
264
- });
265
- pi.registerFlag("cursor-cloud-branch", {
266
- description: `Set Cursor cloud branch/ref for this run (or set ${CURSOR_CLOUD_BRANCH_ENV})`,
267
- type: "string",
268
- default: "",
269
- });
270
- pi.registerFlag("cursor-cloud-context", {
271
- description: `Set Cursor cloud context handoff: never, fresh, or bootstrap (or set ${CURSOR_CLOUD_CONTEXT_ENV})`,
272
- type: "string",
273
- default: "",
274
- });
275
- pi.registerFlag("cursor-cloud-direct-push", {
276
- description: `Allow Cursor cloud direct push for this run (or set ${CURSOR_CLOUD_DIRECT_PUSH_ENV}=1)`,
277
- type: "boolean",
278
- default: false,
279
- });
280
- pi.registerFlag("cursor-cloud-auto-create-pr", {
281
- description: `Ask Cursor cloud to create a pull request for this run (or set ${CURSOR_CLOUD_AUTO_CREATE_PR_ENV}=1)`,
282
- type: "boolean",
283
- default: false,
284
- });
285
- pi.registerFlag("cursor-cloud-skip-reviewer-request", {
286
- description: `Ask Cursor cloud not to request you as a reviewer for this run (or set ${CURSOR_CLOUD_SKIP_REVIEWER_REQUEST_ENV}=1)`,
287
- type: "boolean",
288
- default: false,
289
- });
290
- pi.registerFlag("cursor-cloud-allow-local-state", {
291
- description: `Allow Cursor cloud to proceed with local-only state for this run (or set ${CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV}=1)`,
292
- type: "boolean",
293
- default: false,
294
- });
295
- pi.registerFlag("cursor-cloud-env", {
296
- description: `Reserved Cursor cloud env var names; env forwarding is not implemented yet (or set ${CURSOR_CLOUD_ENV_ENV})`,
297
- type: "string",
298
- default: "",
299
- });
300
- pi.registerFlag("cursor-cloud-env-from-files", {
301
- description: `Reserved Cursor cloud env-file forwarding flag; not implemented yet (or set ${CURSOR_CLOUD_ENV_FROM_FILES_ENV}=1)`,
302
- type: "boolean",
303
- default: false,
304
- });
305
- pi.registerFlag("cursor-cloud-env-type", {
306
- description: `Select Cursor-managed cloud environment type: cloud, pool, or machine (or set ${CURSOR_CLOUD_ENV_TYPE_ENV})`,
307
- type: "string",
308
- default: "",
309
- });
310
- pi.registerFlag("cursor-cloud-env-name", {
311
- description: `Select Cursor-managed cloud environment name (or set ${CURSOR_CLOUD_ENV_NAME_ENV})`,
312
- type: "string",
313
- default: "",
314
- });
315
- pi.registerFlag("cursor-cloud-ack", {
316
- description: `Acknowledge first-use Cursor cloud runtime risks for this run (or set ${CURSOR_CLOUD_ACK_ENV}=1)`,
317
- type: "boolean",
318
- default: false,
319
- });
126
+ export function registerCursorLocalRuntimeFlags(pi: Pick<ExtensionAPI, "registerFlag">): void {
320
127
  pi.registerFlag("cursor-auto-review", {
321
128
  description: `Enable Cursor SDK local Auto-review for this run (or set ${CURSOR_AUTO_REVIEW_ENV}=1)`,
322
129
  type: "boolean",
@@ -344,154 +151,8 @@ function registerCursorRuntimeFlags(pi: Pick<ExtensionAPI, "registerFlag">): voi
344
151
  });
345
152
  }
346
153
 
347
- async function confirmCloudAcknowledgement(
348
- ctx: Pick<ExtensionContext, "hasUI" | "ui">,
349
- alreadyAcknowledged: boolean,
350
- ): Promise<boolean> {
351
- if (alreadyAcknowledged) return true;
352
- if (!ctx.hasUI) {
353
- ctx.ui.notify(
354
- `Cursor cloud runtime requires first-use acknowledgement. Pass --cursor-cloud-ack or set ${CURSOR_CLOUD_ACK_ENV}=1, then retry.`,
355
- "error",
356
- );
357
- return false;
358
- }
359
- const confirmed = await ctx.ui.confirm("Enable Cursor Cloud runtime?", CURSOR_CLOUD_ACK_DISCLOSURE);
360
- if (!confirmed) ctx.ui.notify("Cursor cloud runtime change cancelled; no settings were written.", "info");
361
- return confirmed;
362
- }
363
-
364
- function registerCursorRuntimeCommand(
365
- pi: Pick<ExtensionAPI, "appendEntry" | "registerCommand">,
366
- refreshStatus: CursorStatusRefresh,
367
- ): void {
368
- pi.registerCommand("cursor-runtime", {
369
- description: "Set Cursor runtime for this session: local or cloud",
370
- handler: async (args, ctx) => {
371
- const usage = "Usage: /cursor-runtime local|cloud [--save-user|--save-project]";
372
- const tokens = args.trim().split(/\s+/).filter(Boolean);
373
- const raw = tokens[0];
374
- const saveUser = tokens.includes("--save-user");
375
- const saveProject = tokens.includes("--save-project");
376
- const extra = tokens.slice(1).filter((token) => token !== "--save-user" && token !== "--save-project");
377
- const currentResolution = resolveCursorStatusRuntime(ctx);
378
- if (!raw) {
379
- ctx.ui.notify(
380
- currentResolution.kind === "invalid"
381
- ? `${currentResolution.message} ${usage}`
382
- : `Cursor runtime is ${formatResolvedCursorRuntime(currentResolution.runtime)}. ${usage}`,
383
- currentResolution.kind === "invalid" ? "error" : "info",
384
- );
385
- return;
386
- }
387
- if ((raw !== "local" && raw !== "cloud") || extra.length > 0 || (saveUser && saveProject)) {
388
- ctx.ui.notify(`Invalid Cursor runtime arguments. ${usage}`, "error");
389
- return;
390
- }
391
- if (currentResolution.kind === "invalid") {
392
- ctx.ui.notify(`${currentResolution.message} Fix the explicit override before changing the session runtime.`, "error");
393
- return;
394
- }
395
- if (saveProject && (getCursorSessionCwd() !== ctx.cwd || !getCursorSessionProjectTrusted())) {
396
- ctx.ui.notify(
397
- "Cannot save Cursor project config without explicit project-trust provenance. Ensure .pi/settings.json or another Pi project resource exists, trust the project, then restart pi; or restart with --approve. Project-local package installs must use --approve on every run that reads or writes .pi/cursor-sdk.json.",
398
- "error",
399
- );
400
- return;
401
- }
402
- const cloudAcknowledged = raw === "cloud" && await confirmCloudAcknowledgement(
403
- ctx,
404
- resolveEffectiveCursorConfigForContext(ctx).cloud.acknowledged.value,
405
- );
406
- if (raw === "cloud" && !cloudAcknowledged) return;
407
- if (saveUser || saveProject) {
408
- try {
409
- if (saveUser) {
410
- updateCursorSdkConfig(
411
- getCursorSdkUserConfigPath(),
412
- (current) => mergeCursorSdkConfigForUpdate(current, {
413
- runtime: raw,
414
- ...(cloudAcknowledged ? { cloud: { acknowledged: true } } : {}),
415
- }),
416
- { newFileMode: 0o600 },
417
- );
418
- } else {
419
- updateCursorSdkConfig(
420
- getCursorSdkProjectConfigPath(ctx.cwd),
421
- (current) => mergeCursorSdkConfigForUpdate(current, { runtime: raw }),
422
- );
423
- }
424
- } catch (error) {
425
- const effectiveResolution = resolveCursorStatusRuntime(ctx);
426
- refreshStatus(ctx);
427
- const effective = effectiveResolution.kind === "valid"
428
- ? ` Effective runtime remains ${formatResolvedCursorRuntime(effectiveResolution.runtime)}.`
429
- : ` ${effectiveResolution.message}`;
430
- ctx.ui.notify(
431
- `Failed to save Cursor runtime preference to ${saveUser ? "user" : "project"} config: ${error instanceof Error ? error.message : String(error)}.${effective}`,
432
- "error",
433
- );
434
- return;
435
- }
436
- }
437
- try {
438
- persistCursorRuntimePreference(pi, raw, cloudAcknowledged);
439
- } catch (error) {
440
- const effectiveResolution = resolveCursorStatusRuntime(ctx);
441
- refreshStatus(ctx);
442
- const effective = effectiveResolution.kind === "valid"
443
- ? ` Effective runtime is ${formatResolvedCursorRuntime(effectiveResolution.runtime)}.`
444
- : ` ${effectiveResolution.message}`;
445
- const persisted = saveUser
446
- ? "User config was saved, but persisting the session runtime entry failed."
447
- : saveProject
448
- ? "Project config was saved, but persisting the session runtime entry failed."
449
- : "Persisting the session runtime entry failed.";
450
- ctx.ui.notify(
451
- `${persisted} ${error instanceof Error ? error.message : String(error)}.${effective}`,
452
- "error",
453
- );
454
- return;
455
- }
456
- const effectiveResolution = resolveCursorStatusRuntime(ctx);
457
- refreshStatus(ctx);
458
- const saved = saveUser
459
- ? " Saved to user config."
460
- : saveProject
461
- ? " Saved to project config; cloud acknowledgement remains session/user-scoped."
462
- : "";
463
- const effective = effectiveResolution.kind === "valid"
464
- ? ` Effective runtime is ${formatResolvedCursorRuntime(effectiveResolution.runtime)}.`
465
- : ` ${effectiveResolution.message}`;
466
- ctx.ui.notify(
467
- raw === "cloud"
468
- ? `Cursor runtime request saved for this session and cloud risk acknowledged.${effective}${saved}`
469
- : `Cursor runtime request saved for this session.${effective}${saved}`,
470
- "info",
471
- );
472
- },
473
- });
474
- }
475
-
476
- export function registerCursorCloudRuntimeControls(
477
- pi: CursorRuntimeStateExtensionApi,
478
- options: { refreshStatus: CursorStatusRefresh },
479
- ): void {
480
- registerCursorCloudLifecycleLedger(pi);
481
- registerCursorRuntimeFlags(pi);
482
- registerCursorRuntimeCommand(pi, options.refreshStatus);
483
- pi.registerCommand("cursor-cloud", {
484
- description: "List, archive, or delete recorded Cursor cloud agents for this session branch",
485
- handler: async (args, ctx) => {
486
- await runCursorCloudLifecycleCommand(pi, args, ctx);
487
- },
488
- });
489
- }
490
-
491
154
  export function resetCursorRuntimeStateForTests(): void {
492
155
  cliCursorSnapshot = { config: {} };
493
156
  cliLocalForceConsumed = false;
494
157
  envLocalForceConsumed = false;
495
- sessionCursorRuntime = undefined;
496
- sessionCursorCloudAcknowledged = false;
497
158
  }
@@ -1,5 +1,4 @@
1
1
  import type { SDKAgent } from "@cursor/sdk";
2
- import type { CursorRuntime } from "./cursor-config.js";
3
2
  import { asRecord, getArray, getString } from "./cursor-record-utils.js";
4
3
  import { readCursorSdkTurnUsage, type CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
5
4
 
@@ -9,10 +8,6 @@ const BILLED_USAGE_TIMEOUT_MS = 5000;
9
8
  // Upgrade to session-scoped storage if multi-day processes retain enough agent IDs to matter.
10
9
  const seenBilledRunIdsByAgent = new Map<string, Set<string>>();
11
10
 
12
- export function isCursorSdkClientMintedRunId(runId: string): boolean {
13
- return runId.startsWith("run-");
14
- }
15
-
16
11
  export function sumCursorSdkTurnUsage(usages: readonly CursorSdkTurnUsage[]): CursorSdkTurnUsage | undefined {
17
12
  if (usages.length === 0) return undefined;
18
13
  return usages.reduce(
@@ -52,7 +47,7 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs = BILLED_USAGE_TIMEOUT_MS
52
47
 
53
48
  export function selectCursorBilledTurnUsage(
54
49
  agentUsage: unknown,
55
- options: { runtime: CursorRuntime; runId?: string; seenRunIds?: ReadonlySet<string> },
50
+ options: { seenRunIds?: ReadonlySet<string> },
56
51
  ): { turn?: CursorSdkTurnUsage; runIds: string[] } {
57
52
  const runs = (getArray(asRecord(agentUsage), "runs") ?? []).flatMap((item) => {
58
53
  const record = asRecord(item);
@@ -60,40 +55,24 @@ export function selectCursorBilledTurnUsage(
60
55
  const usage = readCursorSdkTurnUsage(record?.usage);
61
56
  return runId && usage ? [{ runId, usage }] : [];
62
57
  });
63
- if (options.runtime === "cloud" && options.runId) {
64
- const match = runs.find((run) => run.runId === options.runId);
65
- return match ? { turn: match.usage, runIds: [match.runId] } : { runIds: [] };
66
- }
67
58
  const unseen = runs.filter((run) => !options.seenRunIds?.has(run.runId));
68
59
  return { turn: sumCursorSdkTurnUsage(unseen.map((run) => run.usage)), runIds: unseen.map((run) => run.runId) };
69
60
  }
70
61
 
71
62
  export async function fetchCursorSdkAgentUsage(
72
63
  agent: SDKAgent,
73
- options: { runtime: CursorRuntime; runId?: string },
74
64
  ): Promise<unknown | undefined> {
75
65
  if (typeof agent.getUsage !== "function") return undefined;
76
- const query =
77
- options.runtime === "cloud" && options.runId && isCursorSdkClientMintedRunId(options.runId)
78
- ? { runId: options.runId }
79
- : undefined;
80
- return withTimeout(Promise.resolve(agent.getUsage(query)));
66
+ return withTimeout(Promise.resolve(agent.getUsage()));
81
67
  }
82
68
 
83
69
  export async function attachCursorSdkBilledTurnUsage(options: {
84
70
  agent: SDKAgent;
85
71
  agentId: string;
86
- runtime: CursorRuntime;
87
- runId?: string;
88
72
  }): Promise<{ agentUsage?: unknown; turn?: CursorSdkTurnUsage }> {
89
- const agentUsage = await fetchCursorSdkAgentUsage(options.agent, {
90
- runtime: options.runtime,
91
- runId: options.runId,
92
- });
73
+ const agentUsage = await fetchCursorSdkAgentUsage(options.agent);
93
74
  if (!agentUsage) return {};
94
75
  const selected = selectCursorBilledTurnUsage(agentUsage, {
95
- runtime: options.runtime,
96
- runId: options.runtime === "cloud" ? options.runId : undefined,
97
76
  seenRunIds: peekCursorBilledUsageRunIds(options.agentId),
98
77
  });
99
78
  rememberCursorBilledUsageRunIds(options.agentId, selected.runIds);
@@ -0,0 +1,106 @@
1
+ import { accessSync, constants, existsSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, isAbsolute, join } from "node:path";
4
+
5
+ const RIPGREP_ENV = "CURSOR_RIPGREP_PATH";
6
+ const TREE_SITTER_VENDOR_ENV = "CURSOR_TREE_SITTER_VENDOR_DIR";
7
+
8
+ function cursorSdkPlatformPackageName(): string {
9
+ return `@cursor/sdk-${process.platform}-${process.arch}`;
10
+ }
11
+
12
+ function resolveCursorSdkPlatformPackageDir(
13
+ fromModuleUrl: string | URL = import.meta.url,
14
+ ): string | undefined {
15
+ try {
16
+ const require = createRequire(fromModuleUrl);
17
+ const sdkEntry = require.resolve("@cursor/sdk");
18
+ return dirname(
19
+ require.resolve(`${cursorSdkPlatformPackageName()}/package.json`, {
20
+ paths: [dirname(sdkEntry)],
21
+ }),
22
+ );
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ }
27
+
28
+ function resolveCursorSdkPlatformBinary(
29
+ binaryName: string,
30
+ fromModuleUrl: string | URL = import.meta.url,
31
+ ): string | undefined {
32
+ try {
33
+ const packageDirectory = resolveCursorSdkPlatformPackageDir(fromModuleUrl);
34
+ if (!packageDirectory) return undefined;
35
+ const binaryPath = join(packageDirectory, "bin", binaryName);
36
+ accessSync(binaryPath, constants.X_OK);
37
+ return binaryPath;
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ export function resolveBundledCursorRipgrepPath(
44
+ fromModuleUrl: string | URL = import.meta.url,
45
+ ): string | undefined {
46
+ return resolveCursorSdkPlatformBinary(process.platform === "win32" ? "rg.exe" : "rg", fromModuleUrl);
47
+ }
48
+
49
+ export function ensureCursorRipgrepPath(): string | undefined {
50
+ const configuredPath = process.env[RIPGREP_ENV];
51
+ if (configuredPath && isAbsolute(configuredPath)) return configuredPath;
52
+
53
+ const bundledPath = resolveBundledCursorRipgrepPath();
54
+ if (bundledPath) process.env[RIPGREP_ENV] = bundledPath;
55
+ return bundledPath;
56
+ }
57
+
58
+ export function resolveBundledCursorTreeSitterVendorDir(
59
+ fromModuleUrl: string | URL = import.meta.url,
60
+ ): string | undefined {
61
+ const packageDirectory = resolveCursorSdkPlatformPackageDir(fromModuleUrl);
62
+ if (!packageDirectory) return undefined;
63
+ const vendorDir = join(packageDirectory, "vendor");
64
+ if (!existsSync(join(vendorDir, "tree-sitter", "index.js"))) return undefined;
65
+ return vendorDir;
66
+ }
67
+
68
+ export function ensureCursorTreeSitterVendorDir(): string | undefined {
69
+ const configuredPath = process.env[TREE_SITTER_VENDOR_ENV];
70
+ if (configuredPath && isAbsolute(configuredPath)) return configuredPath;
71
+
72
+ const bundledPath = resolveBundledCursorTreeSitterVendorDir();
73
+ if (bundledPath) process.env[TREE_SITTER_VENDOR_ENV] = bundledPath;
74
+ return bundledPath;
75
+ }
76
+
77
+ export function resolveBundledCursorSandboxPath(
78
+ fromModuleUrl: string | URL = import.meta.url,
79
+ ): string | undefined {
80
+ return resolveCursorSdkPlatformBinary(
81
+ process.platform === "win32" ? "cursorsandbox.exe" : "cursorsandbox",
82
+ fromModuleUrl,
83
+ );
84
+ }
85
+
86
+ /**
87
+ * The SDK finds `cursorsandbox` by walking from `process.argv[1]`, not from
88
+ * `require.resolve`. Point that walk at the installed platform package for the
89
+ * duration of local `Agent.create` / `Agent.resume`.
90
+ */
91
+ export async function runWithCursorSdkPlatformPackageVisible<T>(
92
+ run: () => Promise<T>,
93
+ fromModuleUrl: string | URL = import.meta.url,
94
+ ): Promise<T> {
95
+ const packageDirectory = resolveCursorSdkPlatformPackageDir(fromModuleUrl);
96
+ if (!packageDirectory) return run();
97
+ const hostEntry = join(packageDirectory, "package.json");
98
+ if (!existsSync(hostEntry)) return run();
99
+ const previousArgv1 = process.argv[1];
100
+ process.argv[1] = hostEntry;
101
+ try {
102
+ return await run();
103
+ } finally {
104
+ if (process.argv[1] === hostEntry) process.argv[1] = previousArgv1;
105
+ }
106
+ }
@@ -14,6 +14,7 @@ import {
14
14
  } from "./cursor-session-agent-resume.js";
15
15
  import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
16
16
  import { loadCursorSdk, type CursorSdkModule } from "./cursor-sdk-runtime.js";
17
+ import { runWithCursorSdkPlatformPackageVisible } from "./cursor-sdk-platform-package.js";
17
18
  import {
18
19
  cursorSessionStoreIdentitiesEqual,
19
20
  openCursorSessionStore,
@@ -501,20 +502,23 @@ async function createSessionAgentEntry(
501
502
  let agent: SDKAgent | undefined;
502
503
  let effectiveSendState = sendState;
503
504
  let resumed = false;
504
- if (resumeHandle && resumeAttemptAllowed && resumeAgent) {
505
- try {
506
- agent = await resumeAgent(resumeHandle.agentId, buildAgentOptions());
507
- effectiveSendState = { ...resumeHandle.sendState };
508
- resumed = true;
509
- } catch {
510
- if (persistentStore) resumeNotice = LOCAL_RESUME_FALLBACK_NOTICE;
511
- if (!cursorSessionStoreIdentitiesEqual(sessionStore.identity, identities.sessionStore)) {
512
- await sessionStore.dispose().catch(() => undefined);
513
- sessionStore = await openCursorSessionStore(params.cwd, identities.sessionStore);
505
+ await runWithCursorSdkPlatformPackageVisible(async () => {
506
+ if (resumeHandle && resumeAttemptAllowed && resumeAgent) {
507
+ try {
508
+ agent = await resumeAgent(resumeHandle.agentId, buildAgentOptions());
509
+ effectiveSendState = { ...resumeHandle.sendState };
510
+ resumed = true;
511
+ } catch {
512
+ if (persistentStore) resumeNotice = LOCAL_RESUME_FALLBACK_NOTICE;
513
+ const currentStore = sessionStore;
514
+ if (currentStore && !cursorSessionStoreIdentitiesEqual(currentStore.identity, identities.sessionStore)) {
515
+ await currentStore.dispose().catch(() => undefined);
516
+ sessionStore = await openCursorSessionStore(params.cwd, identities.sessionStore);
517
+ }
514
518
  }
515
519
  }
516
- }
517
- agent ??= await createAgent(buildAgentOptions());
520
+ agent ??= await createAgent(buildAgentOptions());
521
+ });
518
522
  if (!agent) throw new Error("Cursor SDK agent creation returned no agent");
519
523
  if (!sessionStore) throw new Error("Cursor SDK session store was not opened");
520
524