pi-cursor-sdk 0.1.60 → 0.1.61

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 (56) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +123 -9
  3. package/docs/cursor-model-ux-spec.md +19 -11
  4. package/docs/cursor-native-tool-replay.md +4 -2
  5. package/docs/cursor-tool-surfaces.md +7 -1
  6. package/docs/platform-smoke.md +51 -20
  7. package/package.json +13 -2
  8. package/platform-smoke.config.mjs +1 -0
  9. package/scripts/cloud-runtime-smoke.d.mts +40 -1
  10. package/scripts/cloud-runtime-smoke.mjs +444 -325
  11. package/scripts/lib/cloud-smoke-artifacts.d.mts +16 -0
  12. package/scripts/lib/cloud-smoke-artifacts.mjs +94 -0
  13. package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +209 -0
  14. package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +585 -0
  15. package/scripts/lib/cloud-smoke-github.d.mts +78 -0
  16. package/scripts/lib/cloud-smoke-github.mjs +331 -0
  17. package/scripts/lib/cloud-smoke-pi-runner.d.mts +42 -0
  18. package/scripts/lib/cloud-smoke-pi-runner.mjs +214 -0
  19. package/scripts/lib/cloud-smoke-shutdown.d.mts +67 -0
  20. package/scripts/lib/cloud-smoke-shutdown.mjs +133 -0
  21. package/scripts/lib/local-resume-smoke-harness.mjs +2 -0
  22. package/scripts/local-resume-cleanup-smoke.mjs +11 -2
  23. package/scripts/platform-smoke/card-detect.mjs +1 -0
  24. package/scripts/platform-smoke/scenarios.d.mts +22 -0
  25. package/scripts/platform-smoke/scenarios.mjs +17 -0
  26. package/scripts/platform-smoke/targets.mjs +1 -0
  27. package/src/cursor-agent-message-web-tools.ts +14 -6
  28. package/src/cursor-cloud-local-state.ts +536 -0
  29. package/src/cursor-cloud-options.ts +46 -70
  30. package/src/cursor-config.ts +160 -14
  31. package/src/cursor-http1.ts +65 -0
  32. package/src/cursor-live-run-coordinator.ts +8 -1
  33. package/src/cursor-pi-tool-bridge-abort.ts +10 -3
  34. package/src/cursor-pi-tool-bridge-constants.ts +6 -0
  35. package/src/cursor-pi-tool-bridge-env.ts +9 -0
  36. package/src/cursor-pi-tool-bridge-run.ts +27 -5
  37. package/src/cursor-pi-tool-bridge.ts +11 -2
  38. package/src/cursor-provider-errors.ts +72 -2
  39. package/src/cursor-provider-live-run-drain.ts +2 -1
  40. package/src/cursor-provider-run-finalizer.ts +8 -2
  41. package/src/cursor-provider-run-outcome.ts +7 -1
  42. package/src/cursor-provider-turn-finalize.ts +23 -4
  43. package/src/cursor-provider-turn-prepare.ts +17 -3
  44. package/src/cursor-provider-turn-runner.ts +5 -0
  45. package/src/cursor-provider-turn-send.ts +1 -1
  46. package/src/cursor-question-tool.ts +8 -0
  47. package/src/cursor-ripgrep-path.ts +32 -0
  48. package/src/cursor-runtime-state.ts +119 -100
  49. package/src/cursor-sdk-process-error-guard.ts +19 -0
  50. package/src/cursor-session-agent-cleanup.ts +66 -15
  51. package/src/cursor-session-agent-lifecycle.ts +9 -4
  52. package/src/cursor-session-agent-resume.ts +50 -7
  53. package/src/cursor-session-agent.ts +106 -67
  54. package/src/cursor-session-scope.ts +22 -2
  55. package/src/cursor-session-store.ts +155 -0
  56. package/src/cursor-state.ts +162 -21
