pi-cursor-sdk 0.1.6 → 0.1.7

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.7 - 2026-05-10
6
+
7
+ ### Fixed
8
+
9
+ - Preserve Cursor post-tool thinking and text that arrive before a native replay tool-use turn closes.
10
+ - Count prompt input only once when one Cursor SDK run is split across multiple native replay turns.
11
+ - Tighten native replay registration tests and documentation around registration opt-out behavior.
12
+
5
13
  ## 0.1.6 - 2026-05-10
6
14
 
7
15
  ### Fixed
@@ -17,8 +17,8 @@ Current implementation notes:
17
17
  - Cursor auth uses pi-native API-key resolution for provider `cursor`: CLI `--api-key`, stored `~/.pi/agent/auth.json` API key from `/login`, then `CURSOR_API_KEY`. The extension config file stores only non-secret Cursor-only state such as fast defaults.
18
18
  - Local agents do not pass `settingSources` by default because the current Cursor SDK writes setting/rule loading INFO logs directly to terminal output, which corrupts pi's TUI.
19
19
  - Cursor SDK models are treated as thinking-capable even when pi reports `thinking=no`; that pi column only means the SDK did not expose a pi-controllable thinking parameter for that model.
20
- - Cursor-side thinking remains visible. Cursor internal tool activity is recorded from SDK events and scrubbed. In interactive TTY sessions, supported completed `read`, `bash`, and `ls` activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so the TUI can show native green cards without forcing Cursor to call pi tools or rerunning Cursor's reads/shell commands. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. When these native cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK completions arrive: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later Cursor tool batches as further `toolUse` turns, then Cursor's final assistant answer. Non-interactive runs keep bounded scrubbed transcript output instead, preserving `pi -p` assistant text output. Cursor text deltas stream live when native tool replay is not active.
21
- - Cursor SDK usage events report cumulative internal agent/tool/cache work, not the replayable pi prompt context. The extension reports approximate prompt/output usage for pi context display and compaction decisions instead of copying raw Cursor SDK usage.
20
+ - Cursor-side thinking remains visible. Cursor internal tool activity is recorded from SDK events and scrubbed. In interactive TTY sessions, supported completed `read`, `bash`, and `ls` activity is replayed through pi's native tool-call rendering path with recorded Cursor results, so the TUI can show native green cards without forcing Cursor to call pi tools or rerunning Cursor's reads/shell commands. Native replay wrappers are registered only for tool names not already owned by another extension; conflicting tools use the bounded scrubbed transcript fallback. `PI_CURSOR_NATIVE_TOOL_DISPLAY=0` disables native replay, and `PI_CURSOR_REGISTER_NATIVE_TOOLS=0` is a registration-only opt-out that keeps the transcript fallback without shadowing pi tool names. When these native cards are emitted, the provider mirrors Codex's turn shape as Cursor SDK completions arrive: assistant `toolUse`, pi `toolResult`s, live post-tool Cursor thinking/text, any later Cursor tool batches as further `toolUse` turns, then Cursor's final assistant answer. Non-interactive runs keep bounded scrubbed transcript output instead, preserving `pi -p` assistant text output. Cursor text deltas stream live when native tool replay is not active.
21
+ - Cursor SDK usage events report cumulative internal agent/tool/cache work, not the replayable pi prompt context. The extension reports approximate prompt/output usage for pi context display and compaction decisions instead of copying raw Cursor SDK usage. When native replay splits one Cursor SDK run into multiple pi turns, prompt input is counted once for the run; later synthetic replay turns report `input: 0` and only their own output estimate.
22
22
  - For models without a catalog `context` parameter, context windows are not hardcoded. The extension ships a bundled SDK-derived default/non-Max cache generated from `createAgentPlatform().checkpointStore.loadLatest(agentId).tokenDetails.maxTokens`. Successful runs can update a local override cache, but model discovery does not probe models at startup.
