pi-cursor-sdk 0.1.18 → 0.1.20

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 (49) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +59 -1
  3. package/docs/cursor-live-smoke-checklist.md +4 -1
  4. package/docs/cursor-model-ux-spec.md +7 -5
  5. package/docs/cursor-native-tool-replay.md +99 -3
  6. package/docs/cursor-testing-lessons.md +234 -5
  7. package/package.json +10 -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/probe-mcp-coldstart.mjs +244 -0
  13. package/scripts/validate-smoke-jsonl.mjs +27 -3
  14. package/src/context.ts +45 -32
  15. package/src/cursor-agent-message-web-tools.ts +172 -0
  16. package/src/cursor-agents-context.ts +176 -0
  17. package/src/cursor-incomplete-tool-visibility.ts +124 -0
  18. package/src/cursor-live-run-coordinator.ts +18 -7
  19. package/src/cursor-mcp-timeout-override.ts +66 -11
  20. package/src/cursor-model.ts +12 -0
  21. package/src/cursor-native-tool-display-registration.ts +1 -4
  22. package/src/cursor-native-tool-display-replay.ts +65 -6
  23. package/src/cursor-native-tool-display-tools.ts +20 -0
  24. package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
  25. package/src/cursor-pi-tool-bridge-run.ts +16 -1
  26. package/src/cursor-pi-tool-bridge-types.ts +3 -0
  27. package/src/cursor-provider-errors.ts +96 -0
  28. package/src/cursor-provider-live-run-drain.ts +181 -62
  29. package/src/cursor-provider-turn-coordinator.ts +220 -33
  30. package/src/cursor-provider.ts +302 -93
  31. package/src/cursor-question-tool.ts +1 -4
  32. package/src/cursor-sdk-abort-error-guard.ts +109 -0
  33. package/src/cursor-sdk-event-debug-constants.ts +40 -0
  34. package/src/cursor-sdk-event-debug-session.ts +163 -0
  35. package/src/cursor-sdk-event-debug.ts +602 -0
  36. package/src/cursor-sensitive-text.ts +27 -7
  37. package/src/cursor-session-agent.ts +279 -82
  38. package/src/cursor-session-send-policy.ts +43 -0
  39. package/src/cursor-setting-sources.ts +29 -0
  40. package/src/cursor-state.ts +1 -5
  41. package/src/cursor-tool-lifecycle.ts +85 -0
  42. package/src/cursor-tool-names.ts +39 -0
  43. package/src/cursor-tool-transcript.ts +4 -2
  44. package/src/cursor-tool-visibility.ts +63 -0
  45. package/src/cursor-transcript-tool-formatters.ts +228 -5
  46. package/src/cursor-transcript-tool-specs.ts +135 -24
  47. package/src/cursor-transcript-utils.ts +12 -0
  48. package/src/cursor-web-tool-activity.ts +84 -0
  49. package/src/index.ts +4 -1
@@ -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,
@@ -34,6 +35,7 @@ export interface CursorReplayToolDetails {
34
35
  diff?: string;
35
36
  firstChangedLine?: number;
36
37
  expandedText?: string;
38
+ collapseDetailsByDefault?: boolean;
37
39
  }
38
40
 
39
41
  export function asCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
@@ -258,9 +260,27 @@ function getCursorReplayActivityTitle(toolName: CursorReplayToolName, args: Reco
258
260
  return getCursorReplayToolLabel(toolName);
259
261
  }
260
262
 
263
+ function formatReplayRecordingDurationMs(ms: number | undefined): string | undefined {
264
+ if (ms === undefined || !Number.isFinite(ms) || ms < 0) return undefined;
265
+ if (ms < 1000) return `${Math.round(ms)}ms`;
266
+ const seconds = ms / 1000;
267
+ return seconds < 60 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
268
+ }
269
+
270
+ function formatReplaySemSearchQuery(args: Record<string, unknown> | undefined): string | undefined {
271
+ const query = typeof args?.query === "string" ? args.query.trim() : undefined;
272
+ if (!query) return undefined;
273
+ const targetDirectories = Array.isArray(args?.targetDirectories)
274
+ ? args.targetDirectories.filter((entry): entry is string => typeof entry === "string")
275
+ : [];
276
+ const dirHint =
277
+ targetDirectories.length > 0 ? ` (${targetDirectories.length} dir${targetDirectories.length === 1 ? "" : "s"})` : "";
278
+ return `${query}${dirHint}`;
279
+ }
280
+
261
281
  function getCursorReplayCallSummary(toolName: CursorReplayToolName, args: Record<string, unknown> | undefined): string | undefined {
262
282
  const activitySummary = typeof args?.activitySummary === "string" && args.activitySummary.trim() ? args.activitySummary.trim() : undefined;
263
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME && activitySummary) return activitySummary;
283
+ if (activitySummary) return activitySummary;
264
284
 
