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
@@ -9,6 +9,7 @@ import {
9
9
  ListToolsRequestSchema,
10
10
  type CallToolResult,
11
11
  } from "@modelcontextprotocol/sdk/types.js";
12
+ import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
12
13
  import { MCP_ENDPOINT_ROOT, MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
13
14
  import {
14
15
  type CursorPiToolBridgeDiagnosticEvent,
@@ -17,6 +18,7 @@ import {
17
18
  type CursorPiToolBridgeRequestDiagnosticFields,
18
19
  writeCursorPiToolBridgeDiagnostic,
19
20
  } from "./cursor-pi-tool-bridge-diagnostics.js";
21
+ import { resolveCursorPiToolBridgeCallTimeoutMs } from "./cursor-pi-tool-bridge-env.js";
20
22
  import type {
21
23
  CursorPiBridgeToolRequest,
22
24
  CursorPiToolBridgeRun,
@@ -46,6 +48,7 @@ interface PendingBridgeCall {
46
48
  reject: (error: Error) => void;
47
49
  signal?: AbortSignal;
48
50
  onAbort?: () => void;
51
+ timeout?: ReturnType<typeof setTimeout>;
49
52
  settled: boolean;
50
53
  }
51
54
 
@@ -58,6 +61,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
58
61
  private readonly registry: CursorPiToolBridgeRunHost;
59
62
  private readonly env: Record<string, string | undefined>;
60
63
  private readonly endpointPath: string;
64
+ private readonly callTimeoutMs: number;
61
65
  private readonly knownMcpToolNames: ReadonlySet<string>;
62
66
  private readonly knownCursorMcpCallIds = new Set<string>();
63
67
  private readonly queuedRequests: CursorPiBridgeToolRequest[] = [];
@@ -87,6 +91,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
87
91
  this.debugRecorder = options.debugRecorder;
88
92
  this.id = `cursor-pi-bridge-run-${randomUUID()}`;
89
93
  this.endpointPath = `${MCP_ENDPOINT_ROOT}/${randomUUID()}/mcp`;
94
+ this.callTimeoutMs = resolveCursorPiToolBridgeCallTimeoutMs(env);
90
95
  this.knownMcpToolNames = new Set(snapshot.tools.map((tool) => tool.mcpToolName));
91
96
  }
92
97
 
@@ -218,7 +223,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
218
223
  }
219
224
  this.queuedRequests.splice(0);
220
225
  for (const pending of [...this.pendingByBridgeCallId.values()]) {
221
- this.rejectPending(pending, error, "cancelled");
226
+ this.rejectAndAbortPending(pending, error, "cancelled");
222
227
  }
223
228
  }
224
229
 
@@ -290,7 +295,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
290
295
  settled: false,
291
296
  };
292
297
  pending.onAbort = () => {
293
- this.rejectPending(pending, new Error("Cursor MCP bridge tool request was aborted"), "cancelled");
298
+ this.rejectAndAbortPending(pending, new Error("Cursor MCP bridge tool request was aborted"), "cancelled");
294
299
  };
