pi-cursor-sdk 0.1.18 → 0.1.19

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 (46) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +37 -0
  3. package/docs/cursor-live-smoke-checklist.md +3 -0
  4. package/docs/cursor-model-ux-spec.md +4 -3
  5. package/docs/cursor-native-tool-replay.md +96 -2
  6. package/docs/cursor-testing-lessons.md +234 -5
  7. package/package.json +8 -2
  8. package/scripts/debug-provider-events.mjs +403 -0
  9. package/scripts/debug-sdk-events.mjs +413 -0
  10. package/scripts/lib/cursor-probe-utils.mjs +52 -0
  11. package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
  12. package/scripts/validate-smoke-jsonl.mjs +27 -3
  13. package/src/context.ts +45 -32
  14. package/src/cursor-agent-message-web-tools.ts +172 -0
  15. package/src/cursor-agents-context.ts +176 -0
  16. package/src/cursor-incomplete-tool-visibility.ts +118 -0
  17. package/src/cursor-live-run-coordinator.ts +18 -7
  18. package/src/cursor-model.ts +12 -0
  19. package/src/cursor-native-tool-display-registration.ts +1 -4
  20. package/src/cursor-native-tool-display-replay.ts +63 -5
  21. package/src/cursor-native-tool-display-tools.ts +20 -0
  22. package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
  23. package/src/cursor-pi-tool-bridge-run.ts +16 -1
  24. package/src/cursor-pi-tool-bridge-types.ts +3 -0
  25. package/src/cursor-provider-errors.ts +96 -0
  26. package/src/cursor-provider-live-run-drain.ts +181 -62
  27. package/src/cursor-provider-turn-coordinator.ts +198 -32
  28. package/src/cursor-provider.ts +270 -83
  29. package/src/cursor-question-tool.ts +1 -4
  30. package/src/cursor-sdk-abort-error-guard.ts +109 -0
  31. package/src/cursor-sdk-event-debug-constants.ts +40 -0
  32. package/src/cursor-sdk-event-debug-session.ts +163 -0
  33. package/src/cursor-sdk-event-debug.ts +597 -0
  34. package/src/cursor-sensitive-text.ts +27 -7
  35. package/src/cursor-session-agent.ts +25 -3
  36. package/src/cursor-session-send-policy.ts +43 -0
  37. package/src/cursor-setting-sources.ts +29 -0
  38. package/src/cursor-state.ts +1 -5
  39. package/src/cursor-tool-lifecycle.ts +111 -0
  40. package/src/cursor-tool-names.ts +12 -0
  41. package/src/cursor-tool-transcript.ts +4 -2
  42. package/src/cursor-transcript-tool-formatters.ts +228 -5
  43. package/src/cursor-transcript-tool-specs.ts +113 -14
  44. package/src/cursor-transcript-utils.ts +12 -0
  45. package/src/cursor-web-tool-activity.ts +84 -0
  46. package/src/index.ts +4 -1
@@ -6,6 +6,7 @@ import {
6
6
  registerNativeCursorTool,
7
7
  type NativeCursorToolName,
8
8
  } from "./cursor-native-tool-display-tools.js";