23
23
  - Max Mode context windows are distinct from default/non-Max context windows. `@cursor/sdk` 1.0.12 exposes internal protobuf fields named `maxMode`/`max_mode`, but the public `ModelSelection` type and the local executor path do not pass a Max Mode selector for local agent runs. Do not advertise Max Mode context windows unless the SDK catalog exposes an exact parameter/variant or the SDK public API adds a Max Mode selector that the extension actually sends.
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "pi provider extension backed by @cursor/sdk local agents",
5
5
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
6
6
  "license": "MIT",
@@ -12,6 +12,7 @@ import type { CursorPiToolDisplay } from "./cursor-tool-transcript.js";
12
12
  const NATIVE_CURSOR_TOOL_NAMES = ["read", "bash", "ls"] as const;
13
13
  type NativeCursorToolName = (typeof NATIVE_CURSOR_TOOL_NAMES)[number];
14
14
  const NATIVE_CURSOR_TOOL_DISPLAY_ENV = "PI_CURSOR_NATIVE_TOOL_DISPLAY";
15
+ // Registration-only kill switch for users who want transcript fallback without shadowing read/bash/ls.
15
16
  const NATIVE_CURSOR_TOOL_REGISTRATION_ENV = "PI_CURSOR_REGISTER_NATIVE_TOOLS";
16
17
 
