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
@@ -9,7 +9,26 @@ import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js
9
9
  import { asRecord, getField, hasUsableText } from "./cursor-record-utils.js";
10
10
  import { scrubPiToolDisplay, scrubSensitiveText } from "./cursor-sensitive-text.js";
11
11
  import { buildCursorPiToolDisplay, formatCursorToolTranscript, getCursorCreatePlanText, mergeCursorToolCalls } from "./cursor-tool-transcript.js";
12
- import { getString, getToolArgs, getToolName } from "./cursor-transcript-utils.js";
12
+ import {
13
+ recordDiscardedIncompleteStartedToolCall,
14
+ type CursorSdkEventDebugRecorder,
15
+ DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
16
+ } from "./cursor-sdk-event-debug.js";
17
+ import {
18
+ buildIncompleteCursorToolDisplay,
19
+ buildIncompleteCursorToolRunOutcome,
20
+ formatIncompleteCursorToolTrace,
21
+ resolveIncompleteCursorToolVisibility,
22
+ type IncompleteCursorToolRunOutcome,
23
+ type IncompleteCursorToolDiscardReason,
24
+ } from "./cursor-incomplete-tool-visibility.js";
25
+ import { getToolName } from "./cursor-transcript-utils.js";
26
+ import {
27
+ CURSOR_TOOL_LIFECYCLE_DEFER_MS,
28
+ formatCursorToolLifecycleProgressText,
29
+ isCursorToolLifecycleEligible,
30
+ } from "./cursor-tool-lifecycle.js";
31
+ import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
13
32
 