9
+ import { isCursorModel } from "./cursor-model.js";
9
10
  import {
10
11
  isCursorNativeToolDisplayRequested,
11
12
  isCursorNativeToolRegistrationRequested,
@@ -33,10 +34,6 @@ function hasNonBuiltinTool(pi: Pick<ExtensionAPI, "getAllTools">, toolName: Nati
33
34
 
34
35
  type NativeRegistrationContext = { hasUI: boolean; ui: Pick<ExtensionContext["ui"], "notify">; model?: ExtensionContext["model"] };
35
36
 
36
- function isCursorModel(model: ExtensionContext["model"]): boolean {
37
- return model?.provider === "cursor" || model?.api === "cursor-sdk";
38
- }
39
-
40
37
  export function syncRegisteredNativeCursorToolsForModel(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">, model: ExtensionContext["model"]): void {
41
38
  if (registeredNativeToolNames.size === 0) return;
42
39
  const activeToolNames = new Set(pi.getActiveTools());
@@ -4,6 +4,7 @@ import { getLanguageFromPath, highlightCode, type ToolDefinition } from "@earend
4
4
  import { Image, Text, type Component } from "@earendil-works/pi-tui";
5
5
  import { Type } from "typebox";
6
6
  import { resolveCursorEditDiff } from "./cursor-edit-diff.js";
7
+ import { LOCAL_READ_PREVIEW_NOTICE, isLocalReadPreviewContent } from "./cursor-transcript-utils.js";
7
8
  import {
8
9
  CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
9
10
  getCursorReplayDisplayLabel,
@@ -258,9 +259,27 @@ function getCursorReplayActivityTitle(toolName: CursorReplayToolName, args: Reco
258
259
  return getCursorReplayToolLabel(toolName);
259
260
  }
260
261
 
262
+ function formatReplayRecordingDurationMs(ms: number | undefined): string | undefined {
263
+ if (ms === undefined || !Number.isFinite(ms) || ms < 0) return undefined;
264
+ if (ms < 1000) return `${Math.round(ms)}ms`;
265
+ const seconds = ms / 1000;
266
+ return seconds < 60 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
267
+ }
268
+
269
+ function formatReplaySemSearchQuery(args: Record<string, unknown> | undefined): string | undefined {
270
+ const query = typeof args?.query === "string" ? args.query.trim() : undefined;
271
+ if (!query) return undefined;
272
+ const targetDirectories = Array.isArray(args?.targetDirectories)
273
+ ? args.targetDirectories.filter((entry): entry is string => typeof entry === "string")
274
+ : [];
275
+ const dirHint =
276
+ targetDirectories.length > 0 ? ` (${targetDirectories.length} dir${targetDirectories.length === 1 ? "" : "s"})` : "";
277
+ return `${query}${dirHint}`;
278
+ }
279
+
261
280
  function getCursorReplayCallSummary(toolName: CursorReplayToolName, args: Record<string, unknown> | undefined): string | undefined {
262
281
  const activitySummary = typeof args?.activitySummary === "string" && args.activitySummary.trim() ? args.activitySummary.trim() : undefined;
263
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME && activitySummary) return activitySummary;
282
+ if (activitySummary) return activitySummary;
264
283
 
265
284
  const path = typeof args?.path === "string" ? args.path : undefined;
266
285
  const description = typeof args?.description === "string" ? args.description : undefined;
@@ -271,19 +290,33 @@ function getCursorReplayCallSummary(toolName: CursorReplayToolName, args: Record
271
290
 
272
291
  if (toolName === "cursor_edit" || toolName === "cursor_write" || toolName === "cursor_delete") return path ?? "unknown";
273
292
  if (toolName === "cursor_read_lints") {
274
- const target = paths.length > 0 ? paths.join(" ") : path;
275
- if (target && diagnosticCount !== undefined) return `${target} · ${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"}`;
293
+ const target = paths.length > 0 ? paths.join(", ") : path;
294
+ if (target && diagnosticCount !== undefined) {
295
+ return `${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"} in ${target}`;
296
+ }
276
297
  return target;
277
298
  }
278
299
  if (toolName === "cursor_update_todos" || toolName === "cursor_create_plan") {
279
300
  return totalCount !== undefined ? `${totalCount} item${totalCount === 1 ? "" : "s"}` : undefined;
280
301
  }
281
302
  if (toolName === "cursor_task") return description;
282
- if (toolName === "cursor_generate_image") return prompt;
303
+ if (toolName === "cursor_generate_image") return path ?? prompt;
283
304
  if (toolName === "cursor_mcp") return typeof args?.toolName === "string" ? args.toolName : undefined;
305
+ if (toolName === "cursor_sem_search") return formatReplaySemSearchQuery(args);
306
+ if (toolName === "cursor_record_screen") {
307
+ const duration = formatReplayRecordingDurationMs(
308
+ typeof args?.recordingDurationMs === "number" ? args.recordingDurationMs : undefined,
309
+ );
310
+ if (path && duration) return `${path} · ${duration}`;
311
+ if (path) return path;
312
+ if (typeof args?.mode === "string") return args.mode;
313
+ }
314
+ if (toolName === "cursor_web_search") return typeof args?.query === "string" ? args.query : undefined;
315
+ if (toolName === "cursor_web_fetch") return typeof args?.url === "string" ? args.url : undefined;
284
316
  if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) {
285
- if (typeof args?.path === "string") return args.path;
317
+ if (path) return path;
286
318
  if (typeof args?.toolName === "string") return args.toolName;
319
+ return formatReplaySemSearchQuery(args);
287
320
  }
288
321
  return undefined;
289
322
  }
@@ -440,6 +473,31 @@ export function renderCursorReplayResult(
440
473
  return new Text(text || theme.fg("success", "Cursor tool result replayed"), 0, 0);
441
474
  }
442
475
 
476
+ export function renderNativeLookingCursorReadReplayResult(
477
+ result: Parameters<CursorReplayRenderResult>[0],
478
+ options: Parameters<CursorReplayRenderResult>[1],
479
+ theme: Parameters<CursorReplayRenderResult>[2],
480
+ context: Parameters<CursorReplayRenderResult>[3],
481
+ renderBase: () => Component | undefined,
482
+ ): Component {
483
+ const base = renderBase?.() ?? new Text("", 0, 0);
484
+ const readArgs = context.args as Record<string, unknown> | undefined;
485
+ const replayDetails = result.details as Record<string, unknown> | undefined;
486
+ const usesLocalPreview =
487
+ readArgs?.localReadPreview === true ||
488
+ replayDetails?.localReadPreview === true ||
489
+ isLocalReadPreviewContent(firstContentText(result));
490
+ if (usesLocalPreview && !options.expanded && !context.isError) {
491
+ const noticeText = `\n${theme.fg("warning", LOCAL_READ_PREVIEW_NOTICE)}`;
492
+ if (base instanceof Text) {
493
+ base.setText(noticeText);
494
+ return base;
495
+ }
496
+ return new Text(noticeText, 0, 0);
497
+ }
498
+ return base;
499
+ }
500
+
443
501
  export function createCursorReplayOnlyToolDefinition(toolName: CursorReplayToolName): ToolDefinition<typeof cursorReplayToolSchema, unknown> {
444
502
  const cursorToolName = toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "activity" : getCursorReplaySourceToolName(toolName);
445
503
  const sideEffectDescription = toolName === "cursor_edit" || toolName === "cursor_write" || toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "file mutations" : "real tool work";
@@ -21,6 +21,7 @@ import {
21
21
  createCursorReplayOnlyToolDefinition,
22
22
  renderCursorReplayResult,
23
23
  renderNativeLookingCursorFileMutationCall,
24
+ renderNativeLookingCursorReadReplayResult,
24
25
  } from "./cursor-native-tool-display-replay.js";
25
26
  import {
26
27
  consumeCursorNativeToolDisplay,
@@ -65,6 +66,20 @@ export function wrapNativeCursorTool<TParams extends TSchema, TDetails, TState>(
65
66
  return getCurrentDefinition().execute(toolCallId, params, signal, onUpdate, ctx);
66
67
  },
67
68
  renderCall(args, theme, context) {
69
+ if (definition.name === "read" && isCursorReplayToolCallId(context.toolCallId)) {
70
+ const currentRenderCall = getCurrentDefinition().renderCall;
71
+ const rendered = currentRenderCall ? currentRenderCall(args, theme, context) : new Text("", 0, 0);
72
+ if ((args as Record<string, unknown>).localReadPreview === true && !context.expanded) {
73
+ const baseText = rendered.render(120).join("\n").trimEnd();
74
+ const labeled = `${baseText}${theme.fg("muted", " · local file preview")}`;
75
+ if (rendered instanceof Text) {
76
+ rendered.setText(labeled);
77
+ return rendered;
78
+ }
79
+ return new Text(labeled, 0, 0);
80
+ }
81
+ return rendered;
82
+ }
68
83
  if (isCursorFileMutationToolName(definition.name) && isCursorReplayToolCallId(context.toolCallId)) {
69
84
  return renderNativeLookingCursorFileMutationCall(definition.name, args as Record<string, unknown>, theme, context.isPartial);
70
85
  }
@@ -76,6 +91,11 @@ export function wrapNativeCursorTool<TParams extends TSchema, TDetails, TState>(
76
91
  if (isCursorFileMutationToolName(definition.name) && details?.cursorToolName === definition.name) {
77
92
  return renderCursorReplayResult(result, options, theme, context, context.isError);
78
93
  }
94
+ if (definition.name === "read" && isCursorReplayToolCallId(context.toolCallId)) {
95
+ return renderNativeLookingCursorReadReplayResult(result, options, theme, context, () =>
96
+ getCurrentDefinition().renderResult?.(result, options, theme, context),
97
+ );
98
+ }
79
99
  const currentRenderResult = getCurrentDefinition().renderResult;
80
100
  return currentRenderResult ? currentRenderResult(result, options, theme, context) : new Text("", 0, 0);
81
101
  },
@@ -1,5 +1,6 @@
1
1
  import { stableNameHash } from "./cursor-pi-tool-bridge-mcp.js";
2
2
  import { parseEnvBoolean } from "./cursor-env-boolean.js";
3
+ import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
3
4
 
4
5
  export const CURSOR_PI_TOOL_BRIDGE_DEBUG_ENV = "PI_CURSOR_PI_TOOL_BRIDGE_DEBUG";
5
6
  export const CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX = "[pi-cursor-sdk:bridge]";
@@ -169,7 +170,16 @@ export function serializeCursorPiToolBridgeDiagnostic(event: CursorPiToolBridgeD
169
170
  return assertNeverDiagnosticEvent(event);
170
171
  }
171
172
 
172
- export function writeCursorPiToolBridgeDiagnostic(env: Record<string, string | undefined>, event: CursorPiToolBridgeDiagnosticEvent): void {
173
+ export function writeCursorPiToolBridgeDiagnostic(
174
+ env: Record<string, string | undefined>,
175
+ event: CursorPiToolBridgeDiagnosticEvent,
176
+ debugRecorder?: CursorSdkEventDebugRecorder,
177
+ ): void {
178
+ try {
179
+ debugRecorder?.recordBridgeDiagnostic(event);
180
+ } catch {
181
+ // Diagnostics must never affect bridge execution.
182
+ }
173
183
  if (!resolveCursorPiToolBridgeDebugEnabled(env)) return;
174
184
  try {
175
185
  process.stderr.write(`${CURSOR_PI_TOOL_BRIDGE_DIAGNOSTIC_PREFIX} ${JSON.stringify(serializeCursorPiToolBridgeDiagnostic(event))}\n`);
@@ -67,6 +67,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
67
67
  private readonly pendingByBridgeCallId = new Map<string, PendingBridgeCall>();
68
68
  private readonly pendingByCursorMcpCallId = new Map<string, PendingBridgeCall>();
69
69
  private onToolRequest?: (request: CursorPiBridgeToolRequest) => void;
70
+ private debugRecorder: CursorPiToolBridgeRunOptions["debugRecorder"];
70
71
  private liveRunHandlerDetached = false;
71
72
  private mcpServer?: McpProtocolServer;
72
73
  private mcpTransport?: StreamableHTTPServerTransport;
@@ -85,6 +86,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
85
86
  this.snapshot = snapshot;
86
87
  this.enabled = enabled;
87
88
  this.onToolRequest = options.onToolRequest;
89
+ this.debugRecorder = options.debugRecorder;
88
90
  this.id = `cursor-pi-bridge-run-${randomUUID()}`;
89
91
  this.endpointPath = `${MCP_ENDPOINT_ROOT}/${randomUUID()}/mcp`;
90
92
  this.knownMcpToolNames = new Set(snapshot.tools.map((tool) => tool.mcpToolName));
@@ -146,6 +148,10 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
146
148
  }
147
149
  }
148
150
 
151
+ setDebugRecorder(recorder?: CursorPiToolBridgeRunOptions["debugRecorder"]): void {
152
+ this.debugRecorder = recorder;
153
+ }
154
+
149
155
  resolveToolResults(toolResults: readonly ToolResultMessage[]): void {
150
156
  for (const toolResult of toolResults) {
151
157
  const pending = this.pendingByPiToolCallId.get(toolResult.toolCallId);
@@ -290,9 +296,11 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
290
296
  }
291
297
  this.queuedRequests.push(request);
292
298
  this.emitRequestQueuedDiagnostic(request);
299
+ this.debugRecorder?.recordBridgeRaw({ kind: "queued", request });
293
300
  return;
294
301
  }
295
302
  this.emitRequestQueuedDiagnostic(request);
303
+ this.debugRecorder?.recordBridgeRaw({ kind: "queued", request });
296
304
  this.dispatchPendingToolRequest(pending, this.onToolRequest);
297
305
  });
298
306
  }
@@ -321,6 +329,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
321
329
  pending.settled = true;
322
330
  this.removePending(pending);
323
331
  this.emitRequestResolvedDiagnostic(pending.request, result.isError === true);
332
+ this.debugRecorder?.recordBridgeRaw({ kind: "resolved", request: pending.request, result });
324
333
  pending.resolve(result);
325
334
  }
326
335
 
@@ -329,6 +338,12 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
329
338
  pending.settled = true;
330
339
  this.removePending(pending);
331
340
  this.emitRequestRejectedDiagnostic(pending.request, kind);
341
+ this.debugRecorder?.recordBridgeRaw({
342
+ kind: "rejected",
343
+ request: pending.request,
344
+ error: error.message,
345
+ rejectionKind: kind,
346
+ });
332
347
  pending.reject(error);
333
348
  }
334
349
 
@@ -366,7 +381,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
366
381
  }
367
382
 
368
383
  private emitDiagnostic(event: CursorPiToolBridgeDiagnosticEvent): void {
369
- writeCursorPiToolBridgeDiagnostic(this.env, event);
384
+ writeCursorPiToolBridgeDiagnostic(this.env, event, this.debugRecorder);
370
385
  }
371
386
 
372
387
  private pendingCount(): number {
@@ -1,5 +1,6 @@
1
1
  import type { McpServerConfig } from "@cursor/sdk";
2
2
  import type { Context, ToolResultMessage } from "@earendil-works/pi-ai";
3
+ import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
3
4
  import type {
4
5
  ExtensionAPI,
5
6
  ExtensionHandler,
@@ -64,6 +65,7 @@ export interface CursorPiToolBridgeRun {
64
65
  hasPendingPiToolCallId(piToolCallId: string): boolean;
65
66
  isBridgeMcpToolCall(toolCall: unknown): boolean;
66
67
  setOnToolRequest(handler?: (request: CursorPiBridgeToolRequest) => void): void;
68
+ setDebugRecorder(recorder?: CursorSdkEventDebugRecorder): void;
67
69
  cancel(reason: string): void;
68
70
  dispose(): Promise<void>;
69
71
  }
@@ -77,4 +79,5 @@ export interface CursorPiToolBridge {
77
79
 
78
80
  export interface CursorPiToolBridgeRunOptions {
79
81
  onToolRequest?: (request: CursorPiBridgeToolRequest) => void;
82
+ debugRecorder?: CursorSdkEventDebugRecorder;
80
83
  }
@@ -0,0 +1,96 @@
1
+ import type { RunResult } from "@cursor/sdk";
2
+ import { scrubSensitiveText } from "./cursor-sensitive-text.js";
3
+
4
+ export const MISSING_CURSOR_API_KEY_MESSAGE =
5
+ "Cursor SDK runs require a Cursor API key. Run /login -> Use an API key -> Cursor, set CURSOR_API_KEY before starting pi, or restart pi with --api-key.";
6
+ const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
7
+ "Cursor SDK request failed. The API key may be missing, invalid, or unauthorized. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
8
+ const AUTH_CURSOR_SDK_ERROR_MESSAGE =
9
+ "Cursor SDK request failed because the API key may be invalid or unauthorized. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
10
+ const NETWORK_CURSOR_SDK_ERROR_MESSAGE =
11
+ "Cursor SDK request timed out during network I/O. Check your connection and retry; if this keeps happening, try again later or verify Cursor service availability.";
12
+
13
+ const GENERIC_CURSOR_RUN_FAILURE_TEXT = "cursor sdk run failed";
14
+
15
+ export type CursorSdkRunFailureSource = Pick<RunResult, "id" | "status" | "durationMs" | "model" | "result">;
16
+
17
+ function isGenericErrorMessage(message: string): boolean {
18
+ const normalized = message.trim().toLowerCase();
19
+ return normalized === "" || normalized === "error" || normalized === "unknown error";
20
+ }
21
+
22
+ function isKnownGenericRunFailureText(message: string): boolean {
23
+ const normalized = message.trim().toLowerCase();
24
+ return normalized === "" || normalized === GENERIC_CURSOR_RUN_FAILURE_TEXT || isGenericErrorMessage(normalized);
25
+ }
26
+
27
+ function isLikelyAuthError(message: string): boolean {
28
+ return /\b(unauthorized|unauthorised|forbidden|invalid api key|invalid key|authentication|auth|401|403)\b/i.test(message);
29
+ }
30
+
31
+ function isLikelyNetworkTimeout(message: string): boolean {
32
+ return (
33
+ /\b(ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENETUNREACH|EAI_AGAIN)\b/i.test(message) ||
34
+ /\bConnectError\b.*\b(unavailable|deadline|timeout|timed out)\b/i.test(message) ||
35
+ /\bread ETIMEDOUT\b/i.test(message)
36
+ );
37
+ }
38
+
39
+ function shortRunId(runId: string): string {
40
+ const trimmed = runId.trim();
41
+ if (trimmed.length <= 12) return trimmed;
42
+ return `${trimmed.slice(0, 8)}…`;
43
+ }
44
+
45
+ export function formatCursorSdkRunFailureDetail(result: CursorSdkRunFailureSource, runResult?: string): string {
46
+ const fromWait = result.result?.trim();
47
+ if (fromWait && !isKnownGenericRunFailureText(fromWait)) {
48
+ return fromWait;
49
+ }
50
+ const fromRun = runResult?.trim();
51
+ if (fromRun && !isKnownGenericRunFailureText(fromRun)) {
52
+ return fromRun;
53
+ }
54
+
55
+ const parts = ["Cursor SDK run failed"];
56
+ if (result.model?.id) parts.push(`model ${result.model.id}`);
57
+ parts.push(`run ${shortRunId(result.id)}`);
58
+ if (typeof result.durationMs === "number") parts.push(`${result.durationMs}ms`);
59
+ return parts.join(" · ");
60
+ }
61
+
62
+ export type CursorSdkAbortCause = "user_interrupt" | "sdk_cancelled" | "live_run_disposed" | "unknown";
63
+
64
+ export function formatCursorSdkAbortMessage(cause: CursorSdkAbortCause): string {
65
+ switch (cause) {
66
+ case "user_interrupt":
67
+ return "Cancelled: prompt interrupted.";
68
+ case "sdk_cancelled":
69
+ return "Cancelled: Cursor SDK run was cancelled.";
70
+ case "live_run_disposed":
71
+ return "Cancelled: Cursor SDK live run ended before completion.";
72
+ case "unknown":
73
+ return "Cancelled: Cursor SDK run aborted.";
74
+ }
75
+ }
76
+
77
+ export function resolveCursorSdkAbortCause(options: {
78
+ signalAborted?: boolean;
79
+ sdkStatusCancelled?: boolean;
80
+ liveRunDisposed?: boolean;
81
+ }): CursorSdkAbortCause {
82
+ if (options.signalAborted) return "user_interrupt";
83
+ if (options.sdkStatusCancelled) return "sdk_cancelled";
84
+ if (options.liveRunDisposed) return "live_run_disposed";
85
+ return "unknown";
86
+ }
87
+
88
+ export function sanitizeCursorProviderError(error: unknown, apiKey?: string): string {
89
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
90
+ if (message === MISSING_CURSOR_API_KEY_MESSAGE) return MISSING_CURSOR_API_KEY_MESSAGE;
91
+ const scrubbed = scrubSensitiveText(message, apiKey).trim();
92
+ if (isGenericErrorMessage(scrubbed)) return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
93
+ if (isLikelyAuthError(scrubbed)) return AUTH_CURSOR_SDK_ERROR_MESSAGE;
94
+ if (isLikelyNetworkTimeout(scrubbed)) return NETWORK_CURSOR_SDK_ERROR_MESSAGE;
95
+ return scrubbed || GENERIC_CURSOR_SDK_ERROR_MESSAGE;
96
+ }