@@ -1,14 +1,11 @@
1
- import { execFileSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { dirname, join } from "node:path";
4
1
  import type { AgentModeOption, AgentOptions, ModelSelection } from "@cursor/sdk";
5
2
  import { isCursorCloudEnvironmentType, type CursorResolvedSdkConfig } from "./cursor-config.js";
6
-
7
- export interface CursorCloudLocalState {
8
- insideGitRepo: boolean;
9
- dirty: boolean;
10
- unpushed: boolean;
11
- }
3
+ import {
4
+ normalizeCursorCloudStartingRef,
5
+ parseCursorCloudRepositoryUrl,
6
+ type CursorCloudLocalState,
7
+ type CursorCloudLocalStateUnknownReason,
8
+ } from "./cursor-cloud-local-state.js";
12
9
 
13
10
  export interface CursorCloudPreflightIssue {
14
11
  code:
@@ -20,6 +17,7 @@ export interface CursorCloudPreflightIssue {
20
17
  | "cloud_environment_type_required"
21
18
  | "cloud_environment_repo_conflict"
22
19
  | "cloud_branch_repo_required"
20
+ | "cloud_branch_invalid"
23
21
  | "cloud_repo_invalid";
24
22
  message: string;
25
23
  }
@@ -29,63 +27,21 @@ export interface CursorCloudPreflightResult {
29
27
  issues: CursorCloudPreflightIssue[];
30
28
  }
31
29
 
32
- type GitRunner = (cwd: string, args: string[]) => string | undefined;
33
-
34
30
  const CLOUD_REPO_URL_MESSAGE = "Cursor cloud repository must be an HTTPS repository URL without embedded credentials, query parameters, or fragments.";
35
-
36
- export function parseCursorCloudRepositoryUrl(value: string | undefined): string | undefined {
37
- const trimmed = value?.trim();
38
- if (!trimmed) return undefined;
39
- try {
40
- const url = new URL(trimmed);
41
- if (
42
- url.protocol !== "https:" ||
43
- url.username !== "" ||
44
- url.password !== "" ||
45
- url.search !== "" ||
46
- url.hash !== "" ||
47
- url.pathname === "/"
48
- ) return undefined;
49
- return trimmed;
50
- } catch {
51
- return undefined;
52
- }
53
- }
54
-
55
- function git(cwd: string, args: string[]): string | undefined {
56
- try {
57
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
58
- } catch {
59
- return undefined;
60
- }
61
- }
62
-
63
- function hasGitMetadata(cwd: string): boolean {
64
- let current = cwd;
65
- while (true) {
66
- if (existsSync(join(current, ".git"))) return true;
67
- const parent = dirname(current);
68
- if (parent === current) return false;
69
- current = parent;
70
- }
71
- }
72
-
73
- export function inspectCursorCloudLocalState(cwd: string, runGit: GitRunner = git): CursorCloudLocalState {
74
- const insideGitRepo = runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
75
- if (insideGitRepo !== "true") {
76
- return hasGitMetadata(cwd)
77
- ? { insideGitRepo: true, dirty: true, unpushed: true }
78
- : { insideGitRepo: false, dirty: false, unpushed: false };
79
- }
80
- const status = runGit(cwd, ["status", "--porcelain=v1"]);
81
- const dirty = status === undefined || status.length > 0;
82
- const hasHead = runGit(cwd, ["rev-parse", "--verify", "HEAD"]) !== undefined;
83
- const upstream = runGit(cwd, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
84
- const ahead = upstream ? runGit(cwd, ["rev-list", "--count", "@{upstream}..HEAD"]) : undefined;
85
- const aheadCount = ahead === undefined ? Number.NaN : Number(ahead);
86
- const unpushed = hasHead && (!upstream || !Number.isFinite(aheadCount) || aheadCount > 0);
87
- return { insideGitRepo: true, dirty, unpushed };
88
- }
31
+ const CLOUD_STARTING_REF_MESSAGE = "Cursor cloud branch/ref must be a valid Git branch name, refs/heads/<branch>, or full commit SHA; invalid branch names and other refs/* are unsupported.";
32
+ const CLOUD_LOCAL_STATE_REASON_LABELS: Record<CursorCloudLocalStateUnknownReason["code"], string> = {
33
+ bare_repo: "the repository is bare",
34
+ repository_detection_failed: "Git could not determine whether the working directory is a repository",
35
+ status_failed: "Git could not determine whether the worktree or index is clean",
36
+ index_failed: "Git index inspection failed",
37
+ hidden_index_state: "the index hides worktree changes",
38
+ history_probe_failed: "Git could not inspect replacement or graft history overrides",
39
+ history_overrides: "replacement refs or graft metadata are present",
40
+ head_unavailable: "local HEAD is unavailable",
41
+ unverified_target: "the cloud target has no locally verified tracking ref",
42
+ target_probe_failed: "Git failed while resolving the cloud target",
43
+ comparison_failed: "the local HEAD comparison failed",
44
+ };
89
45
 
90
46
  export function buildCursorCloudAgentOptions(options: {
91
47
  apiKey: string;
@@ -101,6 +57,8 @@ export function buildCursorCloudAgentOptions(options: {
101
57
  const configuredRepo = resolvedConfig.cloud.repo.value;
102
58
  const repo = parseCursorCloudRepositoryUrl(configuredRepo);
103
59
  if (configuredRepo && !repo) throw new Error(CLOUD_REPO_URL_MESSAGE);
60
+ const startingRef = normalizeCursorCloudStartingRef(resolvedConfig.cloud.branch.value);
61
+ if (startingRef.kind === "unsupported") throw new Error(CLOUD_STARTING_REF_MESSAGE);
104
62
  const cloud = {
105
63
  ...(isCursorCloudEnvironmentType(environmentType)
106
64
  ? {
@@ -115,12 +73,18 @@ export function buildCursorCloudAgentOptions(options: {
115
73
  repos: [
116
74
  {
117
75
  url: repo,
118
- ...(resolvedConfig.cloud.branch.value ? { startingRef: resolvedConfig.cloud.branch.value } : {}),
76
+ ...(startingRef.kind === "branch" || startingRef.kind === "commit" ? { startingRef: startingRef.value } : {}),
119
77
  },
120
78
  ],
121
79
  }
122
80
  : {}),
123
81
  ...(resolvedConfig.cloud.directPush.value ? { workOnCurrentBranch: true } : {}),
82
+ ...(resolvedConfig.cloud.autoCreatePR.source !== "builtin"
83
+ ? { autoCreatePR: resolvedConfig.cloud.autoCreatePR.value }
84
+ : {}),
85
+ ...(resolvedConfig.cloud.skipReviewerRequest.source !== "builtin"
86
+ ? { skipReviewerRequest: resolvedConfig.cloud.skipReviewerRequest.value }
87
+ : {}),
124
88
  };
125
89
  return {
126
90
  apiKey: options.apiKey,
@@ -133,7 +97,7 @@ export function buildCursorCloudAgentOptions(options: {
133
97
 
134
98
  export function preflightCursorCloudRuntime(options: {
135
99
  resolvedConfig: CursorResolvedSdkConfig;
136
- localState?: CursorCloudLocalState;
100
+ localState: CursorCloudLocalState;
137
101
  hasPriorContext?: boolean;
138
102
  }): CursorCloudPreflightResult {
139
103
  const { resolvedConfig } = options;
@@ -151,13 +115,22 @@ export function preflightCursorCloudRuntime(options: {
151
115
  });
152
116
  }
153
117
  if (
154
- options.localState?.insideGitRepo &&
155
- (options.localState.dirty || options.localState.unpushed) &&
118
+ options.localState.insideGitRepo !== false &&
119
+ (options.localState.dirty !== false || options.localState.comparison !== "contains_head") &&
156
120
  !resolvedConfig.cloud.allowLocalState.value
157
121
  ) {
122
+ const causes: string[] = [];
123
+ if (options.localState.dirty === true) causes.push("the worktree or index is dirty");
124
+ if (options.localState.dirty === "unknown") causes.push("Git could not determine whether the worktree or index is clean");
125
+ if (options.localState.comparison === "unpushed") causes.push("local HEAD contains unpushed commits");
126
+ if (options.localState.comparison === "unknown") {
127
+ causes.push(...options.localState.reasons
128
+ .filter((reason) => reason.code !== "status_failed")
129
+ .map((reason) => CLOUD_LOCAL_STATE_REASON_LABELS[reason.code]));
130
+ }
158
131
  issues.push({
159
132
  code: "local_state_not_allowed",
160
- message: "Cursor cloud runtime cannot see dirty or unpushed local state; pass --cursor-cloud-allow-local-state only after accepting that risk.",
133
+ message: `Cursor cloud runtime cannot safely omit local state because ${causes.join("; ")}. Configure an explicit repository branch/ref with current local tracking evidence, or pass --cursor-cloud-allow-local-state only after accepting that risk.`,
161
134
  });
162
135
  }
163
136
  if (resolvedConfig.cloud.envNames.value.length > 0 || resolvedConfig.cloud.envFromFiles.value) {
@@ -178,6 +151,9 @@ export function preflightCursorCloudRuntime(options: {
178
151
  message: "Cursor cloud branch/ref requires --cursor-cloud-repo because the installed Cursor SDK supports startingRef only on cloud.repos entries.",
179
152
  });
180
153
  }
154
+ if (normalizeCursorCloudStartingRef(resolvedConfig.cloud.branch.value).kind === "unsupported") {
155
+ issues.push({ code: "cloud_branch_invalid", message: CLOUD_STARTING_REF_MESSAGE });
156
+ }
181
157
  const environment = resolvedConfig.cloud.environment.value;
182
158
  if (environment?.type && !isCursorCloudEnvironmentType(environment.type)) {
183
159
  issues.push({
@@ -1,5 +1,16 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ openSync,
8
+ readFileSync,
9
+ renameSync,
10
+ rmSync,
11
+ statSync,
12
+ writeFileSync,
13
+ } from "node:fs";
3
14
  import { dirname, join } from "node:path";
4
15
  import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
5
16
  import { parseOptionalEnvBoolean } from "./cursor-env-boolean.js";
@@ -12,6 +23,8 @@ export const CURSOR_CLOUD_REPO_ENV = "PI_CURSOR_CLOUD_REPO";
12
23
  export const CURSOR_CLOUD_BRANCH_ENV = "PI_CURSOR_CLOUD_BRANCH";
13
24
  export const CURSOR_CLOUD_CONTEXT_ENV = "PI_CURSOR_CLOUD_CONTEXT";
14
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";
15
28
  export const CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV = "PI_CURSOR_CLOUD_ALLOW_LOCAL_STATE";
16
29
  export const CURSOR_CLOUD_ENV_ENV = "PI_CURSOR_CLOUD_ENV";
17
30
  export const CURSOR_CLOUD_ENV_FROM_FILES_ENV = "PI_CURSOR_CLOUD_ENV_FROM_FILES";
@@ -22,6 +35,7 @@ export const CURSOR_AUTO_REVIEW_ENV = "PI_CURSOR_AUTO_REVIEW";
22
35
  export const CURSOR_SANDBOX_ENV = "PI_CURSOR_SANDBOX";
23
36
  export const CURSOR_LOCAL_FORCE_ENV = "PI_CURSOR_LOCAL_FORCE";
24
37
  export const CURSOR_LOCAL_RESUME_ENV = "PI_CURSOR_LOCAL_RESUME";
38
+ export const CURSOR_HTTP1_ENV = "PI_CURSOR_HTTP_1_1";
25
39
 
26
40
  export type CursorConfigSource = "cli" | "environment" | "project" | "user" | "session" | "model-alias" | "builtin";
27
41
  export type CursorConfigTrustLevel = "one-shot" | "environment" | "trusted-project" | "user" | "session" | "model-catalog" | "builtin";
@@ -42,6 +56,8 @@ export interface CursorSdkConfig {
42
56
  branch?: string;
43
57
  contextHandoff?: CursorCloudContextHandoff;
44
58
  directPush?: boolean;
59
+ autoCreatePR?: boolean;
60
+ skipReviewerRequest?: boolean;
45
61
  allowLocalState?: boolean;
46
62
  envNames?: string[];
47
63
  envFromFiles?: boolean;
@@ -56,6 +72,7 @@ export interface CursorSdkConfig {
56
72
  };
57
73
  force?: boolean;
58
74
  resume?: boolean;
75
+ useHttp1ForAgent?: boolean;
59
76
  };
60
77
  }
61
78
 
@@ -82,6 +99,8 @@ export interface CursorResolvedSdkConfig {
82
99
  branch: CursorResolvedSetting<string | undefined>;
83
100
  contextHandoff: CursorResolvedSetting<CursorCloudContextHandoff>;
84
101
  directPush: CursorResolvedSetting<boolean>;
102
+ autoCreatePR: CursorResolvedSetting<boolean>;
103
+ skipReviewerRequest: CursorResolvedSetting<boolean>;
85
104
  allowLocalState: CursorResolvedSetting<boolean>;
86
105
  envNames: CursorResolvedSetting<string[]>;
87
106
  envFromFiles: CursorResolvedSetting<boolean>;
@@ -93,6 +112,7 @@ export interface CursorResolvedSdkConfig {
93
112
  sandboxEnabled: CursorResolvedSetting<boolean>;
94
113
  force: CursorResolvedSetting<boolean>;
95
114
  resume: CursorResolvedSetting<boolean>;
115
+ useHttp1ForAgent: CursorResolvedSetting<boolean>;
96
116
  };
97
117
  }
98
118
 
@@ -143,6 +163,8 @@ const BUILT_IN_CURSOR_CONFIG: Required<Pick<CursorSdkConfig, "runtime">> & {
143
163
  branch: "",
144
164
  contextHandoff: "fresh",
145
165
  directPush: false,
166
+ autoCreatePR: false,
167
+ skipReviewerRequest: false,
146
168
  allowLocalState: false,
147
169
  envNames: [],
148
170
  envFromFiles: false,
@@ -240,6 +262,8 @@ export function parseCursorSdkConfig(value: unknown): CursorSdkConfig | undefine
240
262
  if (branch) parsedCloud.branch = branch;
241
263
  if (isCursorCloudContextHandoff(cloud.contextHandoff)) parsedCloud.contextHandoff = cloud.contextHandoff;
242
264
  if (typeof cloud.directPush === "boolean") parsedCloud.directPush = cloud.directPush;
265
+ if (typeof cloud.autoCreatePR === "boolean") parsedCloud.autoCreatePR = cloud.autoCreatePR;
266
+ if (typeof cloud.skipReviewerRequest === "boolean") parsedCloud.skipReviewerRequest = cloud.skipReviewerRequest;
243
267
  if (typeof cloud.allowLocalState === "boolean") parsedCloud.allowLocalState = cloud.allowLocalState;
244
268
  if (envNames) parsedCloud.envNames = envNames;
245
269
  if (typeof cloud.envFromFiles === "boolean") parsedCloud.envFromFiles = cloud.envFromFiles;
@@ -255,6 +279,7 @@ export function parseCursorSdkConfig(value: unknown): CursorSdkConfig | undefine
255
279
  if (typeof local.sandbox === "boolean") parsedLocal.sandbox = local.sandbox;
256
280
  if (typeof local.force === "boolean") parsedLocal.force = local.force;
257
281
  if (typeof local.resume === "boolean") parsedLocal.resume = local.resume;
282
+ if (typeof local.useHttp1ForAgent === "boolean") parsedLocal.useHttp1ForAgent = local.useHttp1ForAgent;
258
283
  const sandboxOptions = asRecord(local.sandboxOptions);
259
284
  if (typeof sandboxOptions?.enabled === "boolean") parsedLocal.sandboxOptions = { enabled: sandboxOptions.enabled };
260
285
  if (Object.keys(parsedLocal).length > 0) config.local = parsedLocal;
@@ -280,6 +305,25 @@ function readCursorSdkConfigFile(path: string): CursorSdkConfig {
280
305
  }
281
306
  }
282
307
 
308
+ export function loadCursorSdkConfigForUpdate(path: string): Record<string, unknown> {
309
+ if (!existsSync(path)) return {};
310
+ let source: string;
311
+ try {
312
+ source = readFileSync(path, "utf-8");
313
+ } catch (error) {
314
+ throw new Error(`Failed to read Cursor SDK config ${path}: ${error instanceof Error ? error.message : String(error)}`);
315
+ }
316
+ let parsed: unknown;
317
+ try {
318
+ parsed = JSON.parse(source);
319
+ } catch {
320
+ throw new Error(`Invalid JSON in Cursor SDK config ${path}`);
321
+ }
322
+ const record = asRecord(parsed);
323
+ if (!record) throw new Error(`Invalid Cursor SDK config ${path}: expected a JSON object`);
324
+ return record;
325
+ }
326
+
283
327
  export function loadCursorSdkUserConfig(path = getCursorSdkUserConfigPath()): CursorSdkConfig {
284
328
  return readCursorSdkConfigFile(path);
285
329
  }
@@ -296,7 +340,39 @@ export function loadCursorSdkConfig(options: LoadCursorSdkConfigOptions = {}): {
296
340
  return project ? { user, project } : { user };
297
341
  }
298
342
 
299
- function replaceJsonFile(path: string, config: CursorSdkConfig, mode?: number): void {
343
+ const CONFIG_LOCK_RETRY_MS = 20;
344
+ const CONFIG_LOCK_TIMEOUT_MS = 5_000;
345
+ const configLockWaitBuffer = new Int32Array(new SharedArrayBuffer(4));
346
+
347
+ function sleepForConfigLock(): void {
348
+ Atomics.wait(configLockWaitBuffer, 0, 0, CONFIG_LOCK_RETRY_MS);
349
+ }
350
+
351
+ function acquireCursorSdkConfigLock(path: string): () => void {
352
+ mkdirSync(dirname(path), { recursive: true });
353
+ const lockPath = `${path}.lock`;
354
+ const deadline = Date.now() + CONFIG_LOCK_TIMEOUT_MS;
355
+ while (true) {
356
+ try {
357
+ const descriptor = openSync(lockPath, "wx", 0o600);
358
+ return () => {
359
+ try {
360
+ closeSync(descriptor);
361
+ } finally {
362
+ rmSync(lockPath, { force: true });
363
+ }
364
+ };
365
+ } catch (error) {
366
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
367
+ if (Date.now() >= deadline) {
368
+ throw new Error(`Timed out waiting for Cursor SDK config lock ${lockPath}; remove it only if no pi process is writing this config`);
369
+ }
370
+ sleepForConfigLock();
371
+ }
372
+ }
373
+ }
374
+
375
+ function replaceJsonFile(path: string, config: object, mode?: number): void {
300
376
  mkdirSync(dirname(path), { recursive: true });
301
377
  const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
302
378
  let replaced = false;
@@ -320,31 +396,61 @@ function replaceJsonFile(path: string, config: CursorSdkConfig, mode?: number):
320
396
  }
321
397
  }
322
398
 
399
+ function withCursorSdkConfigLock<T>(path: string, operation: () => T): T {
400
+ const release = acquireCursorSdkConfigLock(path);
401
+ try {
402
+ return operation();
403
+ } finally {
404
+ release();
405
+ }
406
+ }
407
+
408
+ export function updateCursorSdkConfig(
409
+ path: string,
410
+ update: (current: Record<string, unknown>) => Record<string, unknown>,
411
+ options: { newFileMode?: number } = {},
412
+ ): Record<string, unknown> {
413
+ return withCursorSdkConfigLock(path, () => {
414
+ const updated = update(loadCursorSdkConfigForUpdate(path));
415
+ const mode = existsSync(path) ? statSync(path).mode & 0o777 : options.newFileMode;
416
+ replaceJsonFile(path, updated, mode);
417
+ return updated;
418
+ });
419
+ }
420
+
323
421
  export function saveCursorSdkUserConfig(config: CursorSdkConfig, path = getCursorSdkUserConfigPath()): void {
324
- const mode = existsSync(path) ? statSync(path).mode & 0o777 : 0o600;
325
- replaceJsonFile(path, config, mode);
422
+ updateCursorSdkConfig(path, () => ({ ...config }), { newFileMode: 0o600 });
326
423
  }
327
424
 
328
425
  export function saveCursorSdkProjectConfig(cwd: string, config: CursorSdkConfig, configDirName = CONFIG_DIR_NAME): void {
329
426
  const path = getCursorSdkProjectConfigPath(cwd, configDirName);
330
- const mode = existsSync(path) ? statSync(path).mode & 0o777 : undefined;
331
- replaceJsonFile(path, config, mode);
427
+ updateCursorSdkConfig(path, () => ({ ...config }));
332
428
  }
333
429
 
334
430
  export function mergeCursorSdkConfig(base: CursorSdkConfig, patch: CursorSdkConfig): CursorSdkConfig {
431
+ return mergeCursorSdkConfigForUpdate({ ...base }, patch) as CursorSdkConfig;
432
+ }
433
+
434
+ export function mergeCursorSdkConfigForUpdate(
435
+ base: Record<string, unknown>,
436
+ patch: CursorSdkConfig,
437
+ ): Record<string, unknown> {
438
+ const baseCloud = asRecord(base.cloud);
439
+ const baseLocal = asRecord(base.local);
440
+ const baseSandboxOptions = asRecord(baseLocal?.sandboxOptions);
335
441
  return {
336
442
  ...base,
337
443
  ...patch,
338
- ...(base.cloud || patch.cloud
339
- ? { cloud: { ...base.cloud, ...patch.cloud } }
444
+ ...(baseCloud || patch.cloud
445
+ ? { cloud: { ...baseCloud, ...patch.cloud } }
340
446
  : {}),
341
- ...(base.local || patch.local
447
+ ...(baseLocal || patch.local
342
448
  ? {
343
449
  local: {
344
- ...base.local,
450
+ ...baseLocal,
345
451
  ...patch.local,
346
- ...(base.local?.sandboxOptions || patch.local?.sandboxOptions
347
- ? { sandboxOptions: { ...base.local?.sandboxOptions, ...patch.local?.sandboxOptions } }
452
+ ...(baseSandboxOptions || patch.local?.sandboxOptions
453
+ ? { sandboxOptions: { ...baseSandboxOptions, ...patch.local?.sandboxOptions } }
348
454
  : {}),
349
455
  },
350
456
  }
@@ -356,7 +462,10 @@ export function cursorFastDefaultsFromConfig(config: CursorSdkConfig | undefined
356
462
  return new Map(Object.entries(config?.fastDefaults ?? {}));
357
463
  }
358
464
 
359
- export function withCursorFastDefaults(config: CursorSdkConfig, fastDefaults: Map<string, boolean>): CursorSdkConfig {
465
+ export function withCursorFastDefaults<T extends object>(
466
+ config: T,
467
+ fastDefaults: Map<string, boolean>,
468
+ ): T & { fastDefaults: Record<string, boolean> } {
360
469
  return {
361
470
  ...config,
362
471
  fastDefaults: Object.fromEntries([...fastDefaults.entries()].sort(([a], [b]) => a.localeCompare(b))),
@@ -423,6 +532,7 @@ const RUNTIME_ORDER: CursorFieldSource[] = ["cli", "environment", "session", "pr
423
532
  const CLOUD_ORDER: CursorFieldSource[] = ["cli", "environment", "session", "user", "builtin"];
424
533
  const LOCAL_ORDER: CursorFieldSource[] = ["cli", "environment", "project", "user", "builtin"];
425
534
  const LOCAL_FORCE_ORDER: CursorFieldSource[] = ["cli", "environment", "builtin"];
535
+ const HTTP1_ORDER: CursorFieldSource[] = ["session", "environment", "user", "builtin"];
426
536
 
427
537
  function buildFieldLayers<T>(order: CursorFieldSource[], values: CursorFieldValues<T>): Array<CursorResolvedSetting<T> | undefined> {
428
538
  return order.map((source) => (source === "builtin" ? resolved("builtin", values.builtin as T) : valueFrom(source, values[source])));
@@ -453,6 +563,8 @@ export function cursorSdkConfigFromEnv(env: Record<string, string | undefined> =
453
563
  '"never", "fresh", or "bootstrap"',
454
564
  );
455
565
  const directPush = parseOptionalEnvBoolean(env[CURSOR_CLOUD_DIRECT_PUSH_ENV]);
566
+ const autoCreatePR = parseOptionalEnvBoolean(env[CURSOR_CLOUD_AUTO_CREATE_PR_ENV]);
567
+ const skipReviewerRequest = parseOptionalEnvBoolean(env[CURSOR_CLOUD_SKIP_REVIEWER_REQUEST_ENV]);
456
568
  const allowLocalState = parseOptionalEnvBoolean(env[CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV]);
457
569
  const envNames = parseExplicitCursorCloudEnvNames(env[CURSOR_CLOUD_ENV_ENV], CURSOR_CLOUD_ENV_ENV);
458
570
  const envFromFiles = parseOptionalEnvBoolean(env[CURSOR_CLOUD_ENV_FROM_FILES_ENV]);
@@ -466,6 +578,8 @@ export function cursorSdkConfigFromEnv(env: Record<string, string | undefined> =
466
578
  branch !== undefined ||
467
579
  isCursorCloudContextHandoff(contextHandoff) ||
468
580
  directPush !== undefined ||
581
+ autoCreatePR !== undefined ||
582
+ skipReviewerRequest !== undefined ||
469
583
  allowLocalState !== undefined ||
470
584
  envNames !== undefined ||
471
585
  envFromFiles !== undefined ||
@@ -477,6 +591,8 @@ export function cursorSdkConfigFromEnv(env: Record<string, string | undefined> =
477
591
  ...(branch !== undefined ? { branch } : {}),
478
592
  ...(isCursorCloudContextHandoff(contextHandoff) ? { contextHandoff } : {}),
479
593
  ...(directPush !== undefined ? { directPush } : {}),
594
+ ...(autoCreatePR !== undefined ? { autoCreatePR } : {}),
595
+ ...(skipReviewerRequest !== undefined ? { skipReviewerRequest } : {}),
480
596
  ...(allowLocalState !== undefined ? { allowLocalState } : {}),
481
597
  ...(envNames !== undefined ? { envNames } : {}),
482
598
  ...(envFromFiles !== undefined ? { envFromFiles } : {}),
@@ -488,12 +604,14 @@ export function cursorSdkConfigFromEnv(env: Record<string, string | undefined> =
488
604
  const sandbox = parseOptionalEnvBoolean(env[CURSOR_SANDBOX_ENV]);
489
605
  const force = parseOptionalEnvBoolean(env[CURSOR_LOCAL_FORCE_ENV]);
490
606
  const resume = parseOptionalEnvBoolean(env[CURSOR_LOCAL_RESUME_ENV]);
491
- if (autoReview !== undefined || sandbox !== undefined || force !== undefined || resume !== undefined) {
607
+ const useHttp1ForAgent = parseOptionalEnvBoolean(env[CURSOR_HTTP1_ENV]);
608
+ if (autoReview !== undefined || sandbox !== undefined || force !== undefined || resume !== undefined || useHttp1ForAgent !== undefined) {
492
609
  config.local = {
493
610
  ...(autoReview !== undefined ? { autoReview } : {}),
494
611
  ...(sandbox !== undefined ? { sandboxOptions: { enabled: sandbox } } : {}),
495
612
  ...(force !== undefined ? { force } : {}),
496
613
  ...(resume !== undefined ? { resume } : {}),
614
+ ...(useHttp1ForAgent !== undefined ? { useHttp1ForAgent } : {}),
497
615
  };
498
616
  }
499
617
  return config;
@@ -567,6 +685,28 @@ export function resolveCursorSdkConfig(options: ResolveCursorSdkConfigOptions =
567
685
  },
568
686
  (value) => (value ? 1 : 0),
569
687
  ),
688
+ autoCreatePR: resolveSafetyField(
689
+ CLOUD_ORDER,
690
+ {
691
+ cli: cli?.cloud?.autoCreatePR,
692
+ environment: env.cloud?.autoCreatePR,
693
+ session: session?.cloud?.autoCreatePR,
694
+ user: user?.cloud?.autoCreatePR,
695
+ builtin: builtIn.cloud.autoCreatePR,
696
+ },
697
+ (value) => (value ? 1 : 0),
698
+ ),
699
+ skipReviewerRequest: resolveSafetyField(
700
+ CLOUD_ORDER,
701
+ {
702
+ cli: cli?.cloud?.skipReviewerRequest,
703
+ environment: env.cloud?.skipReviewerRequest,
704
+ session: session?.cloud?.skipReviewerRequest,
705
+ user: user?.cloud?.skipReviewerRequest,
706
+ builtin: builtIn.cloud.skipReviewerRequest,
707
+ },
708
+ (value) => (value ? 1 : 0),
709
+ ),
570
710
  allowLocalState: resolveSafetyField(
571
711
  CLOUD_ORDER,
572
712
  {
@@ -638,6 +778,12 @@ export function resolveCursorSdkConfig(options: ResolveCursorSdkConfigOptions =
638
778
  user: user?.local?.resume,
639
779
  builtin: true,
640
780
  }),
781
+ useHttp1ForAgent: resolveOrdinaryField(HTTP1_ORDER, {
782
+ session: session?.local?.useHttp1ForAgent,
783
+ environment: env.local?.useHttp1ForAgent,
784
+ user: user?.local?.useHttp1ForAgent,
785
+ builtin: false,
786
+ }),
641
787
  },
642
788
  };
643
789
  }
@@ -0,0 +1,65 @@
1
+ import type { CursorSdkModule } from "./cursor-sdk-runtime.js";
2
+ import type { CursorResolvedSetting } from "./cursor-config.js";
3
+ import { asRecord } from "./cursor-record-utils.js";
4
+
5
+ export const CURSOR_HTTP1_ENTRY_TYPE = "cursor-http1-state";
6
+
7
+ export interface CursorHttp1EntryData {
8
+ enabled: boolean;
9
+ }
10
+
11
+ type CursorHttp1Sdk = {
12
+ Cursor: Pick<CursorSdkModule["Cursor"], "configure">;
13
+ };
14
+
15
+ let sessionCursorHttp1Enabled: boolean | undefined;
16
+ let globalPreferenceAuthoritative = false;
17
+ let configuredCursor: CursorHttp1Sdk["Cursor"] | undefined;
18
+
19
+ export function isCursorHttp1EntryData(value: unknown): value is CursorHttp1EntryData {
20
+ return typeof asRecord(value)?.enabled === "boolean";
21
+ }
22
+
23
+ export function getStoredCursorHttp1Enabled(): boolean | undefined {
24
+ return sessionCursorHttp1Enabled;
25
+ }
26
+
27
+ export function setStoredCursorHttp1Enabled(enabled: boolean | undefined): void {
28
+ sessionCursorHttp1Enabled = enabled;
29
+ }
30
+
31
+ export function getResolvedSessionCursorHttp1Enabled(): boolean | undefined {
32
+ return globalPreferenceAuthoritative ? undefined : sessionCursorHttp1Enabled;
33
+ }
34
+
35
+ export function setCursorHttp1GlobalPreferenceAuthoritative(authoritative: boolean): void {
36
+ globalPreferenceAuthoritative = authoritative;
37
+ }
38
+
39
+ export function clearCursorSdkHttp1(): void {
40
+ if (configuredCursor === undefined) return;
41
+ configuredCursor.configure({ local: { useHttp1ForAgent: null } });
42
+ configuredCursor = undefined;
43
+ }
44
+
45
+ export function configureCursorSdkHttp1(
46
+ sdk: CursorHttp1Sdk,
47
+ setting: CursorResolvedSetting<boolean>,
48
+ ): boolean | undefined {
49
+ if (setting.source !== "builtin") {
50
+ sdk.Cursor.configure({ local: { useHttp1ForAgent: setting.value } });
51
+ configuredCursor = sdk.Cursor;
52
+ return setting.value;
53
+ }
54
+ if (configuredCursor === sdk.Cursor) clearCursorSdkHttp1();
55
+ else configuredCursor = undefined;
56
+ return undefined;
57
+ }
58
+
59
+ export const __testUtils = {
60
+ reset(): void {
61
+ sessionCursorHttp1Enabled = undefined;
62
+ globalPreferenceAuthoritative = false;
63
+ configuredCursor = undefined;
64
+ },
65
+ };
@@ -105,6 +105,11 @@ export interface CursorLiveRunCoordinator {
105
105
 
106
106
  type CursorLiveBridgeMatcher = Pick<CursorPiToolBridgeRun, "hasPendingPiToolCallId">;
107
107
 
108
+ function isPendingBridgeToolRequest(run: CursorLiveRun, request: CursorPiBridgeToolRequest): boolean {
109
+ const bridgeRun = [run.bridgeRun, run.sessionBridgeRun].find((candidate) => candidate?.id === request.runId);
110
+ return bridgeRun?.hasPendingPiToolCallId(request.piToolCallId) === true;
111
+ }
112
+
108
113
  export interface CursorLiveRunRecord {
109
114
  id: string;
110
115
  disposed: boolean;
@@ -384,7 +389,9 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
384
389
  const requests: CursorPiBridgeToolRequest[] = [];
385
390
  while (run.pendingEvents[0]?.type === "bridge-tool") {
386
391
  const event = run.pendingEvents.shift();
387
- if (event?.type === "bridge-tool") requests.push(event.request);
392
+ if (event?.type === "bridge-tool" && isPendingBridgeToolRequest(run, event.request)) {
393
+ requests.push(event.request);
394
+ }
388
395
  }
389
396
  return requests;
390
397
  },
@@ -54,11 +54,18 @@ class CursorPiToolBridgeToolExecutionAbortTracker {
54
54
  for (const toolCallId of [...this.activeExecutions.keys()]) this.finish(toolCallId);
55
55
  }
56
56
 
57
+ abort(toolCallId: string, reason: string): boolean {
58
+ const execution = this.activeExecutions.get(toolCallId);
59
+ if (!execution) return false;
60
+ this.cancelExecution(execution, reason);
61
+ this.abortExecution(execution);
62
+ this.finish(toolCallId);
63
+ return true;
64
+ }
65
+
57
66
  abortAll(reason: string): void {
58
67
  for (const execution of [...this.activeExecutions.values()]) {
59
- this.cancelExecution(execution, reason);
60
- this.abortExecution(execution);
61
- this.finish(execution.toolCallId);
68
+ this.abort(execution.toolCallId, reason);
62
69
  }
63
70
  }
64
71
 
@@ -1,2 +1,8 @@
1
1
  export const MCP_SERVER_NAME = "pi_tools";
2
2
  export const MCP_ENDPOINT_ROOT = "/cursor-pi-tool-bridge";
3
+
4
+ const CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN = /^cursor-pi-bridge-run-[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}-tool-\d+$/i;
5
+
6
+ export function isCursorPiBridgeToolCallId(toolCallId: string): boolean {
7
+ return CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN.test(toolCallId);
8
+ }
@@ -1,7 +1,9 @@
1
1
  import { parseEnvBoolean } from "./cursor-env-boolean.js";
2
+ import { resolveCursorMcpToolTimeoutMs } from "./cursor-mcp-timeout-override.js";
2
3
 
3
4
  export const CURSOR_PI_TOOL_BRIDGE_ENV = "PI_CURSOR_PI_TOOL_BRIDGE";
4
5
  export const CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV = "PI_CURSOR_EXPOSE_BUILTIN_TOOLS";
6
+ export const CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV = "PI_CURSOR_PI_BRIDGE_CALL_TIMEOUT_MS";
5
7
 
6
8
  export function resolveCursorPiToolBridgeEnabled(env: Record<string, string | undefined> = process.env): boolean {
7
9
  return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_ENV], true);
@@ -10,3 +12,10 @@ export function resolveCursorPiToolBridgeEnabled(env: Record<string, string | un
10
12
  export function resolveCursorPiToolBridgeBuiltinsEnabled(env: Record<string, string | undefined> = process.env): boolean {
11
13
  return parseEnvBoolean(env[CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV], false);
12
14
  }
15
+
16
+ export function resolveCursorPiToolBridgeCallTimeoutMs(env: Record<string, string | undefined> = process.env): number {
17
+ const mcpToolTimeoutMs = resolveCursorMcpToolTimeoutMs(env);
18
+ const parsed = Number(env[CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV]?.trim());
19
+ if (!Number.isFinite(parsed) || parsed <= 0) return mcpToolTimeoutMs;
20
+ return Math.min(Math.max(Math.trunc(parsed), 1), mcpToolTimeoutMs);
21
+ }