14
33
  function formatCursorToolName(toolCall: unknown): string {
15
34
  return truncateCursorDisplayLine(getToolName(toolCall), 80) || "unknown";
@@ -25,20 +44,12 @@ interface CursorShellOutputDeltas {
25
44
  stderr: string[];
26
45
  }
27
46
 
28
- function isCursorShellToolCall(toolCall: unknown): boolean {
29
- const normalizedName = getToolName(toolCall).replace(/\s+/g, " ").trim().toLowerCase();
30
- return normalizedName === "shell" || normalizedName === "run_terminal_cmd" || normalizedName === "terminal" || normalizedName === "bash";
31
- }
32
-
33
- function isCursorTaskToolCall(toolCall: unknown): boolean {
34
- return getToolName(toolCall).replace(/\s+/g, " ").trim().toLowerCase() === "task";
47
+ function getNormalizedCursorToolName(toolCall: unknown): string {
48
+ return classifyCursorToolVisibility(toolCall).normalizedName;
35
49
  }
36
50
 
37
- function extractCursorTaskProgressLabel(toolCall: unknown, apiKey?: string): string | undefined {
38
- if (!isCursorTaskToolCall(toolCall)) return undefined;
39
- const description = getString(getToolArgs(toolCall), "description");
40
- if (!description?.trim()) return undefined;
41
- return truncateCursorDisplayLine(scrubSensitiveText(description, apiKey));
51
+ function isCursorShellToolCall(toolCall: unknown): boolean {
52
+ return classifyCursorToolVisibility(toolCall).normalizedKey === "shell";
42
53
  }
43
54
 
44
55
  function getCursorShellOutputDelta(update: InteractionUpdate): CursorShellOutputDelta | undefined {
@@ -83,13 +94,15 @@ function mergeShellOutputDeltasIntoCursorToolCall(toolCall: unknown, deltas: Cur
83
94
  };
84
95
  }
85
96
 
97
+ type CursorToolDisplaySource = "started" | "fallback" | "transcript";
98
+
86
99
  type ToolCompletionResolution =
87
100
  | { action: "ignore-bridge"; identity?: string }
88
101
  | {
89
102
  action: "handle";
90
103
  toolCall: unknown;
91
104
  identity?: string;
92
- source?: "started" | "fallback";
105
+ source?: CursorToolDisplaySource;
93
106
  matchedStartedCallId?: string;
94
107
  };
95
108
 
@@ -103,6 +116,7 @@ export interface CursorSdkTurnCoordinatorOptions {
103
116
  activeToolNames?: ReadonlySet<string>;
104
117
  nativeReplayId: string;
105
118
  textDeltas: string[];
119
+ debugRecorder?: CursorSdkEventDebugRecorder;
106
120
  }
107
121
 
108
122
  export class CursorSdkTurnCoordinator {
@@ -117,6 +131,7 @@ export class CursorSdkTurnCoordinator {
117
131
  readonly textDeltas: string[];
118
132
 
119
133
  private readonly contentEmitter: CursorPartialContentEmitter;
134
+ private readonly debugRecorder?: CursorSdkEventDebugRecorder;
120
135
  private nativeToolDisplayCounter = 0;
121
136
  private nativeToolReplayStarted = false;
122
137
  private cursorPlanTextCandidate: string | undefined;
@@ -128,7 +143,8 @@ export class CursorSdkTurnCoordinator {
128
143
  private readonly completedToolIdentities = new Set<string>();
129
144
  private readonly completedStartedToolFingerprints = new Set<string>();
130
145
  private readonly completedFallbackToolFingerprints = new Set<string>();
131
- private readonly emittedTaskProgressCallIds = new Set<string>();
146
+ private readonly emittedLifecycleCallIds = new Set<string>();
147
+ private readonly lifecycleTimers = new Map<string, ReturnType<typeof setTimeout>>();
132
148
 
133
149
  constructor(options: CursorSdkTurnCoordinatorOptions) {
134
150
  this.stream = options.stream;
@@ -140,6 +156,7 @@ export class CursorSdkTurnCoordinator {
140
156
  this.activeToolNames = options.activeToolNames;
141
157
  this.nativeReplayId = options.nativeReplayId;
142
158
  this.textDeltas = options.textDeltas;
159
+ this.debugRecorder = options.debugRecorder;
143
160
  this.contentEmitter = new CursorPartialContentEmitter(options.stream, options.partial, undefined, false);
144
161
  }
145
162
 
@@ -151,13 +168,40 @@ export class CursorSdkTurnCoordinator {
151
168
  return this.nativeToolReplayStarted;
152
169
  }
153
170
 
154
- discardIncompleteStartedToolCalls(): void {
171
+ discardIncompleteStartedToolCalls(
172
+ outcome: IncompleteCursorToolRunOutcome = buildIncompleteCursorToolRunOutcome(),
173
+ ): void {
174
+ for (const [callId, toolCall] of this.startedToolCalls) {
175
+ if (typeof callId !== "string") continue;
176
+ const toolName = getNormalizedCursorToolName(toolCall);
177
+ recordDiscardedIncompleteStartedToolCall(this.debugRecorder, process.env, {
178
+ toolName,
179
+ callId,
180
+ reason: outcome.reason,
181
+ });
182
+ const visibilityDecision = resolveIncompleteCursorToolVisibility(toolCall, outcome);
183
+ if (visibilityDecision !== "emit") {
184
+ this.recordDisplayDecision({
185
+ action: "skip-incomplete-fast-local",
186
+ toolName,
187
+ source: "started",
188
+ reason:
189
+ visibilityDecision === "debugOnly" && outcome.assistantTextProduced
190
+ ? "successful-run-text-produced"
191
+ : visibilityDecision,
192
+ });
193
+ continue;
194
+ }
195
+ this.emitIncompleteStartedToolCall(toolCall, outcome.reason);
196
+ }
155
197
  this.startedToolCalls.clear();
156
198
  this.bridgeStartedToolCallIds.clear();
157
199
  this.activeShellCallIds.clear();
158
200
  this.ambiguousShellOutputCallIds.clear();
159
201
  this.shellOutputDeltasByCallId.clear();
160
- this.emittedTaskProgressCallIds.clear();
202
+ this.emittedLifecycleCallIds.clear();
203
+ for (const timer of this.lifecycleTimers.values()) clearTimeout(timer);
204
+ this.lifecycleTimers.clear();
161
205
  }
162
206
 
163
207
  closeTraceBlock(): void {
@@ -195,14 +239,14 @@ export class CursorSdkTurnCoordinator {
195
239
  return;
196
240
  }
197
241
  if (update.type === "partial-tool-call") {
198
- this.maybeEmitCursorTaskProgress(update.callId, update.toolCall);
242
+ this.maybeScheduleCursorToolLifecycle(update.callId, update.toolCall);
199
243
  return;
200
244
  }
201
245
  if (update.type === "tool-call-started") {
202
246
  if (this.liveRun?.bridgeRun?.isBridgeMcpToolCall(update.toolCall)) {
203
247
  if (typeof update.callId === "string") this.bridgeStartedToolCallIds.add(update.callId);
204
248
  } else {
205
- this.maybeEmitCursorTaskProgress(update.callId, update.toolCall);
249
+ this.maybeScheduleCursorToolLifecycle(update.callId, update.toolCall);
206
250
  this.startedToolCalls.set(update.callId, update.toolCall);
207
251
  if (isCursorShellToolCall(update.toolCall)) this.activeShellCallIds.add(update.callId);
208
252
  }
@@ -215,7 +259,10 @@ export class CursorSdkTurnCoordinator {
215
259
  toolCall: update.toolCall,
216
260
  startedToolCall: this.startedToolCalls.get(update.callId),
217
261
  });
218
- if (resolution.action === "ignore-bridge") return;
262
+ if (resolution.action === "ignore-bridge") {
263
+ this.recordIgnoreBridgeDecision(resolution.identity, getToolName(update.toolCall), "delta");
264
+ return;
265
+ }
219
266
  this.handleCompletedToolCall(resolution.toolCall, {
220
267
  identity: resolution.identity,
221
268
  source: resolution.source,
@@ -251,7 +298,10 @@ export class CursorSdkTurnCoordinator {
251
298
  callId: stepId,
252
299
  toolCall,
253
300
  });
254
- if (resolution.action === "ignore-bridge") return;
301
+ if (resolution.action === "ignore-bridge") {
302
+ this.recordIgnoreBridgeDecision(resolution.identity, getToolName(toolCall), "step");
303
+ return;
304
+ }
255
305
  this.handleCompletedToolCall(resolution.toolCall, {
256
306
  identity: resolution.identity,
257
307
  source: resolution.source,
@@ -319,20 +369,52 @@ export class CursorSdkTurnCoordinator {
319
369
  };
320
370
  }
321
371
 
372
+ handleTranscriptCompletedToolCalls(toolCalls: readonly { identity: string; toolCall: unknown }[]): void {
373
+ for (const { identity, toolCall } of toolCalls) {
374
+ this.handleCompletedToolCall(toolCall, { identity, source: "transcript" });
375
+ }
376
+ }
377
+
322
378
  private handleCompletedToolCall(
323
379
  toolCall: unknown,
324
- options: { identity?: string; source?: "started" | "fallback" } = {},
380
+ options: { identity?: string; source?: CursorToolDisplaySource } = {},
325
381
  ): void {
326
382
  const planText = getCursorCreatePlanText(toolCall);
327
383
  if (planText) this.cursorPlanTextCandidate = scrubSensitiveText(planText, this.resolvedApiKey);
328
384
 
329
385
  const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd: this.cwd }), this.resolvedApiKey);
330
386
  const display = buildCursorPiToolDisplay(toolCall, { cwd: this.cwd });
387
+ const toolName = display.toolName;
331
388
  const fingerprint = this.getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
332
- if (options.identity && this.completedToolIdentities.has(options.identity)) return;
389
+ if (options.identity && this.completedToolIdentities.has(options.identity)) {
390
+ this.recordDisplayDecision({
391
+ action: "skip-duplicate",
392
+ toolName,
393
+ identity: options.identity,
394
+ source: options.source,
395
+ reason: "identity-already-completed",
396
+ });
397
+ return;
398
+ }
333
399
  if (options.source === "started") {
334
- if (this.completedFallbackToolFingerprints.has(fingerprint)) return;
400
+ if (this.completedFallbackToolFingerprints.has(fingerprint)) {
401
+ this.recordDisplayDecision({
402
+ action: "skip-duplicate",
403
+ toolName,
404
+ identity: options.identity,
405
+ source: options.source,
406
+ reason: "fallback-fingerprint-already-completed",
407
+ });
408
+ return;
409
+ }
335
410
  } else if (this.completedStartedToolFingerprints.has(fingerprint) || this.completedFallbackToolFingerprints.has(fingerprint)) {
411
+ this.recordDisplayDecision({
412
+ action: "skip-duplicate",
413
+ toolName,
414
+ identity: options.identity,
415
+ source: options.source,
416
+ reason: "fingerprint-already-completed",
417
+ });
336
418
  return;
337
419
  }
338
420
  if (options.identity) this.completedToolIdentities.add(options.identity);
@@ -353,6 +435,15 @@ export class CursorSdkTurnCoordinator {
353
435
  this.nativeToolReplayStarted = true;
354
436
  const id = `${this.nativeReplayId}-tool-${++this.nativeToolDisplayCounter}`;
355
437
  const scrubbedDisplay = scrubPiToolDisplay(display, this.resolvedApiKey);
438
+ this.recordDisplayDecision({
439
+ action: "queue_replay",
440
+ disposition,
441
+ toolName,
442
+ identity: options.identity,
443
+ source: options.source,
444
+ transcript,
445
+ replayToolId: id,
446
+ });
356
447
  cursorLiveRuns.queueEvent(this.liveRun, {
357
448
  type: "tool",
358
449
  tool: { ...scrubbedDisplay, id },
@@ -362,11 +453,84 @@ export class CursorSdkTurnCoordinator {
362
453
 
363
454
  const traceText =
364
455
  disposition === "inactive_trace"
365
- ? formatInactiveCursorReplayTrace(display)
456
+ ? formatInactiveCursorReplayTrace(scrubPiToolDisplay(display, this.resolvedApiKey))
366
457
  : transcript || `Cursor tool: ${formatCursorToolName(toolCall)} completed`;
458
+ this.recordDisplayDecision({
459
+ action: "emit_trace",
460
+ disposition,
461
+ toolName,
462
+ identity: options.identity,
463
+ source: options.source,
464
+ transcript,
465
+ traceText,
466
+ });
367
467
  this.emitCursorToolTrace(traceText);
368
468
  }
369
469
 
470
+ private emitIncompleteStartedToolCall(toolCall: unknown, reason: IncompleteCursorToolDiscardReason): void {
471
+ const display = scrubPiToolDisplay(
472
+ buildIncompleteCursorToolDisplay(toolCall, reason, { apiKey: this.resolvedApiKey }),
473
+ this.resolvedApiKey,
474
+ );
475
+ const toolName = display.toolName;
476
+ const disposition = resolveNativeReplayDisposition({
477
+ toolName,
478
+ useNativeToolReplay: this.useNativeToolReplay,
479
+ activeToolNames: this.activeToolNames,
480
+ hasLiveRun: this.liveRun !== undefined,
481
+ });
482
+
483
+ // Aborted live runs emit trace visibility only; do not synthesize a toolUse replay turn.
484
+ if (disposition === "queue_replay" && this.liveRun && reason !== "abort") {
485
+ this.nativeToolReplayStarted = true;
486
+ const id = `${this.nativeReplayId}-tool-${++this.nativeToolDisplayCounter}`;
487
+ this.recordDisplayDecision({
488
+ action: "queue_replay",
489
+ disposition,
490
+ toolName,
491
+ source: "started",
492
+ reason: "incomplete-started-tool-call",
493
+ replayToolId: id,
494
+ });
495
+ cursorLiveRuns.queueEvent(this.liveRun, {
496
+ type: "tool",
497
+ tool: { ...display, id },
498
+ });
499
+ return;
500
+ }
501
+
502
+ const traceText =
503
+ disposition === "inactive_trace"
504
+ ? formatInactiveCursorReplayTrace(display)
505
+ : formatIncompleteCursorToolTrace(display);
506
+ this.recordDisplayDecision({
507
+ action: "emit_trace",
508
+ disposition,
509
+ toolName,
510
+ source: "started",
511
+ reason: "incomplete-started-tool-call",
512
+ traceText,
513
+ });
514
+ this.emitCursorToolTrace(traceText);
515
+ }
516
+
517
+ private recordIgnoreBridgeDecision(
518
+ identity: string | undefined,
519
+ toolName: string,
520
+ source: "delta" | "step",
521
+ ): void {
522
+ this.debugRecorder?.recordDisplayDecision({
523
+ action: "ignore-bridge",
524
+ toolName,
525
+ identity,
526
+ source,
527
+ });
528
+ }
529
+
530
+ private recordDisplayDecision(decision: Parameters<CursorSdkEventDebugRecorder["recordDisplayDecision"]>[0]): void {
531
+ this.debugRecorder?.recordDisplayDecision(decision);
532
+ }
533
+
370
534
  private emitCursorToolTrace(text: string): void {
371
535
  const traceText = text.endsWith("\n") ? text : `${text}\n`;
372
536
  if (this.liveRun) {
@@ -377,17 +541,39 @@ export class CursorSdkTurnCoordinator {
377
541
  this.contentEmitter.appendThinkingBlock(traceText);
378
542
  }
379
543
 
380
- private maybeEmitCursorTaskProgress(callId: unknown, toolCall: unknown): void {
381
- if (typeof callId !== "string" || this.emittedTaskProgressCallIds.has(callId)) return;
544
+ private maybeScheduleCursorToolLifecycle(callId: unknown, toolCall: unknown): void {
545
+ if (typeof callId !== "string" || this.emittedLifecycleCallIds.has(callId)) return;
382
546
  if (this.liveRun?.bridgeRun?.isBridgeMcpToolCall(toolCall)) return;
383
- const label = extractCursorTaskProgressLabel(toolCall, this.resolvedApiKey);
384
- if (!label) return;
385
- this.emittedTaskProgressCallIds.add(callId);
386
- this.emitCursorTaskProgress(label);
547
+ if (!isCursorToolLifecycleEligible(toolCall)) return;
548
+
549
+ this.cancelCursorToolLifecycleTimer(callId);
550
+ const timer = setTimeout(() => {
551
+ this.lifecycleTimers.delete(callId);
552
+ if (!this.startedToolCalls.has(callId)) return;
553
+ if (this.emittedLifecycleCallIds.has(callId)) return;
554
+ this.emitCursorToolLifecycle(callId, toolCall);
555
+ }, CURSOR_TOOL_LIFECYCLE_DEFER_MS);
556
+ timer.unref?.();
557
+ this.lifecycleTimers.set(callId, timer);
387
558
  }
388
559
 
389
- private emitCursorTaskProgress(label: string): void {
390
- const progressText = `Cursor task: ${label}\n`;
560
+ private cancelCursorToolLifecycleTimer(callId: string): void {
561
+ const timer = this.lifecycleTimers.get(callId);
562
+ if (!timer) return;
563
+ clearTimeout(timer);
564
+ this.lifecycleTimers.delete(callId);
565
+ }
566
+
567
+ private emitCursorToolLifecycle(callId: string, toolCall: unknown): void {
568
+ const progressText = formatCursorToolLifecycleProgressText(toolCall, this.resolvedApiKey);
569
+ if (!progressText) return;
570
+ this.emittedLifecycleCallIds.add(callId);
571
+ this.debugRecorder?.recordCoordinatorEvent("tool_lifecycle", {
572
+ callId,
573
+ toolName: getNormalizedCursorToolName(toolCall),
574
+ progressText,
575
+ liveRun: this.liveRun !== undefined,
576
+ });
391
577
  if (this.liveRun) {
392
578
  cursorLiveRuns.queueEvent(this.liveRun, { type: "thinking-delta", text: progressText });
393
579
  return;
@@ -408,6 +594,7 @@ export class CursorSdkTurnCoordinator {
408
594
  }
409
595
 
410
596
  private clearStartedToolCall(callId: string): void {
597
+ this.cancelCursorToolLifecycleTimer(callId);
411
598
  this.startedToolCalls.delete(callId);
412
599
  this.bridgeStartedToolCallIds.delete(callId);
413
600
  this.activeShellCallIds.delete(callId);