295
300
  if (signal?.aborted) {
296
301
  pending.onAbort();
@@ -301,6 +306,11 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
301
306
  this.pendingByBridgeCallId.set(request.bridgeCallId, pending);
302
307
  this.pendingByCursorMcpCallId.set(cursorMcpCallId, pending);
303
308
  this.knownCursorMcpCallIds.add(cursorMcpCallId);
309
+ pending.timeout = setTimeout(() => {
310
+ const reason = `Cursor pi bridge CallTool timed out after ${this.callTimeoutMs} ms`;
311
+ this.rejectAndAbortPending(pending, new Error(reason));
312
+ }, this.callTimeoutMs);
313
+ pending.timeout.unref?.();
304
314
  if (!this.onToolRequest) {
305
315
  if (this.liveRunHandlerDetached) {
306
316
  this.rejectPending(pending, new Error("Cursor pi tool bridge has no active live run"), "cancelled");
@@ -345,8 +355,8 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
345
355
  pending.resolve(result);
346
356
  }
347
357
 
348
- private rejectPending(pending: PendingBridgeCall, error: Error, kind: "cancelled" | "error" = "error"): void {
349
- if (pending.settled) return;
358
+ private rejectPending(pending: PendingBridgeCall, error: Error, kind: "cancelled" | "error" = "error"): boolean {
359
+ if (pending.settled) return false;
350
360
  pending.settled = true;
351
361
  this.removePending(pending);
352
362
  this.emitRequestRejectedDiagnostic(pending.request, kind);
@@ -357,6 +367,17 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
357
367
  rejectionKind: kind,
358
368
  });
359
369
  pending.reject(error);
370
+ return true;
371
+ }
372
+
373
+ private rejectAndAbortPending(
374
+ pending: PendingBridgeCall,
375
+ error: Error,
376
+ kind: "cancelled" | "error" = "error",
377
+ ): void {
378
+ if (this.rejectPending(pending, error, kind)) {
379
+ bridgeToolExecutionAbortTracker.abort(pending.request.piToolCallId, error.message);
380
+ }
360
381
  }
361
382
 
362
383
  private lifecycleDiagnosticFields(pendingCount = this.pendingCount()): CursorPiToolBridgeLifecycleDiagnosticFields {
@@ -401,7 +422,8 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
401
422
  }
402
423
 
403
424
  private removePending(pending: PendingBridgeCall): void {
404
- pending.signal?.removeEventListener("abort", pending.onAbort ?? (() => undefined));
425
+ if (pending.onAbort) pending.signal?.removeEventListener("abort", pending.onAbort);
426
+ if (pending.timeout) clearTimeout(pending.timeout);
405
427
  this.pendingByPiToolCallId.delete(pending.request.piToolCallId);
406
428
  this.pendingByBridgeCallId.delete(pending.request.bridgeCallId);
407
429
  if (pending.request.cursorMcpCallId) this.pendingByCursorMcpCallId.delete(pending.request.cursorMcpCallId);
@@ -7,10 +7,11 @@ import {
7
7
  } from "./cursor-pi-tool-bridge-diagnostics.js";
8
8
  import {
9
9
  CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
10
+ CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
10
11
  CURSOR_PI_TOOL_BRIDGE_ENV,
11
12
  } from "./cursor-pi-tool-bridge-env.js";
12
13
  import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
13
- import { MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
14
+ import { isCursorPiBridgeToolCallId, MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
14
15
  import { LOOPBACK_HOST, CursorPiToolBridgeRegistry } from "./cursor-pi-tool-bridge-server.js";
15
16
  import type {
16
17
  CursorPiToolBridge,
@@ -34,8 +35,10 @@ export type { CursorPiToolBridgeDiagnosticEvent } from "./cursor-pi-tool-bridge-
34
35
  export { resolveCursorPiToolBridgeDebugEnabled } from "./cursor-pi-tool-bridge-diagnostics.js";
35
36
  export {
36
37
  CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
38
+ CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
37
39
  CURSOR_PI_TOOL_BRIDGE_ENV,
38
40
  resolveCursorPiToolBridgeBuiltinsEnabled,
41
+ resolveCursorPiToolBridgeCallTimeoutMs,
39
42
  resolveCursorPiToolBridgeEnabled,
40
43
  } from "./cursor-pi-tool-bridge-env.js";
41
44
  export {
@@ -91,7 +94,12 @@ export function registerCursorPiToolBridge(pi: CursorPiToolBridgeExtensionApi):
91
94
  const bridge = new CursorPiToolBridgeRegistry(pi);
92
95
  registeredCursorPiToolBridge = bridge;
93
96
  pi.on("tool_call", (event, ctx) => {
94
- if (!bridge.hasPendingPiToolCallId(event.toolCallId)) return undefined;
97
+ if (registeredCursorPiToolBridge !== bridge) return undefined;
98
+ if (!bridge.hasPendingPiToolCallId(event.toolCallId)) {
99
+ return isCursorPiBridgeToolCallId(event.toolCallId)
100
+ ? { block: true, reason: "Cursor pi bridge tool call is no longer pending" }
101
+ : undefined;
102
+ }
95
103
  const windowsAbortMarker = installWindowsBridgeBashAbortMarker(event);
96
104
  const trackingStarted = bridgeToolExecutionAbortTracker.track(event.toolCallId, {
97
105
  signal: ctx.signal,
@@ -124,6 +132,7 @@ export function getRegisteredCursorPiToolBridge(): CursorPiToolBridge | undefine
124
132
  export const __testUtils = {
125
133
  CURSOR_PI_TOOL_BRIDGE_ENV,
126
134
  CURSOR_PI_TOOL_BRIDGE_BUILTINS_ENV,
135
+ CURSOR_PI_TOOL_BRIDGE_CALL_TIMEOUT_MS_ENV,
127
136
  CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV,
128
137
  CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX,
129
138
  LOOPBACK_HOST,
@@ -1,4 +1,5 @@
1
1
  import type { RunError, RunResult } from "@cursor/sdk";
2
+ import type { CursorRuntime } from "./cursor-config.js";
2
3
  import { asRecord } from "./cursor-record-utils.js";
3
4
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
4
5
 
@@ -8,6 +9,8 @@ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
8
9
  "Cursor SDK request failed. The Cursor SDK API key may be missing, invalid, or unauthorized. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
9
10
  const AUTH_CURSOR_SDK_ERROR_MESSAGE =
10
11
  "Cursor SDK request failed because the Cursor SDK API key may be invalid or unauthorized. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
12
+ const CLOUD_AUTH_CURSOR_SDK_ERROR_MESSAGE =
13
+ "Cursor Cloud Agents request failed because Cloud API authentication rejected the API key. Use a user API key from Cursor Dashboard -> API Keys or a service account API key from Team settings; Team Admin API keys are not supported as Cursor Cloud Agents credentials. Configure the key with /login -> Use an API key -> Cursor, CURSOR_API_KEY, or --api-key, then retry.";
11
14
  // Keep "Network error" aligned with pi's agent-level retry classifier.
12
15
  const NETWORK_CURSOR_SDK_ERROR_MESSAGE =
13
16
  "Network error: Cursor SDK request failed during network or service I/O. Check your connection; pi will retry automatically when auto-retry is enabled.";
@@ -40,6 +43,58 @@ function getErrorStringField(record: Record<string, unknown> | undefined, key: s
40
43
  return typeof value === "string" ? value : undefined;
41
44
  }
42
45
 
46
+ function getErrorName(error: unknown, record: Record<string, unknown> | undefined): string | undefined {
47
+ return error instanceof Error ? error.name : getErrorStringField(record, "name");
48
+ }
49
+
50
+ function scrubHttpsHelpUrl(helpUrl: string | undefined, apiKey: string | undefined): string | undefined {
51
+ if (!helpUrl) return undefined;
52
+ try {
53
+ const url = new URL(helpUrl);
54
+ if (url.protocol !== "https:") return undefined;
55
+ url.username = "";
56
+ url.password = "";
57
+ url.pathname = url.pathname
58
+ .split("/")
59
+ .map((segment) => {
60
+ const decoded = decodeURIComponent(segment);
61
+ const scrubbed = scrubSensitiveText(decoded, apiKey);
62
+ return scrubbed === decoded ? segment : encodeURIComponent(scrubbed);
63
+ })
64
+ .join("/");
65
+ const scrubbedParams = new URLSearchParams();
66
+ for (const [name, value] of url.searchParams) {
67
+ const scrubbedName = scrubSensitiveText(name, apiKey);
68
+ const scrubbedPair = scrubSensitiveText(`${name}=${value}`, apiKey);
69
+ scrubbedParams.append(scrubbedName, scrubbedPair === `${name}=${value}` ? value : "[redacted]");
70
+ }
71
+ url.search = scrubbedParams.toString();
72
+ if (url.hash) {
73
+ const decoded = decodeURIComponent(url.hash.slice(1));
74
+ const scrubbed = scrubSensitiveText(decoded, apiKey);
75
+ if (scrubbed !== decoded) url.hash = encodeURIComponent(scrubbed);
76
+ }
77
+ return scrubSensitiveText(url.href, apiKey).trim() || undefined;
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ function formatIntegrationNotConnectedError(
84
+ error: unknown,
85
+ record: Record<string, unknown> | undefined,
86
+ scrubbedMessage: string,
87
+ apiKey: string | undefined,
88
+ ): string | undefined {
89
+ if (getErrorName(error, record) !== "IntegrationNotConnectedError") return undefined;
90
+ const provider = scrubSensitiveText(getErrorStringField(record, "provider") ?? "", apiKey).trim();
91
+ if (!provider) return undefined;
92
+ const message = scrubbedMessage || "Cursor Cloud integration is not connected.";
93
+ const punctuation = /[.!?]$/.test(message) ? "" : ".";
94
+ const helpUrl = scrubHttpsHelpUrl(getErrorStringField(record, "helpUrl"), apiKey);
95
+ return `${message}${punctuation} Connect the ${provider} integration${helpUrl ? `: ${helpUrl}` : "."}`;
96
+ }
97
+
43
98
  function getErrorStack(error: unknown, record: Record<string, unknown> | undefined): string {
44
99
  return error instanceof Error ? error.stack ?? "" : getErrorStringField(record, "stack") ?? "";
45
100
  }
@@ -278,12 +333,27 @@ export function resolveCursorSdkAbortCause(options: {
278
333
  return "unknown";
279
334
  }
280
335
 
281
- export function sanitizeCursorProviderError(error: unknown, apiKey?: string): string {
336
+ export function sanitizeCursorProviderError(
337
+ error: unknown,
338
+ apiKey?: string,
339
+ runtimeTarget?: CursorRuntime,
340
+ ): string {
341
+ const record = asRecord(error);
282
342
  const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
283
343
  if (message === MISSING_CURSOR_API_KEY_MESSAGE) return MISSING_CURSOR_API_KEY_MESSAGE;
284
344
  const scrubbed = scrubSensitiveText(message, apiKey).trim();
345
+ const integrationMessage = runtimeTarget === "cloud"
346
+ ? formatIntegrationNotConnectedError(error, record, scrubbed, apiKey)
347
+ : undefined;
348
+ if (integrationMessage) return integrationMessage;
285
349
  const connectClassification = classifyCursorConnectError(error);
286
- if (connectClassification?.kind === "unauthenticated" || isLikelyAuthError(scrubbed)) return AUTH_CURSOR_SDK_ERROR_MESSAGE;
350
+ if (
351
+ (runtimeTarget === "cloud" && getErrorName(error, record) === "AuthenticationError") ||
352
+ connectClassification?.kind === "unauthenticated" ||
353
+ isLikelyAuthError(scrubbed)
354
+ ) {
355
+ return runtimeTarget === "cloud" ? CLOUD_AUTH_CURSOR_SDK_ERROR_MESSAGE : AUTH_CURSOR_SDK_ERROR_MESSAGE;
356
+ }
287
357
  if (connectClassification?.kind === "network" || isLikelyNetworkTimeout(scrubbed)) return NETWORK_CURSOR_SDK_ERROR_MESSAGE;
288
358
  if (isGenericCursorRunFailureMessage(scrubbed)) return RETRYABLE_CURSOR_RUN_FAILURE_PREFIX;
289
359
  if (isGenericErrorMessage(scrubbed)) return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
@@ -278,9 +278,10 @@ async function emitCursorLiveRunPendingToolUseTurn(
278
278
  if (options.mode === "emit") turn.emitter.closeAll();
279
279
  emitCursorNativeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, active, debugRecorder);
280
280
  } else {
281
+ const requests = cursorLiveRuns.collectBridgeToolBatch(run);
282
+ if (requests.length === 0) return "handled";
281
283
  if (!sdkTurnEnded) cursorLiveRuns.ignoreFutureSdkTurnUsage(run);
282
284
  if (options.mode === "emit") turn.emitter.closeAll();
283
- const requests = cursorLiveRuns.collectBridgeToolBatch(run);
284
285
  emitCursorBridgeToolUseTurn(stream, partial, model, context, run, toolResultInputTokens, requests);
285
286
  }
286
287
  return "tool_use";
@@ -10,6 +10,7 @@ import {
10
10
  resolveCursorSdkAbortCause,
11
11
  sanitizeCursorProviderError,
12
12
  } from "./cursor-provider-errors.js";
13
+ import type { CursorRuntime } from "./cursor-config.js";
13
14
  import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
14
15
  import {
15
16
  buildIncompleteCursorToolRunOutcome,
@@ -70,6 +71,7 @@ export interface CursorRunFinalizerParams {
70
71
  sdkEventDebug: () => CursorSdkEventDebugSink | undefined;
71
72
  sdkProcessErrorGuard: ReturnType<typeof installCursorSdkProcessErrorGuard>;
72
73
  resolvedApiKey: () => string | undefined;
74
+ runtimeTarget: () => CursorRuntime | undefined;
73
75
  }
74
76
 
75
77
  export interface StartCursorLiveRunCompletionParams {
@@ -112,7 +114,7 @@ export class CursorRunFinalizer {
112
114
  if (!liveRun.disposed) {
113
115
  cursorLiveRuns.markError(
114
116
  liveRun,
115
- sanitizeCursorProviderError(error, this.params.resolvedApiKey() ?? runnerParams.options?.apiKey),
117
+ sanitizeCursorProviderError(error, this.params.resolvedApiKey() ?? runnerParams.options?.apiKey, "local"),
116
118
  );
117
119
  }
118
120
  this.safeCleanup(() => sdkEventDebug?.recordWaitResult({ status: "error", error: String(error) }));
@@ -212,7 +214,11 @@ export class CursorRunFinalizer {
212
214
  this.pushTerminalError(
213
215
  this.params.runnerParams.partial,
214
216
  "error",
215
- sanitizeCursorProviderError(error, this.params.resolvedApiKey() ?? this.params.runnerParams.options?.apiKey),
217
+ sanitizeCursorProviderError(
218
+ error,
219
+ this.params.resolvedApiKey() ?? this.params.runnerParams.options?.apiKey,
220
+ prepared?.runtimeTarget ?? this.params.runtimeTarget(),
221
+ ),
216
222
  );
217
223
  }
218
224
  }
@@ -6,6 +6,7 @@ import {
6
6
  resolveCursorSdkAbortCause,
7
7
  sanitizeCursorProviderError,
8
8
  } from "./cursor-provider-errors.js";
9
+ import type { CursorRuntime } from "./cursor-config.js";
9
10
  import { hasUsableText } from "./cursor-record-utils.js";
10
11
  import {
11
12
  buildIncompleteCursorToolRunOutcome,
@@ -45,6 +46,7 @@ export interface ResolveCursorRunOutcomeParams {
45
46
  runErrorFallback?: RunError;
46
47
  resolvedApiKey?: string;
47
48
  optionsApiKey?: string;
49
+ runtimeTarget: CursorRuntime;
48
50
  }
49
51
 
50
52
  function hasCursorAssistantText(
@@ -104,7 +106,11 @@ export function resolveCursorRunOutcome(params: ResolveCursorRunOutcomeParams):
104
106
  status: "error",
105
107
  assistantTextProduced: false,
106
108
  }),
107
- errorMessage: sanitizeCursorProviderError(failureDetail, params.resolvedApiKey ?? params.optionsApiKey),
109
+ errorMessage: sanitizeCursorProviderError(
110
+ failureDetail,
111
+ params.resolvedApiKey ?? params.optionsApiKey,
112
+ params.runtimeTarget,
113
+ ),
108
114
  };
109
115
  }
110
116
 
@@ -1,4 +1,4 @@
1
- import type { RunError, SDKAgent } from "@cursor/sdk";
1
+ import type { LocalAgentStore, RunError, SDKAgent } from "@cursor/sdk";
2
2
  import { loadCursorTranscriptWebToolCallsAfterOffset } from "./cursor-agent-message-web-tools.js";
3
3
  import {
4
4
  collectCursorCloudRunReport,
@@ -18,11 +18,21 @@ import {
18
18
  import type { CursorProviderTurnPrepareResult } from "./cursor-provider-turn-types.js";
19
19
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
20
20
 
21
- export async function cacheSdkContextWindow(agentId: string, modelId: string, cwd?: string): Promise<void> {
21
+ export async function cacheSdkContextWindow(
22
+ agentId: string,
23
+ modelId: string,
24
+ cwd?: string,
25
+ store?: LocalAgentStore,
26
+ ): Promise<void> {
22
27
  try {
23
28
  const { createAgentPlatform } = await loadCursorSdk();
24
29
  const platform = await createAgentPlatform(
25
- cwd ? { workspaceRef: cwd, scopedWorkspaceRef: cwd } : undefined,
30
+ cwd || store
31
+ ? {
32
+ ...(cwd ? { workspaceRef: cwd, scopedWorkspaceRef: cwd } : {}),
33
+ ...(store ? { localStore: store } : {}),
34
+ }
35
+ : undefined,
26
36
  );
27
37
  const checkpoint = await platform.checkpointStore.loadLatest(agentId);
28
38
  const contextWindow = getCheckpointContextWindow(checkpoint);
@@ -57,6 +67,7 @@ export function buildCursorRunOutcomeFromWait(params: BuildCursorRunOutcomeParam
57
67
  runErrorFallback: params.runErrorFallback,
58
68
  resolvedApiKey: params.resolvedApiKey,
59
69
  optionsApiKey: params.optionsApiKey,
70
+ runtimeTarget: prepared.runtimeTarget,
60
71
  });
61
72
  }
62
73
 
@@ -64,6 +75,7 @@ async function replayCursorTranscriptWebToolCalls(
64
75
  agentId: string,
65
76
  cwd: string,
66
77
  messageOffset: number | undefined,
78
+ turnStore: LocalAgentStore,
67
79
  turnCoordinator: CursorSdkTurnCoordinator,
68
80
  sdkEventDebug: CursorSdkEventDebugSink | undefined,
69
81
  ): Promise<void> {
@@ -72,6 +84,7 @@ async function replayCursorTranscriptWebToolCalls(
72
84
  agentId,
73
85
  cwd,
74
86
  offset: messageOffset,
87
+ store: turnStore,
75
88
  });
76
89
  if (transcriptToolCalls.length === 0) return;
77
90
  sdkEventDebug?.recordCoordinatorEvent("cursor-transcript-web-tools", {
@@ -175,6 +188,7 @@ export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorR
175
188
  params.run.agentId,
176
189
  params.prepared.cwd,
177
190
  params.cursorAgentMessageOffset,
191
+ params.prepared.sessionAgentLease.store,
178
192
  params.prepared.runtime.turnCoordinator,
179
193
  params.sdkEventDebug,
180
194
  );
@@ -186,7 +200,12 @@ export async function awaitFinalizeCursorRunOutcome(params: AwaitFinalizeCursorR
186
200
  // Debug artifact failures must never affect provider execution.
187
201
  }
188
202
  if (params.prepared.runtimeTarget === "local" && params.cacheContextWindow !== false) {
189
- await cacheSdkContextWindow(params.contextWindowAgentId ?? params.run.agentId, params.modelId, params.prepared.cwd);
203
+ await cacheSdkContextWindow(
204
+ params.contextWindowAgentId ?? params.run.agentId,
205
+ params.modelId,
206
+ params.prepared.cwd,
207
+ params.prepared.sessionAgentLease.store,
208
+ );
190
209
  }
191
210
  return { outcome, displayOnlyTraceBlock };
192
211
  }
@@ -1,6 +1,8 @@
1
1
  import type { Context, SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
2
2
  import type { AgentModeOption, ModelSelection, SDKAgent } from "@cursor/sdk";
3
+ import { configureCursorSdkHttp1 } from "./cursor-http1.js";
3
4
  import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
5
+ import { ensureCursorRipgrepPath } from "./cursor-ripgrep-path.js";
4
6
  import { installCursorSdkOutputFilter, suppressCursorSdkOutput } from "./cursor-sdk-output-filter.js";
5
7
  import {
6
8
  acquireSessionCursorAgent,
@@ -32,9 +34,9 @@ import { getEffectiveCursorSettingSources } from "./cursor-setting-sources.js";
32
34
  import {
33
35
  formatCursorCloudPreflightError,
34
36
  buildCursorCloudAgentOptions,
35
- inspectCursorCloudLocalState,
36
37
  preflightCursorCloudRuntime,
37
38
  } from "./cursor-cloud-options.js";
39
+ import { inspectCursorCloudLocalState } from "./cursor-cloud-local-state.js";
38
40
  import { getCursorSessionName, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
39
41
  import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
40
42
  import {
@@ -127,7 +129,12 @@ async function prepareCursorCloudProviderTurn(
127
129
  try {
128
130
  const preflight = preflightCursorCloudRuntime({
129
131
  resolvedConfig,
130
- localState: inspectCursorCloudLocalState(cwd),
132
+ localState: resolvedConfig.cloud.allowLocalState.value
133
+ ? { insideGitRepo: false }
134
+ : inspectCursorCloudLocalState(cwd, {
135
+ repo: resolvedConfig.cloud.repo.value,
136
+ branch: resolvedConfig.cloud.branch.value,
137
+ }),
131
138
  hasPriorContext: context.messages.length > 1,
132
139
  });
133
140
  if (!preflight.ok) throw new Error(formatCursorCloudPreflightError(preflight));
@@ -241,11 +248,17 @@ async function prepareCursorLocalProviderTurn(
241
248
  let completed = false;
242
249
 
243
250
  try {
251
+ ensureCursorRipgrepPath();
244
252
  const localSafety = {
245
253
  autoReview: resolvedConfig.local.autoReview.value,
246
254
  sandboxEnabled: resolvedConfig.local.sandboxEnabled.value,
247
255
  };
248
- const { Agent } = await loadCursorSdk();
256
+ const sdk = await loadCursorSdk();
257
+ const { Agent } = sdk;
258
+ const useHttp1ForAgent = configureCursorSdkHttp1(
259
+ sdk,
260
+ resolvedConfig.local.useHttp1ForAgent,
261
+ );
249
262
 
250
263
  installCursorMcpToolTimeoutOverride();
251
264
  restoreCursorSdkOutputFilter = installCursorSdkOutputFilter();
@@ -261,6 +274,7 @@ async function prepareCursorLocalProviderTurn(
261
274
  settingSources,
262
275
  localSafety,
263
276
  localResume: resolvedConfig.local.resume.value,
277
+ useHttp1ForAgent,
264
278
  debugRecorder: sdkEventDebug,
265
279
  onBridgeToolRequest: (request: CursorPiBridgeToolRequest) => {
266
280
  if (liveRunForBridgeQueue && !liveRunForBridgeQueue.disposed) {
@@ -3,6 +3,7 @@ import { drainExistingCursorLiveRunBeforeSend } from "./cursor-provider-live-run
3
3
  import { invalidateSessionAgent } from "./cursor-session-agent.js";
4
4
  import { getCursorSessionCwd, getCursorSessionScopeKey } from "./cursor-session-scope.js";
5
5
  import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
6
+ import type { CursorRuntime } from "./cursor-config.js";
6
7
  import { CursorSdkEventDebugSink } from "./cursor-sdk-event-debug.js";
7
8
  import { awaitFinalizeCursorRunOutcome } from "./cursor-provider-turn-finalize.js";
8
9
  import {
@@ -38,6 +39,7 @@ function requireLocalLivePreparedTurn(prepared: CursorProviderTurnPrepareResult)
38
39
  export class CursorProviderTurnRunner {
39
40
  private sdkEventDebug: CursorSdkEventDebugSink | undefined;
40
41
  private resolvedApiKey: string | undefined;
42
+ private runtimeTarget: CursorRuntime | undefined;
41
43
 
42
44
  constructor(private readonly params: CursorProviderTurnRunnerParams) {}
43
45
 
@@ -59,6 +61,7 @@ export class CursorProviderTurnRunner {
59
61
  sdkEventDebug: () => this.sdkEventDebug,
60
62
  sdkProcessErrorGuard,
61
63
  resolvedApiKey: () => this.resolvedApiKey,
64
+ runtimeTarget: () => this.runtimeTarget,
62
65
  });
63
66
 
64
67
  try {
@@ -74,6 +77,7 @@ export class CursorProviderTurnRunner {
74
77
  // Resolved once here, before any drain await, so the drain decision and the
75
78
  // prepare dispatch below always act on the same config snapshot.
76
79
  const resolvedConfig = resolveCursorProviderTurnConfig(cwd);
80
+ this.runtimeTarget = resolvedConfig.runtime.value;
77
81
  if (resolvedConfig.runtime.value === "local") {
78
82
  // The observed local-executor closed-pipe EPIPE is contained only from this
79
83
  // turn's pre-send live-run drain through run completion; for live runs the
@@ -163,6 +167,7 @@ export class CursorProviderTurnRunner {
163
167
  sdkEventDebug: () => this.sdkEventDebug,
164
168
  sdkProcessErrorGuard: installCursorSdkProcessErrorGuard(),
165
169
  resolvedApiKey: () => this.resolvedApiKey,
170
+ runtimeTarget: () => this.runtimeTarget,
166
171
  });
167
172
  await runFinalizer.applyTerminalEvent({ kind: "error", prepared: undefined, error });
168
173
  await runFinalizer.cleanup(undefined, undefined, undefined);
@@ -76,7 +76,7 @@ export async function sendCursorProviderTurn(sendParams: SendCursorProviderTurnP
76
76
  let cursorAgentMessageOffset: number | undefined;
77
77
  if (prepared.runtimeTarget === "local") {
78
78
  try {
79
- cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd);
79
+ cursorAgentMessageOffset = await countCursorAgentMessages(agent.agentId, cwd, prepared.sessionAgentLease.store);
80
80
  } catch (error) {
81
81
  recordDebug(() => sdkEventDebug?.recordError("cursor_agent_message_count", error));
82
82
  }
@@ -2,11 +2,17 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
2
2
  import { Text } from "@earendil-works/pi-tui";
3
3
  import { Type } from "typebox";
4
4
  import { arePiToolsDisabled } from "./cursor-active-tools.js";
5
+ import { parseEnvBoolean } from "./cursor-env-boolean.js";
5
6
  import { isCursorModel } from "./cursor-model.js";
6
7
  import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
7
8
  import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
8
9
 
9
10
  export const CURSOR_ASK_QUESTION_TOOL_NAME = "cursor_ask_question";
11
+ export const CURSOR_ASK_QUESTION_ENV = "PI_CURSOR_ASK_QUESTION";
12
+
13
+ export function resolveCursorAskQuestionEnabled(env: Record<string, string | undefined> = process.env): boolean {
14
+ return parseEnvBoolean(env[CURSOR_ASK_QUESTION_ENV], true);
15
+ }
10
16
 
11
17
  interface CursorQuestionOption {
12
18
  label: string;
@@ -188,6 +194,8 @@ function syncCursorQuestionToolForModel(pi: Pick<ExtensionAPI, "getActiveTools"
188
194
  }
189
195
 
190
196
  export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi): void {
197
+ if (!resolveCursorAskQuestionEnabled()) return;
198
+
191
199
  pi.registerTool({
192
200
  name: CURSOR_ASK_QUESTION_TOOL_NAME,
193
201
  label: "Cursor question",
@@ -0,0 +1,32 @@
1
+ import { accessSync, constants } 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
+
7
+ export function resolveBundledCursorRipgrepPath(
8
+ fromModuleUrl: string | URL = import.meta.url,
9
+ ): string | undefined {
10
+ try {
11
+ const require = createRequire(fromModuleUrl);
12
+ const platformPackage = `@cursor/sdk-${process.platform}-${process.arch}`;
13
+ const sdkEntry = require.resolve("@cursor/sdk");
14
+ const packageDirectory = dirname(
15
+ require.resolve(`${platformPackage}/package.json`, { paths: [dirname(sdkEntry)] }),
16
+ );
17
+ const ripgrepPath = join(packageDirectory, "bin", process.platform === "win32" ? "rg.exe" : "rg");
18
+ accessSync(ripgrepPath, constants.X_OK);
19
+ return ripgrepPath;
20
+ } catch {
21
+ return undefined;
22
+ }
23
+ }
24
+
25
+ export function ensureCursorRipgrepPath(): string | undefined {
26
+ const configuredPath = process.env[RIPGREP_ENV];
27
+ if (configuredPath && isAbsolute(configuredPath)) return configuredPath;
28
+
29
+ const bundledPath = resolveBundledCursorRipgrepPath();
30
+ if (bundledPath) process.env[RIPGREP_ENV] = bundledPath;
31
+ return bundledPath;
32
+ }