@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
@@ -11,7 +11,6 @@ import {
11
11
  resolveCursorSettingSources,
12
12
  } from "./cursor-setting-sources.js";
13
13
  import type { SettingSource } from "@cursor/sdk";
14
- import type { CursorRuntime } from "./cursor-config.js";
15
14
  export const CURSOR_PRESERVE_PI_AGENTS_MD_ENV = "PI_CURSOR_PRESERVE_PI_AGENTS_MD";
16
15
 
17
16
  /** Opening tag prefix pi `buildSystemPrompt()` uses for each context file (path attribute only). */
@@ -152,9 +151,8 @@ export function resolveCursorFacingSystemPrompt(
152
151
  systemPromptOptions?: BuildSystemPromptOptions,
153
152
  settingSourcesRaw?: string,
154
153
  agentDir?: string,
155
- runtime: CursorRuntime = "local",
156
154
  ): string {
157
- if (runtime === "cloud" || !systemPromptOptions) return systemPrompt;
155
+ if (!systemPromptOptions) return systemPrompt;
158
156
  const contextFiles = systemPromptOptions.contextFiles ?? [];
159
157
  const settingSources =
160
158
  settingSourcesRaw === undefined
@@ -18,19 +18,6 @@ import { asRecord } from "./cursor-record-utils.js";
18
18
 
19
19
  export const CURSOR_SDK_CONFIG_FILE = "cursor-sdk.json";
20
20
 
21
- export const CURSOR_RUNTIME_ENV = "PI_CURSOR_RUNTIME";
22
- export const CURSOR_CLOUD_REPO_ENV = "PI_CURSOR_CLOUD_REPO";
23
- export const CURSOR_CLOUD_BRANCH_ENV = "PI_CURSOR_CLOUD_BRANCH";
24
- export const CURSOR_CLOUD_CONTEXT_ENV = "PI_CURSOR_CLOUD_CONTEXT";
25
- export const CURSOR_CLOUD_DIRECT_PUSH_ENV = "PI_CURSOR_CLOUD_DIRECT_PUSH";
26
- export const CURSOR_CLOUD_AUTO_CREATE_PR_ENV = "PI_CURSOR_CLOUD_AUTO_CREATE_PR";
27
- export const CURSOR_CLOUD_SKIP_REVIEWER_REQUEST_ENV = "PI_CURSOR_CLOUD_SKIP_REVIEWER_REQUEST";
28
- export const CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV = "PI_CURSOR_CLOUD_ALLOW_LOCAL_STATE";
29
- export const CURSOR_CLOUD_ENV_ENV = "PI_CURSOR_CLOUD_ENV";
30
- export const CURSOR_CLOUD_ENV_FROM_FILES_ENV = "PI_CURSOR_CLOUD_ENV_FROM_FILES";
31
- export const CURSOR_CLOUD_ENV_TYPE_ENV = "PI_CURSOR_CLOUD_ENV_TYPE";
32
- export const CURSOR_CLOUD_ENV_NAME_ENV = "PI_CURSOR_CLOUD_ENV_NAME";
33
- export const CURSOR_CLOUD_ACK_ENV = "PI_CURSOR_CLOUD_ACK";
34
21
  export const CURSOR_AUTO_REVIEW_ENV = "PI_CURSOR_AUTO_REVIEW";
35
22
  export const CURSOR_SANDBOX_ENV = "PI_CURSOR_SANDBOX";
36
23
  export const CURSOR_LOCAL_FORCE_ENV = "PI_CURSOR_LOCAL_FORCE";
@@ -39,31 +26,9 @@ export const CURSOR_HTTP1_ENV = "PI_CURSOR_HTTP_1_1";
39
26
 
40
27
  export type CursorConfigSource = "cli" | "environment" | "project" | "user" | "session" | "model-alias" | "builtin";
41
28
  export type CursorConfigTrustLevel = "one-shot" | "environment" | "trusted-project" | "user" | "session" | "model-catalog" | "builtin";
42
- export type CursorRuntime = "local" | "cloud";
43
- export type CursorCloudContextHandoff = "never" | "fresh" | "bootstrap";
44
- export type CursorCloudEnvironmentType = "cloud" | "pool" | "machine";
45
-
46
- export interface CursorCloudEnvironmentConfig {
47
- type?: CursorCloudEnvironmentType | string;
48
- name?: string;
49
- }
50
29
 
51
30
  export interface CursorSdkConfig {
52
31
  fastDefaults?: Record<string, boolean>;
53
- runtime?: CursorRuntime;
54
- cloud?: {
55
- repo?: string;
56
- branch?: string;
57
- contextHandoff?: CursorCloudContextHandoff;
58
- directPush?: boolean;
59
- autoCreatePR?: boolean;
60
- skipReviewerRequest?: boolean;
61
- allowLocalState?: boolean;
62
- envNames?: string[];
63
- envFromFiles?: boolean;
64
- environment?: CursorCloudEnvironmentConfig;
65
- acknowledged?: boolean;
66
- };
67
32
  local?: {
68
33
  autoReview?: boolean;
69
34
  sandbox?: boolean;
@@ -80,37 +45,13 @@ export interface CursorSdkConfig {
80
45
  };
81
46
  }
82
47
 
83
- export interface CursorSafetyCap<T> {
84
- source: CursorConfigSource;
85
- trustLevel: CursorConfigTrustLevel;
86
- value: T;
87
- cappedSource: CursorConfigSource;
88
- cappedValue: T;
89
- reason: "safer-source";
90
- }
91
-
92
48
  export interface CursorResolvedSetting<T> {
93
49
  value: T;
94
50
  source: CursorConfigSource;
95
51
  trustLevel: CursorConfigTrustLevel;
96
- cappedBy?: CursorSafetyCap<T>;
97
52
  }
98
53
 
99
54
  export interface CursorResolvedSdkConfig {
100
- runtime: CursorResolvedSetting<CursorRuntime>;
101
- cloud: {
102
- repo: CursorResolvedSetting<string | undefined>;
103
- branch: CursorResolvedSetting<string | undefined>;
104
- contextHandoff: CursorResolvedSetting<CursorCloudContextHandoff>;
105
- directPush: CursorResolvedSetting<boolean>;
106
- autoCreatePR: CursorResolvedSetting<boolean>;
107
- skipReviewerRequest: CursorResolvedSetting<boolean>;
108
- allowLocalState: CursorResolvedSetting<boolean>;
109
- envNames: CursorResolvedSetting<string[]>;
110
- envFromFiles: CursorResolvedSetting<boolean>;
111
- environment: CursorResolvedSetting<CursorCloudEnvironmentConfig | undefined>;
112
- acknowledged: CursorResolvedSetting<boolean>;
113
- };
114
55
  local: {
115
56
  autoReview: CursorResolvedSetting<boolean>;
116
57
  sandboxEnabled: CursorResolvedSetting<boolean>;
@@ -123,9 +64,9 @@ export interface CursorResolvedSdkConfig {
123
64
  };
124
65
  }
125
66
 
126
- // Widens string-literal-union fields (e.g. runtime, contextHandoff) to raw `string` so CLI callers can
127
- // pass unvalidated input through to validateExplicitValue, while keeping every other field's shape linked
128
- // to CursorSdkConfig so new fields don't need a manually maintained parallel type.
67
+ // Widens string-literal-union fields to raw `string` so CLI callers can pass unvalidated input through to
68
+ // validateExplicitValue, while keeping every other field's shape linked to CursorSdkConfig so new fields
69
+ // don't need a manually maintained parallel type.
129
70
  type WidenLiterals<T> = T extends string
130
71
  ? string
131
72
  : T extends readonly (infer U)[]
@@ -142,7 +83,6 @@ export interface ResolveCursorSdkConfigOptions {
142
83
  session?: CursorSdkConfig;
143
84
  project?: CursorSdkConfig;
144
85
  user?: CursorSdkConfig;
145
- builtIn?: CursorSdkConfig;
146
86
  }
147
87
 
148
88
  export interface LoadCursorSdkConfigOptions {
@@ -161,33 +101,6 @@ const TRUST_LEVELS: Record<CursorConfigSource, CursorConfigTrustLevel> = {
161
101
  builtin: "builtin",
162
102
  };
163
103
 
164
- const BUILT_IN_CURSOR_CONFIG: Required<Pick<CursorSdkConfig, "runtime">> & {
165
- cloud: Required<NonNullable<CursorSdkConfig["cloud"]>>;
166
- } = {
167
- runtime: "local",
168
- cloud: {
169
- repo: "",
170
- branch: "",
171
- contextHandoff: "fresh",
172
- directPush: false,
173
- autoCreatePR: false,
174
- skipReviewerRequest: false,
175
- allowLocalState: false,
176
- envNames: [],
177
- envFromFiles: false,
178
- environment: {},
179
- acknowledged: false,
180
- },
181
- };
182
-
183
- export function isCursorRuntime(value: unknown): value is CursorRuntime {
184
- return value === "local" || value === "cloud";
185
- }
186
-
187
- export function isCursorCloudContextHandoff(value: unknown): value is CursorCloudContextHandoff {
188
- return value === "never" || value === "fresh" || value === "bootstrap";
189
- }
190
-
191
104
  function validateExplicitValue<T extends string>(
192
105
  raw: string | undefined,
193
106
  name: string,
@@ -200,62 +113,23 @@ function validateExplicitValue<T extends string>(
200
113
  return value;
201
114
  }
202
115
 
203
- export function isCursorCloudEnvironmentType(value: unknown): value is CursorCloudEnvironmentType {
204
- return value === "cloud" || value === "pool" || value === "machine";
205
- }
206
-
207
116
  function parseNonEmptyString(value: unknown): string | undefined {
208
117
  if (typeof value !== "string") return undefined;
209
118
  const trimmed = value.trim();
210
119
  return trimmed ? trimmed : undefined;
211
120
  }
212
121
 
213
- function isAllowedEnvName(value: unknown): value is string {
214
- return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value) && !value.startsWith("CURSOR_");
215
- }
216
-
217
122
  function parseToolNames(value: unknown): string[] | undefined {
218
123
  if (!Array.isArray(value)) return undefined;
219
124
  const parsed = [...new Set(value.map((name) => (typeof name === "string" ? name.trim() : "")).filter((name) => name.length > 0))];
220
125
  return parsed.length > 0 ? parsed : undefined;
221
126
  }
222
- function parseEnvNames(value: unknown): string[] | undefined {
223
- const names = Array.isArray(value)
224
- ? value
225
- : typeof value === "string"
226
- ? value.split(",")
227
- : undefined;
228
- if (!names) return undefined;
229
- const parsed = [...new Set(names.map((name) => (typeof name === "string" ? name.trim() : "")).filter(isAllowedEnvName))];
230
- return parsed.length > 0 || (Array.isArray(value) && value.length === 0) ? parsed : undefined;
231
- }
232
-
233
- export function parseExplicitCursorCloudEnvNames(value: string | undefined, name: string): string[] | undefined {
234
- const request = parseNonEmptyString(value);
235
- if (!request) return undefined;
236
- const parsed = parseEnvNames(request);
237
- if (!parsed?.length) throw new Error(`Invalid ${name}: no valid environment variable names were requested.`);
238
- return parsed;
239
- }
240
-
241
- function parseCloudEnvironment(value: unknown): CursorCloudEnvironmentConfig | undefined {
242
- const environment = asRecord(value);
243
- if (!environment) return undefined;
244
- const parsed: CursorCloudEnvironmentConfig = {};
245
- const rawType = parseNonEmptyString(environment.type);
246
- const name = parseNonEmptyString(environment.name);
247
- if (rawType) parsed.type = rawType;
248
- if (name) parsed.name = name;
249
- return Object.keys(parsed).length > 0 ? parsed : undefined;
250
- }
251
127
 
252
128
  export function parseCursorSdkConfig(value: unknown): CursorSdkConfig | undefined {
253
129
  const record = asRecord(value);
254
130
  if (!record) return undefined;
255
131
  const config: CursorSdkConfig = {};
256
132
 
257
- if (isCursorRuntime(record.runtime)) config.runtime = record.runtime;
258
-
259
133
  const fastDefaults = asRecord(record.fastDefaults);
260
134
  if (fastDefaults) {
261
135
  config.fastDefaults = Object.fromEntries(
@@ -263,27 +137,6 @@ export function parseCursorSdkConfig(value: unknown): CursorSdkConfig | undefine
263
137
  );
264
138
  }
265
139
 
266
- const cloud = asRecord(record.cloud);
267
- if (cloud) {
268
- const parsedCloud: NonNullable<CursorSdkConfig["cloud"]> = {};
269
- const repo = parseNonEmptyString(cloud.repo);
270
- const branch = parseNonEmptyString(cloud.branch);
271
- const envNames = parseEnvNames(cloud.envNames);
272
- const environment = parseCloudEnvironment(cloud.environment);
273
- if (repo) parsedCloud.repo = repo;
274
- if (branch) parsedCloud.branch = branch;
275
- if (isCursorCloudContextHandoff(cloud.contextHandoff)) parsedCloud.contextHandoff = cloud.contextHandoff;
276
- if (typeof cloud.directPush === "boolean") parsedCloud.directPush = cloud.directPush;
277
- if (typeof cloud.autoCreatePR === "boolean") parsedCloud.autoCreatePR = cloud.autoCreatePR;
278
- if (typeof cloud.skipReviewerRequest === "boolean") parsedCloud.skipReviewerRequest = cloud.skipReviewerRequest;
279
- if (typeof cloud.allowLocalState === "boolean") parsedCloud.allowLocalState = cloud.allowLocalState;
280
- if (envNames) parsedCloud.envNames = envNames;
281
- if (typeof cloud.envFromFiles === "boolean") parsedCloud.envFromFiles = cloud.envFromFiles;
282
- if (environment) parsedCloud.environment = environment;
283
- if (typeof cloud.acknowledged === "boolean") parsedCloud.acknowledged = cloud.acknowledged;
284
- if (Object.keys(parsedCloud).length > 0) config.cloud = parsedCloud;
285
- }
286
-
287
140
  const local = asRecord(record.local);
288
141
  if (local) {
289
142
  const parsedLocal: NonNullable<CursorSdkConfig["local"]> = {};
@@ -453,15 +306,11 @@ export function mergeCursorSdkConfigForUpdate(
453
306
  base: Record<string, unknown>,
454
307
  patch: CursorSdkConfig,
455
308
  ): Record<string, unknown> {
456
- const baseCloud = asRecord(base.cloud);
457
309
  const baseLocal = asRecord(base.local);
458
310
  const baseSandboxOptions = asRecord(baseLocal?.sandboxOptions);
459
311
  return {
460
312
  ...base,
461
313
  ...patch,
462
- ...(baseCloud || patch.cloud
463
- ? { cloud: { ...baseCloud, ...patch.cloud } }
464
- : {}),
465
314
  ...(baseLocal || patch.local
466
315
  ? {
467
316
  local: {
@@ -490,8 +339,8 @@ export function withCursorFastDefaults<T extends object>(
490
339
  };
491
340
  }
492
341
 
493
- function resolved<T>(source: CursorConfigSource, value: T, cappedBy?: CursorSafetyCap<T>): CursorResolvedSetting<T> {
494
- return { value, source, trustLevel: TRUST_LEVELS[source], ...(cappedBy ? { cappedBy } : {}) };
342
+ function resolved<T>(source: CursorConfigSource, value: T): CursorResolvedSetting<T> {
343
+ return { value, source, trustLevel: TRUST_LEVELS[source] };
495
344
  }
496
345
 
497
346
  function valueFrom<T>(source: CursorConfigSource, value: T | undefined): CursorResolvedSetting<T> | undefined {
@@ -504,50 +353,13 @@ function resolveOrdinary<T>(layers: Array<CursorResolvedSetting<T> | undefined>)
504
353
  return match;
505
354
  }
506
355
 
507
- // One cap engine for every safety-capped field. `cap` receives the candidate cap layer's raw value and the
508
- // resolved base setting; it returns the value to use once capped, or undefined to mean "don't cap". CLI is
509
- // always uncapped (one-shot, explicit operator intent).
510
- function resolveWithCap<T>(
511
- baseLayers: Array<CursorResolvedSetting<T> | undefined>,
512
- capLayer: CursorResolvedSetting<T> | undefined,
513
- cap: (candidate: T, base: CursorResolvedSetting<T>) => T | undefined,
514
- ): CursorResolvedSetting<T> {
515
- const cli = baseLayers[0];
516
- if (cli) return cli;
517
- const base = resolveOrdinary(baseLayers.slice(1));
518
- if (!capLayer) return base;
519
- const cappedValue = cap(capLayer.value, base);
520
- if (cappedValue === undefined) return base;
521
- return resolved(capLayer.source, cappedValue, {
522
- source: capLayer.source,
523
- trustLevel: capLayer.trustLevel,
524
- value: cappedValue,
525
- cappedSource: base.source,
526
- cappedValue: base.value,
527
- reason: "safer-source",
528
- });
529
- }
530
-
531
- function scalarRiskCap<T>(risk: (value: T) => number): (candidate: T, base: CursorResolvedSetting<T>) => T | undefined {
532
- return (candidate, base) => (risk(candidate) < risk(base.value) ? candidate : undefined);
533
- }
534
-
535
- function envNamesCap(candidate: string[], base: CursorResolvedSetting<string[]>): string[] | undefined {
536
- if (base.source === "user") return undefined;
537
- const allowed = new Set(candidate);
538
- const filtered = base.value.filter((name) => allowed.has(name));
539
- return filtered.length === base.value.length ? undefined : filtered;
540
- }
541
-
542
356
  // Field/layer resolver table: each field declares the ordered list of sources it participates in (its
543
- // precedence), plus its per-layer values. Sources omitted from a field's order (e.g. cloud fields skip
544
- // "project", local fields skip "session", force skips both) are the field-specific omissions the review
545
- // asked to keep explicit; test/cursor-config.test.ts asserts each one.
357
+ // precedence), plus its per-layer values. Sources omitted from a field's order (e.g. local fields skip
358
+ // "session", force skips project/user/session) are the field-specific omissions the review asked to keep
359
+ // explicit; test/cursor-config.test.ts asserts each one.
546
360
  type CursorFieldSource = Exclude<CursorConfigSource, "model-alias">;
547
361
  type CursorFieldValues<T> = Partial<Record<CursorFieldSource, T>>;
548
362
 
549
- const RUNTIME_ORDER: CursorFieldSource[] = ["cli", "environment", "session", "project", "user", "builtin"];
550
- const CLOUD_ORDER: CursorFieldSource[] = ["cli", "environment", "session", "user", "builtin"];
551
363
  const LOCAL_ORDER: CursorFieldSource[] = ["cli", "environment", "project", "user", "builtin"];
552
364
  const LOCAL_FORCE_ORDER: CursorFieldSource[] = ["cli", "environment", "builtin"];
553
365
  const HTTP1_ORDER: CursorFieldSource[] = ["session", "environment", "user", "builtin"];
@@ -561,64 +373,8 @@ function resolveOrdinaryField<T>(order: CursorFieldSource[], values: CursorField
561
373
  return resolveOrdinary(buildFieldLayers(order, values));
562
374
  }
563
375
 
564
- function resolveSafetyField<T>(order: CursorFieldSource[], values: CursorFieldValues<T>, risk: (value: T) => number): CursorResolvedSetting<T> {
565
- return resolveWithCap(buildFieldLayers(order, values), valueFrom("user", values.user), scalarRiskCap(risk));
566
- }
567
-
568
- function resolveEnvNamesSafetyField(order: CursorFieldSource[], values: CursorFieldValues<string[]>): CursorResolvedSetting<string[]> {
569
- return resolveWithCap(buildFieldLayers(order, values), valueFrom("user", values.user), envNamesCap);
570
- }
571
-
572
376
  export function cursorSdkConfigFromEnv(env: Record<string, string | undefined> = process.env): CursorSdkConfig {
573
377
  const config: CursorSdkConfig = {};
574
- const runtime = validateExplicitValue(env[CURSOR_RUNTIME_ENV], CURSOR_RUNTIME_ENV, isCursorRuntime, '"local" or "cloud"');
575
- if (runtime) config.runtime = runtime;
576
- const repo = parseNonEmptyString(env[CURSOR_CLOUD_REPO_ENV]);
577
- const branch = parseNonEmptyString(env[CURSOR_CLOUD_BRANCH_ENV]);
578
- const contextHandoff = validateExplicitValue(
579
- env[CURSOR_CLOUD_CONTEXT_ENV],
580
- CURSOR_CLOUD_CONTEXT_ENV,
581
- isCursorCloudContextHandoff,
582
- '"never", "fresh", or "bootstrap"',
583
- );
584
- const directPush = parseOptionalEnvBoolean(env[CURSOR_CLOUD_DIRECT_PUSH_ENV]);
585
- const autoCreatePR = parseOptionalEnvBoolean(env[CURSOR_CLOUD_AUTO_CREATE_PR_ENV]);
586
- const skipReviewerRequest = parseOptionalEnvBoolean(env[CURSOR_CLOUD_SKIP_REVIEWER_REQUEST_ENV]);
587
- const allowLocalState = parseOptionalEnvBoolean(env[CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV]);
588
- const envNames = parseExplicitCursorCloudEnvNames(env[CURSOR_CLOUD_ENV_ENV], CURSOR_CLOUD_ENV_ENV);
589
- const envFromFiles = parseOptionalEnvBoolean(env[CURSOR_CLOUD_ENV_FROM_FILES_ENV]);
590
- const environment = parseCloudEnvironment({
591
- type: env[CURSOR_CLOUD_ENV_TYPE_ENV],
592
- name: env[CURSOR_CLOUD_ENV_NAME_ENV],
593
- });
594
- const acknowledged = parseOptionalEnvBoolean(env[CURSOR_CLOUD_ACK_ENV]);
595
- if (
596
- repo !== undefined ||
597
- branch !== undefined ||
598
- isCursorCloudContextHandoff(contextHandoff) ||
599
- directPush !== undefined ||
600
- autoCreatePR !== undefined ||
601
- skipReviewerRequest !== undefined ||
602
- allowLocalState !== undefined ||
603
- envNames !== undefined ||
604
- envFromFiles !== undefined ||
605
- environment !== undefined ||
606
- acknowledged !== undefined
607
- ) {
608
- config.cloud = {
609
- ...(repo !== undefined ? { repo } : {}),
610
- ...(branch !== undefined ? { branch } : {}),
611
- ...(isCursorCloudContextHandoff(contextHandoff) ? { contextHandoff } : {}),
612
- ...(directPush !== undefined ? { directPush } : {}),
613
- ...(autoCreatePR !== undefined ? { autoCreatePR } : {}),
614
- ...(skipReviewerRequest !== undefined ? { skipReviewerRequest } : {}),
615
- ...(allowLocalState !== undefined ? { allowLocalState } : {}),
616
- ...(envNames !== undefined ? { envNames } : {}),
617
- ...(envFromFiles !== undefined ? { envFromFiles } : {}),
618
- ...(environment !== undefined ? { environment } : {}),
619
- ...(acknowledged !== undefined ? { acknowledged } : {}),
620
- };
621
- }
622
378
  const autoReview = parseOptionalEnvBoolean(env[CURSOR_AUTO_REVIEW_ENV]);
623
379
  const sandbox = parseOptionalEnvBoolean(env[CURSOR_SANDBOX_ENV]);
624
380
  const force = parseOptionalEnvBoolean(env[CURSOR_LOCAL_FORCE_ENV]);
@@ -637,139 +393,12 @@ export function cursorSdkConfigFromEnv(env: Record<string, string | undefined> =
637
393
  }
638
394
 
639
395
  export function resolveCursorSdkConfig(options: ResolveCursorSdkConfigOptions = {}): CursorResolvedSdkConfig {
640
- const cliRuntime = validateExplicitValue(options.cli?.runtime, "--cursor-runtime", isCursorRuntime, '"local" or "cloud"');
641
- const cliContextHandoff = validateExplicitValue(
642
- options.cli?.cloud?.contextHandoff,
643
- "--cursor-cloud-context",
644
- isCursorCloudContextHandoff,
645
- '"never", "fresh", or "bootstrap"',
646
- );
647
396
  const env = cursorSdkConfigFromEnv(options.env);
648
- const builtIn = {
649
- ...BUILT_IN_CURSOR_CONFIG,
650
- ...options.builtIn,
651
- cloud: { ...BUILT_IN_CURSOR_CONFIG.cloud, ...options.builtIn?.cloud },
652
- };
653
397
  const cli = options.cli;
654
398
  const session = options.session;
655
399
  const project = options.project;
656
400
  const user = options.user;
657
401
  return {
658
- runtime: resolveSafetyField(
659
- RUNTIME_ORDER,
660
- {
661
- cli: cliRuntime,
662
- environment: env.runtime,
663
- session: session?.runtime,
664
- project: project?.runtime,
665
- user: user?.runtime,
666
- builtin: builtIn.runtime,
667
- },
668
- (value) => (value === "cloud" ? 1 : 0),
669
- ),
670
- cloud: {
671
- repo: resolveOrdinaryField(CLOUD_ORDER, {
672
- cli: cli?.cloud?.repo,
673
- environment: env.cloud?.repo,
674
- session: session?.cloud?.repo,
675
- user: user?.cloud?.repo,
676
- builtin: undefined,
677
- }),
678
- branch: resolveOrdinaryField(CLOUD_ORDER, {
679
- cli: cli?.cloud?.branch,
680
- environment: env.cloud?.branch,
681
- session: session?.cloud?.branch,
682
- user: user?.cloud?.branch,
683
- builtin: undefined,
684
- }),
685
- contextHandoff: resolveSafetyField(
686
- CLOUD_ORDER,
687
- {
688
- cli: cliContextHandoff,
689
- environment: env.cloud?.contextHandoff,
690
- session: session?.cloud?.contextHandoff,
691
- user: user?.cloud?.contextHandoff,
692
- builtin: builtIn.cloud.contextHandoff,
693
- },
694
- (value) => ({ never: 0, fresh: 1, bootstrap: 2 })[value],
695
- ),
696
- directPush: resolveSafetyField(
697
- CLOUD_ORDER,
698
- {
699
- cli: cli?.cloud?.directPush,
700
- environment: env.cloud?.directPush,
701
- session: session?.cloud?.directPush,
702
- user: user?.cloud?.directPush,
703
- builtin: builtIn.cloud.directPush,
704
- },
705
- (value) => (value ? 1 : 0),
706
- ),
707
- autoCreatePR: resolveSafetyField(
708
- CLOUD_ORDER,
709
- {
710
- cli: cli?.cloud?.autoCreatePR,
711
- environment: env.cloud?.autoCreatePR,
712
- session: session?.cloud?.autoCreatePR,
713
- user: user?.cloud?.autoCreatePR,
714
- builtin: builtIn.cloud.autoCreatePR,
715
- },
716
- (value) => (value ? 1 : 0),
717
- ),
718
- skipReviewerRequest: resolveSafetyField(
719
- CLOUD_ORDER,
720
- {
721
- cli: cli?.cloud?.skipReviewerRequest,
722
- environment: env.cloud?.skipReviewerRequest,
723
- session: session?.cloud?.skipReviewerRequest,
724
- user: user?.cloud?.skipReviewerRequest,
725
- builtin: builtIn.cloud.skipReviewerRequest,
726
- },
727
- (value) => (value ? 1 : 0),
728
- ),
729
- allowLocalState: resolveSafetyField(
730
- CLOUD_ORDER,
731
- {
732
- cli: cli?.cloud?.allowLocalState,
733
- environment: env.cloud?.allowLocalState,
734
- session: session?.cloud?.allowLocalState,
735
- user: user?.cloud?.allowLocalState,
736
- builtin: builtIn.cloud.allowLocalState,
737
- },
738
- (value) => (value ? 1 : 0),
739
- ),
740
- envNames: resolveEnvNamesSafetyField(CLOUD_ORDER, {
741
- cli: cli?.cloud?.envNames,
742
- environment: env.cloud?.envNames,
743
- session: session?.cloud?.envNames,
744
- user: user?.cloud?.envNames,
745
- builtin: builtIn.cloud.envNames,
746
- }),
747
- envFromFiles: resolveSafetyField(
748
- CLOUD_ORDER,
749
- {
750
- cli: cli?.cloud?.envFromFiles,
751
- environment: env.cloud?.envFromFiles,
752
- session: session?.cloud?.envFromFiles,
753
- user: user?.cloud?.envFromFiles,
754
- builtin: builtIn.cloud.envFromFiles,
755
- },
756
- (value) => (value ? 1 : 0),
757
- ),
758
- environment: resolveOrdinaryField(CLOUD_ORDER, {
759
- cli: cli?.cloud?.environment,
760
- environment: env.cloud?.environment,
761
- session: session?.cloud?.environment,
762
- user: user?.cloud?.environment,
763
- builtin: undefined,
764
- }),
765
- acknowledged: resolveOrdinaryField(CLOUD_ORDER, {
766
- cli: cli?.cloud?.acknowledged,
767
- environment: env.cloud?.acknowledged,
768
- session: session?.cloud?.acknowledged,
769
- user: user?.cloud?.acknowledged,
770
- builtin: builtIn.cloud.acknowledged,
771
- }),
772
- },
773
402
  local: {
774
403
  autoReview: resolveOrdinaryField(LOCAL_ORDER, {
775
404
  cli: cli?.local?.autoReview,
@@ -0,0 +1,57 @@
1
+ import type { ExtensionHandler, SessionShutdownEvent } from "@earendil-works/pi-coding-agent";
2
+
3
+ /**
4
+ * First factory invocation in this process owns Cursor's process-global
5
+ * registrars (bridge, session scope, pooled SDK agent). Pi child sessions call
6
+ * `createAgentSession` + `bindExtensions` in the same process, which re-invokes
7
+ * this factory against a new ExtensionAPI while the parent Cursor run is still
8
+ * live. A nested factory must not re-register.
9
+ *
10
+ * Ownership ends after the owner's shutdown handlers finish. Pi then reloads
11
+ * the extension for `new`, `resume`, `fork`, and explicit reload operations.
12
+ */
13
+ export type CursorExtensionFactoryClaim =
14
+ | { kind: "owner"; token: symbol }
15
+ | { kind: "nested" };
16
+
17
+ export interface CursorExtensionFactoryGuardApi {
18
+ on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
19
+ }
20
+
21
+ let factoryOwnerToken: symbol | undefined;
22
+
23
+ export function claimCursorExtensionFactory(): CursorExtensionFactoryClaim {
24
+ if (factoryOwnerToken) return { kind: "nested" };
25
+ const token = Symbol("cursor-extension-factory-owner");
26
+ factoryOwnerToken = token;
27
+ return { kind: "owner", token };
28
+ }
29
+
30
+ export function releaseCursorExtensionFactory(token: symbol): void {
31
+ if (factoryOwnerToken === token) {
32
+ factoryOwnerToken = undefined;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Install the owner's release handler after all other factory registration
38
+ * succeeds. The final handler keeps ownership until earlier shutdown cleanup
39
+ * has finished.
40
+ */
41
+ export function registerCursorExtensionFactoryRelease(
42
+ pi: CursorExtensionFactoryGuardApi,
43
+ claim: Extract<CursorExtensionFactoryClaim, { kind: "owner" }>,
44
+ ): void {
45
+ pi.on("session_shutdown", () => {
46
+ releaseCursorExtensionFactory(claim.token);
47
+ });
48
+ }
49
+
50
+ export const __testUtils = {
51
+ isInstalled(): boolean {
52
+ return factoryOwnerToken !== undefined;
53
+ },
54
+ reset(): void {
55
+ factoryOwnerToken = undefined;
56
+ },
57
+ };
@@ -64,6 +64,10 @@ export class CursorPiToolBridgeRegistry implements CursorPiToolBridge {
64
64
  return run;
65
65
  }
66
66
 
67
+ hasLiveRuns(): boolean {
68
+ return this.runs.size > 0;
69
+ }
70
+
67
71
  async disposeAll(reason = "Cursor pi tool bridge disposed"): Promise<void> {
68
72
  await Promise.all([...this.runs].map(async (run) => {
69
73
  run.cancel(reason);
@@ -89,6 +89,11 @@ Get-CimInstance Win32_Process -Filter "Name = 'bash.exe' OR Name = 'sh.exe'" |
89
89
  }
90
90
 
91
91
  export function registerCursorPiToolBridge(pi: CursorPiToolBridgeExtensionApi): CursorPiToolBridge {
92
+ // Replacing a bridge during a live MCP run cancels its pending pi tool
93
+ // calls. Keep the active registry as a final safety belt.
94
+ if (registeredCursorPiToolBridge?.hasLiveRuns()) {
95
+ return registeredCursorPiToolBridge;
96
+ }
92
97
  bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge extension reloaded");
93
98
  void registeredCursorPiToolBridge?.disposeAll("Cursor pi tool bridge extension reloaded");
94
99
  const bridge = new CursorPiToolBridgeRegistry(pi);