265
285
  const path = typeof args?.path === "string" ? args.path : undefined;
266
286
  const description = typeof args?.description === "string" ? args.description : undefined;
@@ -271,19 +291,33 @@ function getCursorReplayCallSummary(toolName: CursorReplayToolName, args: Record
271
291
 
272
292
  if (toolName === "cursor_edit" || toolName === "cursor_write" || toolName === "cursor_delete") return path ?? "unknown";
273
293
  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"}`;
294
+ const target = paths.length > 0 ? paths.join(", ") : path;
295
+ if (target && diagnosticCount !== undefined) {
296
+ return `${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"} in ${target}`;
297
+ }
276
298
  return target;
277
299
  }
278
300
  if (toolName === "cursor_update_todos" || toolName === "cursor_create_plan") {
279
301
  return totalCount !== undefined ? `${totalCount} item${totalCount === 1 ? "" : "s"}` : undefined;
280
302
  }
281
303
  if (toolName === "cursor_task") return description;
282
- if (toolName === "cursor_generate_image") return prompt;
304
+ if (toolName === "cursor_generate_image") return path ?? prompt;
283
305
  if (toolName === "cursor_mcp") return typeof args?.toolName === "string" ? args.toolName : undefined;
306
+ if (toolName === "cursor_sem_search") return formatReplaySemSearchQuery(args);
307
+ if (toolName === "cursor_record_screen") {
308
+ const duration = formatReplayRecordingDurationMs(
309
+ typeof args?.recordingDurationMs === "number" ? args.recordingDurationMs : undefined,
310
+ );
311
+ if (path && duration) return `${path} · ${duration}`;
312
+ if (path) return path;
313
+ if (typeof args?.mode === "string") return args.mode;
314
+ }
315
+ if (toolName === "cursor_web_search") return typeof args?.query === "string" ? args.query : undefined;
316
+ if (toolName === "cursor_web_fetch") return typeof args?.url === "string" ? args.url : undefined;
284
317
  if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) {
285
- if (typeof args?.path === "string") return args.path;
318
+ if (path) return path;
286
319
  if (typeof args?.toolName === "string") return args.toolName;
320
+ return formatReplaySemSearchQuery(args);
287
321
  }
288
322
  return undefined;
289
323
  }
@@ -373,7 +407,7 @@ function renderExpandableCursorReplayResult(
373
407
  const summary = details?.summary ?? text.split("\n").find((line) => line.trim()) ?? "completed";
374
408
  let rendered = `${theme.fg("toolTitle", theme.bold(title))} ${theme.fg(isError ? "error" : "success", summary)}`;
375
409
  const expandedText = details?.expandedText ?? (text.includes("\n") ? text : undefined);
376
- if (expandedText) {
410
+ if (expandedText && (options.expanded || !details?.collapseDetailsByDefault)) {
377
411
  const preview = options.expanded ? formatMutedBlock(expandedText, theme) : formatCursorReplayPreview(expandedText, theme);
378
412
  if (preview) rendered += `\n${preview}`;
379
413
  }
@@ -440,6 +474,31 @@ export function renderCursorReplayResult(
440
474
  return new Text(text || theme.fg("success", "Cursor tool result replayed"), 0, 0);
441
475
  }
442
476
 
477
+ export function renderNativeLookingCursorReadReplayResult(
478
+ result: Parameters<CursorReplayRenderResult>[0],
479
+ options: Parameters<CursorReplayRenderResult>[1],
480
+ theme: Parameters<CursorReplayRenderResult>[2],
481
+ context: Parameters<CursorReplayRenderResult>[3],
482
+ renderBase: () => Component | undefined,
483
+ ): Component {
484
+ const base = renderBase?.() ?? new Text("", 0, 0);
485
+ const readArgs = context.args as Record<string, unknown> | undefined;
486
+ const replayDetails = result.details as Record<string, unknown> | undefined;
487
+ const usesLocalPreview =
488
+ readArgs?.localReadPreview === true ||
489
+ replayDetails?.localReadPreview === true ||
490
+ isLocalReadPreviewContent(firstContentText(result));
491
+ if (usesLocalPreview && !options.expanded && !context.isError) {
492
+ const noticeText = `\n${theme.fg("warning", LOCAL_READ_PREVIEW_NOTICE)}`;
493
+ if (base instanceof Text) {
494
+ base.setText(noticeText);
495
+ return base;
496
+ }
497
+ return new Text(noticeText, 0, 0);
498
+ }
499
+ return base;
500
+ }
501
+
443
502
  export function createCursorReplayOnlyToolDefinition(toolName: CursorReplayToolName): ToolDefinition<typeof cursorReplayToolSchema, unknown> {
444
503
  const cursorToolName = toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "activity" : getCursorReplaySourceToolName(toolName);
445
504
  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
+ }