17
18
  export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {
@@ -19,8 +20,7 @@ export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {
19
20
  terminate?: boolean;
20
21
  }
21
22
 
22
- let nativeToolDisplayEnabled = false;
23
- const registeredNativeToolNames = new Set<string>();
23
+ const registeredNativeToolNames = new Set<NativeCursorToolName>();
24
24
  const nativeToolResults = new Map<string, CursorNativeToolDisplayItem>();
25
25
 
26
26
  function readBooleanEnv(name: string): boolean | undefined {
@@ -36,13 +36,16 @@ function isCursorNativeToolDisplayRequested(): boolean {
36
36
  return process.stdout.isTTY === true;
37
37
  }
38
38
 
39
+ function isNativeCursorToolName(toolName: string): toolName is NativeCursorToolName {
40
+ return NATIVE_CURSOR_TOOL_NAMES.some((nativeToolName) => nativeToolName === toolName);
41
+ }
42
+
39
43
  function isCursorNativeToolRegistrationRequested(): boolean {
40
- if (readBooleanEnv(NATIVE_CURSOR_TOOL_REGISTRATION_ENV) === false) return false;
41
- return isCursorNativeToolDisplayRequested();
44
+ return readBooleanEnv(NATIVE_CURSOR_TOOL_REGISTRATION_ENV) !== false && isCursorNativeToolDisplayRequested();
42
45
  }
43
46
 
44
47
  export function isCursorNativeToolDisplayEnabled(): boolean {
45
- return nativeToolDisplayEnabled;
48
+ return registeredNativeToolNames.size > 0;
46
49
  }
47
50
 
48
51
  export function isCursorNativeToolDisplayRuntimeEnabled(): boolean {
@@ -50,7 +53,7 @@ export function isCursorNativeToolDisplayRuntimeEnabled(): boolean {
50
53
  }
51
54
 
52
55
  export function canRenderCursorToolNatively(toolName: string): boolean {
53
- return registeredNativeToolNames.has(toolName);
56
+ return isNativeCursorToolName(toolName) && registeredNativeToolNames.has(toolName);
54
57
  }
55
58
 
56
59
  export function recordCursorNativeToolDisplay(item: CursorNativeToolDisplayItem): void {
@@ -66,7 +69,6 @@ function consumeCursorNativeToolDisplay(id: string): CursorNativeToolDisplayItem
66
69
 
67
70
  export const __testUtils = {
68
71
  reset(): void {
69
- nativeToolDisplayEnabled = false;
70
72
  registeredNativeToolNames.clear();
71
73
  nativeToolResults.clear();
72
74
  },
@@ -103,15 +105,13 @@ function registerNativeCursorTool(pi: ExtensionAPI, toolName: NativeCursorToolNa
103
105
  pi.registerTool(wrapNativeCursorTool(createLsToolDefinition(cwd)));
104
106
  }
105
107
 
106
- function getExistingToolOwner(pi: ExtensionAPI, toolName: NativeCursorToolName): string | undefined {
108
+ function hasNonBuiltinTool(pi: ExtensionAPI, toolName: NativeCursorToolName): boolean {
107
109
  const existingTool = pi.getAllTools().find((tool) => tool.name === toolName);
108
- if (!existingTool || existingTool.sourceInfo.source === "builtin") return undefined;
109
- return existingTool.sourceInfo.path ?? existingTool.sourceInfo.source;
110
+ return existingTool !== undefined && existingTool.sourceInfo.source !== "builtin";
110
111
  }
111
112
 
112
113
  function registerAvailableNativeCursorTools(pi: ExtensionAPI, ctx: ExtensionContext): void {
113
114
  if (!isCursorNativeToolRegistrationRequested()) {
114
- nativeToolDisplayEnabled = false;
115
115
  registeredNativeToolNames.clear();
116
116
  return;
117
117
  }
@@ -120,8 +120,7 @@ function registerAvailableNativeCursorTools(pi: ExtensionAPI, ctx: ExtensionCont
120
120
  const skippedToolNames: string[] = [];
121
121
  for (const toolName of NATIVE_CURSOR_TOOL_NAMES) {
122
122
  if (registeredNativeToolNames.has(toolName)) continue;
123
- const existingOwner = getExistingToolOwner(pi, toolName);
124
- if (existingOwner) {
123
+ if (hasNonBuiltinTool(pi, toolName)) {
125
124
  skippedToolNames.push(toolName);
126
125
  continue;
127
126
  }
@@ -129,7 +128,6 @@ function registerAvailableNativeCursorTools(pi: ExtensionAPI, ctx: ExtensionCont
129
128
  registeredNativeToolNames.add(toolName);
130
129
  }
131
130
 
132
- nativeToolDisplayEnabled = registeredNativeToolNames.size > 0;
133
131
  if (skippedToolNames.length > 0 && readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) === true && ctx.hasUI) {
134
132
  ctx.ui.notify(
135
133
  `Cursor native tool replay skipped for ${skippedToolNames.join(", ")} because another extension already provides ${skippedToolNames.length === 1 ? "that tool" : "those tools"}. Cursor will use scrubbed activity transcripts for skipped tools.`,
@@ -139,7 +137,7 @@ function registerAvailableNativeCursorTools(pi: ExtensionAPI, ctx: ExtensionCont
139
137
  }
140
138
 
141
139
  export function registerCursorNativeToolDisplay(pi: ExtensionAPI): void {
142
- pi.on("session_start", async (_event, ctx) => {
140
+ pi.on("session_start", (_event, ctx) => {
143
141
  registerAvailableNativeCursorTools(pi, ctx);
144
142
  });
145
143
  }
@@ -70,6 +70,7 @@ interface CursorNativeLiveRun {
70
70
  id: string;
71
71
  agent: SDKAgent;
72
72
  promptInputTokens: number;
73
+ promptInputTokensReported: boolean;
73
74
  pendingEvents: CursorNativeQueuedEvent[];
74
75
  textDeltas: string[];
75
76
  finalText?: string;
@@ -374,6 +375,13 @@ function collectCursorNativeToolBatch(run: CursorNativeLiveRun): CursorNativeToo
374
375
  return tools;
375
376
  }
376
377
 
378
+ function takeCursorNativePromptInputTokens(run: CursorNativeLiveRun): number {
379
+ // Native replay can split one Cursor run into multiple pi turns; count prompt input once.
380
+ if (run.promptInputTokensReported) return 0;
381
+ run.promptInputTokensReported = true;
382
+ return run.promptInputTokens;
383
+ }
384
+
377
385
  function emitCursorNativeToolUseTurn(
378
386
  stream: AssistantMessageEventStream,
379
387
  partial: AssistantMessage,
@@ -396,7 +404,7 @@ function emitCursorNativeToolUseTurn(
396
404
  if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
397
405
  recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate });
398
406
  }
399
- setApproximateUsage(partial, run.promptInputTokens, outputText);
407
+ setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
400
408
  partial.stopReason = "toolUse";
401
409
  stream.push({ type: "done", reason: "toolUse", message: partial });
402
410
  }
@@ -455,7 +463,7 @@ async function emitCursorNativeRunNextTurn(
455
463
  if (!outputText) {
456
464
  outputText += await emitTextDeltas(stream, partial, splitTextIntoReplayDeltas(run.finalText ?? run.textDeltas.join("")));
457
465
  }
458
- setApproximateUsage(partial, run.promptInputTokens, outputText);
466
+ setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
459
467
  partial.stopReason = "stop";
460
468
  stream.push({ type: "done", reason: "stop", message: partial });
461
469
  await disposeCursorNativeRun(run);
@@ -534,12 +542,13 @@ export function streamCursor(
534
542
  const useNativeToolReplay = isCursorNativeToolDisplayRuntimeEnabled();
535
543
  const nativeReplayId = createCursorNativeReplayId();
536
544
  const textDeltas: string[] = [];
537
- let liveStreamClosed = false;
545
+ let nativeToolReplayStarted = false;
538
546
  const liveRun: CursorNativeLiveRun | undefined = useNativeToolReplay
539
547
  ? {
540
548
  id: nativeReplayId,
541
549
  agent,
542
550
  promptInputTokens,
551
+ promptInputTokensReported: false,
543
552
  pendingEvents: [],
544
553
  textDeltas,
545
554
  done: false,
@@ -651,6 +660,7 @@ export function streamCursor(
651
660
  completedToolFingerprints.add(fingerprint);
652
661
 
653
662
  if (useNativeToolReplay && canRenderCursorToolNatively(display.toolName) && liveRun) {
663
+ nativeToolReplayStarted = true;
654
664
  const id = `${nativeReplayId}-tool-${++nativeToolDisplayCounter}`;
655
665
  queueCursorNativeEvent(liveRun, {
656
666
  type: "tool",
@@ -672,19 +682,19 @@ export function streamCursor(
672
682
 
673
683
  if (update.type === "text-delta") {
674
684
  textDeltas.push(update.text);
675
- if (liveRun && liveStreamClosed) {
685
+ if (liveRun && nativeToolReplayStarted) {
676
686
  queueCursorNativeEvent(liveRun, { type: "text-delta", text: update.text });
677
687
  } else if (!useNativeToolReplay) {
678
688
  appendLiveTextDelta(update.text);
679
689
  }
680
690
  } else if (update.type === "thinking-delta") {
681
- if (liveRun && liveStreamClosed) {
691
+ if (liveRun && nativeToolReplayStarted) {
682
692
  queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: update.text });
683
693
  } else {
684
694
  appendTraceDelta(update.text);
685
695
  }
686
696
  } else if (update.type === "thinking-completed") {
687
- if (liveRun && liveStreamClosed) {
697
+ if (liveRun && nativeToolReplayStarted) {
688
698
  queueCursorNativeEvent(liveRun, { type: "thinking-completed" });
689
699
  } else {
690
700
  closeTraceBlock();
@@ -697,7 +707,7 @@ export function streamCursor(
697
707
  handleCompletedToolCall(mergedToolCall);
698
708
  } else if (update.type === "summary") {
699
709
  const summary = `Cursor summary: ${truncateSingleLine(update.summary)}\n`;
700
- if (liveRun && liveStreamClosed) {
710
+ if (liveRun && nativeToolReplayStarted) {
701
711
  queueCursorNativeEvent(liveRun, { type: "thinking-delta", text: summary });
702
712
  } else {
703
713
  appendTraceDelta(summary);
@@ -754,7 +764,6 @@ export function streamCursor(
754
764
  await waitForCursorNativeRunProgress(liveRun, options?.signal);
755
765
  await settleCursorNativeToolBatch(liveRun);
756
766
  closeTraceBlock();
757
- liveStreamClosed = true;
758
767
  await emitCursorNativeRunNextTurn(stream, partial, liveRun, options?.signal);
759
768
  agent = null;
760
769
  return;