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.
- package/CHANGELOG.md +58 -0
- package/README.md +59 -1
- package/docs/cursor-live-smoke-checklist.md +4 -1
- package/docs/cursor-model-ux-spec.md +7 -5
- package/docs/cursor-native-tool-replay.md +99 -3
- package/docs/cursor-testing-lessons.md +234 -5
- package/package.json +10 -2
- package/scripts/debug-provider-events.mjs +403 -0
- package/scripts/debug-sdk-events.mjs +413 -0
- package/scripts/lib/cursor-probe-utils.mjs +52 -0
- package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
- package/scripts/probe-mcp-coldstart.mjs +244 -0
- package/scripts/validate-smoke-jsonl.mjs +27 -3
- package/src/context.ts +45 -32
- package/src/cursor-agent-message-web-tools.ts +172 -0
- package/src/cursor-agents-context.ts +176 -0
- package/src/cursor-incomplete-tool-visibility.ts +124 -0
- package/src/cursor-live-run-coordinator.ts +18 -7
- package/src/cursor-mcp-timeout-override.ts +66 -11
- package/src/cursor-model.ts +12 -0
- package/src/cursor-native-tool-display-registration.ts +1 -4
- package/src/cursor-native-tool-display-replay.ts +65 -6
- package/src/cursor-native-tool-display-tools.ts +20 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
- package/src/cursor-pi-tool-bridge-run.ts +16 -1
- package/src/cursor-pi-tool-bridge-types.ts +3 -0
- package/src/cursor-provider-errors.ts +96 -0
- package/src/cursor-provider-live-run-drain.ts +181 -62
- package/src/cursor-provider-turn-coordinator.ts +220 -33
- package/src/cursor-provider.ts +302 -93
- package/src/cursor-question-tool.ts +1 -4
- package/src/cursor-sdk-abort-error-guard.ts +109 -0
- package/src/cursor-sdk-event-debug-constants.ts +40 -0
- package/src/cursor-sdk-event-debug-session.ts +163 -0
- package/src/cursor-sdk-event-debug.ts +602 -0
- package/src/cursor-sensitive-text.ts +27 -7
- package/src/cursor-session-agent.ts +279 -82
- package/src/cursor-session-send-policy.ts +43 -0
- package/src/cursor-setting-sources.ts +29 -0
- package/src/cursor-state.ts +1 -5
- package/src/cursor-tool-lifecycle.ts +85 -0
- package/src/cursor-tool-names.ts +39 -0
- package/src/cursor-tool-transcript.ts +4 -2
- package/src/cursor-tool-visibility.ts +63 -0
- package/src/cursor-transcript-tool-formatters.ts +228 -5
- package/src/cursor-transcript-tool-specs.ts +135 -24
- package/src/cursor-transcript-utils.ts +12 -0
- package/src/cursor-web-tool-activity.ts +84 -0
- 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 (
|
|
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)
|
|
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 (
|
|
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(
|
|
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
|
+
}
|