pi-cursor-sdk 0.4.0 → 0.5.0

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 (125) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +65 -13
  3. package/dist/context-window-cache.js +0 -6
  4. package/dist/context.js +8 -0
  5. package/dist/cursor-cloud-lifecycle.js +122 -98
  6. package/dist/cursor-durable-fs.js +97 -3
  7. package/dist/cursor-fallback-models.generated.js +288 -1
  8. package/dist/cursor-fallback-warning.js +2 -2
  9. package/dist/cursor-http1.js +8 -8
  10. package/dist/cursor-live-run-coordinator.js +9 -13
  11. package/dist/cursor-native-replay-routing.js +1 -1
  12. package/dist/cursor-native-tool-display-registration.js +46 -22
  13. package/dist/cursor-native-tool-display-state.js +57 -22
  14. package/dist/cursor-native-tool-display-tools.js +5 -6
  15. package/dist/cursor-pi-tool-bridge-constants.js +23 -2
  16. package/dist/cursor-pi-tool-bridge-run.js +7 -3
  17. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  18. package/dist/cursor-pi-tool-bridge.js +31 -9
  19. package/dist/cursor-provider-binding.js +58 -0
  20. package/dist/cursor-provider-errors.js +28 -7
  21. package/dist/cursor-provider-lazy.js +23 -11
  22. package/dist/cursor-provider-live-run-drain.js +15 -12
  23. package/dist/cursor-provider-run-finalizer.js +19 -8
  24. package/dist/cursor-provider-turn-coordinator.js +1 -0
  25. package/dist/cursor-provider-turn-display-router.js +4 -0
  26. package/dist/cursor-provider-turn-emit.js +1 -0
  27. package/dist/cursor-provider-turn-finalize.js +29 -20
  28. package/dist/cursor-provider-turn-prepare.js +24 -11
  29. package/dist/cursor-provider-turn-runner.js +37 -11
  30. package/dist/cursor-provider-turn-send.js +22 -9
  31. package/dist/cursor-provider-turn-summary.js +89 -0
  32. package/dist/cursor-provider.js +9 -3
  33. package/dist/cursor-request-provenance.js +42 -0
  34. package/dist/cursor-ripgrep-path.js +14 -3
  35. package/dist/cursor-runtime-state.js +50 -43
  36. package/dist/cursor-sdk-billed-usage.js +54 -75
  37. package/dist/cursor-sdk-event-debug-session.js +5 -6
  38. package/dist/cursor-sdk-event-debug.js +9 -6
  39. package/dist/cursor-sdk-process-error-guard.js +8 -7
  40. package/dist/cursor-sdk-runtime.js +13 -0
  41. package/dist/cursor-session-agent-cleanup.js +2 -2
  42. package/dist/cursor-session-agent-lifecycle.js +18 -9
  43. package/dist/cursor-session-agent-lineage.js +28 -12
  44. package/dist/cursor-session-agent-resume.js +51 -35
  45. package/dist/cursor-session-agent.js +9 -9
  46. package/dist/cursor-session-compaction-prep.js +2 -7
  47. package/dist/cursor-session-scope.js +71 -39
  48. package/dist/cursor-session-settings.js +61 -0
  49. package/dist/cursor-skill-tool.js +32 -49
  50. package/dist/cursor-state.js +72 -62
  51. package/dist/cursor-usage-accounting.js +13 -70
  52. package/dist/cursor-usage-command.js +89 -0
  53. package/dist/cursor-usage-ledger.js +512 -0
  54. package/dist/index.js +11 -19
  55. package/dist/model-discovery.js +11 -19
  56. package/docs/cursor-dogfood-checklist.md +3 -3
  57. package/docs/cursor-live-smoke-checklist.md +15 -15
  58. package/docs/cursor-model-ux-spec.md +59 -18
  59. package/docs/cursor-native-tool-replay.md +5 -3
  60. package/docs/cursor-native-tool-visual-audit.md +8 -8
  61. package/docs/cursor-testing-lessons.md +47 -23
  62. package/docs/cursor-tool-surfaces.md +1 -1
  63. package/docs/platform-smoke-implementation.md +10 -6
  64. package/docs/platform-smoke.md +35 -46
  65. package/package.json +6 -6
  66. package/scripts/cloud-runtime-smoke.mjs +4 -2
  67. package/scripts/local-resume-smoke.mjs +1 -1
  68. package/scripts/platform-smoke/card-detect.mjs +2 -8
  69. package/scripts/platform-smoke/targets.mjs +8 -15
  70. package/scripts/platform-smoke/visual-evidence.mjs +7 -2
  71. package/scripts/tmux-live-smoke.sh +57 -90
  72. package/src/context-window-cache.ts +0 -6
  73. package/src/context.ts +8 -0
  74. package/src/cursor-cloud-lifecycle.ts +124 -98
  75. package/src/cursor-durable-fs.ts +65 -3
  76. package/src/cursor-fallback-models.generated.ts +288 -1
  77. package/src/cursor-fallback-warning.ts +2 -2
  78. package/src/cursor-http1.ts +8 -8
  79. package/src/cursor-live-run-coordinator.ts +12 -13
  80. package/src/cursor-native-replay-routing.ts +2 -1
  81. package/src/cursor-native-tool-display-registration.ts +48 -24
  82. package/src/cursor-native-tool-display-state.ts +65 -21
  83. package/src/cursor-native-tool-display-tools.ts +7 -4
  84. package/src/cursor-pi-tool-bridge-constants.ts +27 -2
  85. package/src/cursor-pi-tool-bridge-run.ts +11 -3
  86. package/src/cursor-pi-tool-bridge-snapshot.ts +1 -1
  87. package/src/cursor-pi-tool-bridge.ts +26 -9
  88. package/src/cursor-provider-binding.ts +62 -0
  89. package/src/cursor-provider-errors.ts +27 -7
  90. package/src/cursor-provider-lazy.ts +21 -15
  91. package/src/cursor-provider-live-run-drain.ts +20 -13
  92. package/src/cursor-provider-run-finalizer.ts +24 -15
  93. package/src/cursor-provider-turn-coordinator.ts +2 -0
  94. package/src/cursor-provider-turn-display-router.ts +5 -0
  95. package/src/cursor-provider-turn-emit.ts +3 -1
  96. package/src/cursor-provider-turn-finalize.ts +38 -23
  97. package/src/cursor-provider-turn-prepare.ts +23 -14
  98. package/src/cursor-provider-turn-runner.ts +40 -13
  99. package/src/cursor-provider-turn-send.ts +23 -15
  100. package/src/cursor-provider-turn-summary.ts +94 -0
  101. package/src/cursor-provider-turn-types.ts +37 -4
  102. package/src/cursor-provider.ts +25 -3
  103. package/src/cursor-request-provenance.ts +64 -0
  104. package/src/cursor-ripgrep-path.ts +15 -5
  105. package/src/cursor-runtime-state.ts +53 -48
  106. package/src/cursor-sdk-billed-usage.ts +54 -102
  107. package/src/cursor-sdk-event-debug-session.ts +5 -5
  108. package/src/cursor-sdk-event-debug.ts +10 -6
  109. package/src/cursor-sdk-process-error-guard.ts +7 -7
  110. package/src/cursor-sdk-runtime.ts +14 -0
  111. package/src/cursor-session-agent-cleanup.ts +2 -2
  112. package/src/cursor-session-agent-lifecycle.ts +14 -9
  113. package/src/cursor-session-agent-lineage.ts +26 -12
  114. package/src/cursor-session-agent-resume.ts +49 -36
  115. package/src/cursor-session-agent.ts +13 -10
  116. package/src/cursor-session-compaction-prep.ts +2 -7
  117. package/src/cursor-session-scope.ts +87 -39
  118. package/src/cursor-session-settings.ts +72 -0
  119. package/src/cursor-skill-tool.ts +37 -61
  120. package/src/cursor-state.ts +74 -67
  121. package/src/cursor-usage-accounting.ts +17 -82
  122. package/src/cursor-usage-command.ts +76 -0
  123. package/src/cursor-usage-ledger.ts +500 -0
  124. package/src/index.ts +14 -23
  125. package/src/model-discovery.ts +14 -22
@@ -1,5 +1,7 @@
1
+ import type { ExtensionHandler, SessionShutdownEvent, SourceInfo } from "@earendil-works/pi-coding-agent";
1
2
  import type { CursorPiToolDisplay } from "./cursor-transcript-utils.js";
2
3
  import { parseOptionalEnvBoolean } from "./cursor-env-boolean.js";
4
+ import { normalizeCursorToolName } from "./cursor-tool-presentation-registry.js";
3
5
 
4
6
  export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {
5
7
  id: string;
@@ -9,11 +11,43 @@ export interface CursorNativeToolDisplayItem extends CursorPiToolDisplay {
9
11
  export const NATIVE_CURSOR_TOOL_DISPLAY_ENV = "PI_CURSOR_NATIVE_TOOL_DISPLAY";
10
12
  export const NATIVE_CURSOR_TOOL_REGISTRATION_ENV = "PI_CURSOR_REGISTER_NATIVE_TOOLS";
11
13
 
12
- export const registeredNativeToolNames = new Set<string>();
13
- export const skippedNativeToolNames = new Set<string>();
14
+ export interface CursorNativeToolDisplayState {
15
+ registeredNativeToolSources: Map<string, SourceInfo>;
16
+ skippedNativeToolNames: Set<string>;
17
+ runtimeRequested: boolean;
18
+ resultIds: Set<string>;
19
+ }
20
+
21
+ function createDisplayState(): CursorNativeToolDisplayState {
22
+ return { registeredNativeToolSources: new Map(), skippedNativeToolNames: new Set(), runtimeRequested: false, resultIds: new Set() };
23
+ }
24
+
25
+ let lastDisplayState = createDisplayState();
26
+ const displayStates = new Map<object, CursorNativeToolDisplayState>();
27
+ // IDs index results; the owning display state and tool authorize consumption.
14
28
  export const nativeToolResults = new Map<string, CursorNativeToolDisplayItem>();
29
+ const resultStates = new Map<string, CursorNativeToolDisplayState>();
15
30
 
16
- let nativeToolDisplayRuntimeRequested = false;
31
+ export function getCursorNativeToolDisplayState(pi?: object): CursorNativeToolDisplayState {
32
+ if (!pi) return lastDisplayState;
33
+ const state = displayStates.get(pi);
34
+ if (!state) throw new Error("Cursor native display binding is no longer active.");
35
+ return state;
36
+ }
37
+
38
+ export function registerCursorNativeToolDisplayState(pi: { on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void }): void {
39
+ const state = createDisplayState();
40
+ displayStates.set(pi, state);
41
+ lastDisplayState = state;
42
+ pi.on("session_shutdown", () => {
43
+ displayStates.delete(pi);
44
+ for (const id of state.resultIds) {
45
+ nativeToolResults.delete(id);
46
+ resultStates.delete(id);
47
+ }
48
+ state.resultIds.clear();
49
+ });
50
+ }
17
51
 
18
52
  export function readBooleanEnv(name: string, env: Record<string, string | undefined> = process.env): boolean | undefined {
19
53
  return parseOptionalEnvBoolean(env[name]);
@@ -30,39 +64,48 @@ export function isCursorNativeToolRegistrationRequested(mode?: string): boolean
30
64
  return mode !== "print" && readBooleanEnv(NATIVE_CURSOR_TOOL_REGISTRATION_ENV) !== false && isCursorNativeToolDisplayRequested(mode);
31
65
  }
32
66
 
33
- export function setCursorNativeToolDisplayRuntimeRequested(requested: boolean): void {
34
- nativeToolDisplayRuntimeRequested = requested;
67
+ export function setCursorNativeToolDisplayRuntimeRequested(requested: boolean, pi?: object): void {
68
+ getCursorNativeToolDisplayState(pi).runtimeRequested = requested;
35
69
  }
36
70
 
37
71
  export function isCursorNativeToolDisplayEnabled(): boolean {
38
- return registeredNativeToolNames.size > 0;
72
+ return getCursorNativeToolDisplayState().registeredNativeToolSources.size > 0;
39
73
  }
40
74
 
41
- export function isCursorNativeToolDisplayRuntimeEnabled(): boolean {
42
- return nativeToolDisplayRuntimeRequested && readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) !== false && registeredNativeToolNames.size > 0;
75
+ export function isCursorNativeToolDisplayRuntimeEnabled(state = getCursorNativeToolDisplayState()): boolean {
76
+ return state.runtimeRequested && readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) !== false && state.registeredNativeToolSources.size > 0;
43
77
  }
44
78
 
45
79
  export function canRenderCursorToolNatively(toolName: string): boolean {
46
- return registeredNativeToolNames.has(toolName);
80
+ return getCursorNativeToolDisplayState().registeredNativeToolSources.has(toolName);
47
81
  }
48
82
 
49
- export function isRegisteredCursorNativeToolName(toolName: string): boolean {
50
- return registeredNativeToolNames.has(toolName);
83
+ export function isRegisteredCursorNativeToolName(toolName: string, pi?: object): boolean {
84
+ return (pi ? displayStates.get(pi) : lastDisplayState)?.registeredNativeToolSources.has(toolName) === true;
51
85
  }
52
86
 
53
- export function recordCursorNativeToolDisplay(item: CursorNativeToolDisplayItem): boolean {
54
- if (!canRenderCursorToolNatively(item.toolName)) return false;
87
+ export function recordCursorNativeToolDisplay(item: CursorNativeToolDisplayItem, state = getCursorNativeToolDisplayState()): boolean {
88
+ if (!state.registeredNativeToolSources.has(item.toolName)) return false;
55
89
  nativeToolResults.set(item.id, item);
90
+ resultStates.set(item.id, state);
91
+ state.resultIds.add(item.id);
56
92
  return true;
57
93
  }
58
94
 
59
95
  export function deleteCursorNativeToolDisplay(id: string): void {
60
96
  nativeToolResults.delete(id);
97
+ resultStates.get(id)?.resultIds.delete(id);
98
+ resultStates.delete(id);
61
99
  }
62
100
 
63
- export function consumeCursorNativeToolDisplay(id: string): CursorNativeToolDisplayItem | undefined {
101
+ export function consumeCursorNativeToolDisplay(id: string, state: CursorNativeToolDisplayState, toolName: string): CursorNativeToolDisplayItem | undefined {
64
102
  const item = nativeToolResults.get(id);
65
- if (item) nativeToolResults.delete(id);
103
+ if (item) {
104
+ if (resultStates.get(id) !== state || normalizeCursorToolName(item.toolName) !== normalizeCursorToolName(toolName)) {
105
+ throw new Error(`No owned Cursor ${toolName} result was available. This replay-only call does not execute work.`);
106
+ }
107
+ deleteCursorNativeToolDisplay(id);
108
+ }
66
109
  return item;
67
110
  }
68
111
 
@@ -76,14 +119,15 @@ export function isCursorFileMutationToolName(toolName: string): toolName is "edi
76
119
 
77
120
  export const __testUtils = {
78
121
  nativeToolResultCount: () => nativeToolResults.size,
79
- registerNativeToolNameForTests(toolName: string): void {
80
- nativeToolDisplayRuntimeRequested = true;
81
- registeredNativeToolNames.add(toolName);
122
+ registerNativeToolNameForTests(toolName: string, pi?: object): void {
123
+ const state = getCursorNativeToolDisplayState(pi);
124
+ state.runtimeRequested = true;
125
+ state.registeredNativeToolSources.set(toolName, { path: "test", source: "inline", scope: "temporary", origin: "top-level" });
82
126
  },
83
127
  reset(): void {
84
- nativeToolDisplayRuntimeRequested = false;
85
- registeredNativeToolNames.clear();
86
- skippedNativeToolNames.clear();
128
+ lastDisplayState = createDisplayState();
129
+ displayStates.clear();
87
130
  nativeToolResults.clear();
131
+ resultStates.clear();
88
132
  },
89
133
  };
@@ -10,7 +10,6 @@ import {
10
10
  } from "@earendil-works/pi-coding-agent";
11
11
  import { Text } from "@earendil-works/pi-tui";
12
12
  import type { TSchema } from "typebox";
13
- import { getCursorSessionCwd } from "./cursor-session-scope.js";
14
13
  import {
15
14
  BUILTIN_NATIVE_CURSOR_TOOL_NAMES,
16
15
  CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES,
@@ -33,6 +32,7 @@ import {
33
32
  import {
34
33
  consumeCursorNativeToolDisplay,
35
34
  isCursorReplayToolCallId,
35
+ type CursorNativeToolDisplayState,
36
36
  } from "./cursor-native-tool-display-state.js";
37
37
 
38
38
 
@@ -155,12 +155,13 @@ function getNativeReplayStrategy(toolName: string): NativeReplayStrategy | undef
155
155
  export function wrapNativeCursorTool<TParams extends TSchema, TDetails, TState>(
156
156
  definition: ToolDefinition<TParams, TDetails, TState>,
157
157
  getCurrentDefinition: () => ToolDefinition<TParams, TDetails, TState>,
158
+ state: CursorNativeToolDisplayState,
158
159
  ): ToolDefinition<TParams, TDetails, TState> {
159
160
  const strategy = getNativeReplayStrategy(definition.name);
160
161
  return {
161
162
  ...definition,
162
163
  async execute(toolCallId, params, signal, onUpdate, ctx) {
163
- const cursorDisplay = consumeCursorNativeToolDisplay(toolCallId);
164
+ const cursorDisplay = consumeCursorNativeToolDisplay(toolCallId, state, definition.name);
164
165
  if (cursorDisplay) {
165
166
  if (cursorDisplay.isError) {
166
167
  const text = cursorDisplay.result.content
@@ -211,9 +212,11 @@ export function createNativeCursorToolDefinition(toolName: NativeCursorToolName,
211
212
  export function registerNativeCursorTool(
212
213
  pi: Pick<import("@earendil-works/pi-coding-agent").ExtensionAPI, "registerTool">,
213
214
  toolName: NativeCursorToolName,
215
+ getCwd: () => string,
216
+ state: CursorNativeToolDisplayState,
214
217
  ): void {
215
- const definition = createNativeCursorToolDefinition(toolName, getCursorSessionCwd());
216
- pi.registerTool(wrapNativeCursorTool(definition, () => createNativeCursorToolDefinition(toolName, getCursorSessionCwd())));
218
+ const definition = createNativeCursorToolDefinition(toolName, getCwd());
219
+ pi.registerTool(wrapNativeCursorTool(definition, () => createNativeCursorToolDefinition(toolName, getCwd()), state));
217
220
  }
218
221
 
219
222
  export { CURSOR_MODEL_ACTIVE_REPLAY_TOOL_NAMES, CURSOR_REPLAY_TOOL_NAMES };
@@ -1,8 +1,33 @@
1
1
  export const MCP_SERVER_NAME = "pi_tools";
2
2
  export const MCP_ENDPOINT_ROOT = "/cursor-pi-tool-bridge";
3
+ export const CURSOR_PI_BRIDGE_TOOL_CALL_ID_MAX_LENGTH = 64;
3
4
 
4
- const CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN = /^cursor-pi-bridge-run-[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}-tool-\d+$/i;
5
+ const CURSOR_PI_BRIDGE_RUN_UUID_PATTERN = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
6
+ const CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN = /^cursor-pi-bridge-[0-9a-f]{32}-t\d+$/i;
7
+ const LEGACY_CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN =
8
+ /^cursor-pi-bridge-run-[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}-tool-\d+$/i;
9
+
10
+ /**
11
+ * Build the provider-facing bridge ID while enforcing the 64-character
12
+ * call_id limit used by OpenAI-compatible Responses backends.
13
+ */
14
+ export function buildCursorPiBridgeToolCallId(runUuid: string, counter: number): string {
15
+ if (!CURSOR_PI_BRIDGE_RUN_UUID_PATTERN.test(runUuid)) {
16
+ throw new Error("Cursor pi bridge run UUID must be canonical");
17
+ }
18
+ if (!Number.isSafeInteger(counter) || counter <= 0) {
19
+ throw new Error("Cursor pi bridge tool call counter must be a positive safe integer");
20
+ }
21
+ const toolCallId = `cursor-pi-bridge-${runUuid.replaceAll("-", "")}-t${counter}`;
22
+ if (toolCallId.length > CURSOR_PI_BRIDGE_TOOL_CALL_ID_MAX_LENGTH) {
23
+ throw new Error("Cursor pi bridge tool call ID limit exceeded");
24
+ }
25
+ return toolCallId;
26
+ }
5
27
 
6
28
  export function isCursorPiBridgeToolCallId(toolCallId: string): boolean {
7
- return CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN.test(toolCallId);
29
+ return (
30
+ (toolCallId.length <= CURSOR_PI_BRIDGE_TOOL_CALL_ID_MAX_LENGTH && CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN.test(toolCallId)) ||
31
+ LEGACY_CURSOR_PI_BRIDGE_TOOL_CALL_ID_PATTERN.test(toolCallId)
32
+ );
8
33
  }
@@ -7,7 +7,11 @@ import {
7
7
  WebStandardStreamableHTTPServerTransport,
8
8
  } from "@modelcontextprotocol/server";
9
9
  import { bridgeToolExecutionAbortTracker } from "./cursor-pi-tool-bridge-abort.js";
10
- import { MCP_ENDPOINT_ROOT, MCP_SERVER_NAME } from "./cursor-pi-tool-bridge-constants.js";
10
+ import {
11
+ buildCursorPiBridgeToolCallId,
12
+ MCP_ENDPOINT_ROOT,
13
+ MCP_SERVER_NAME,
14
+ } from "./cursor-pi-tool-bridge-constants.js";
11
15
  import {
12
16
  type CursorPiToolBridgeDiagnosticEvent,
13
17
  type CursorPiToolBridgeLifecycleDiagnosticFields,
@@ -58,6 +62,7 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
58
62
  private readonly registry: CursorPiToolBridgeRunHost;
59
63
  private readonly env: Record<string, string | undefined>;
60
64
  private readonly endpointPath: string;
65
+ private readonly runUuid: string;
61
66
  private readonly callTimeoutMs: number;
62
67
  private readonly knownMcpToolNames: ReadonlySet<string>;
63
68
  private readonly knownCursorMcpCallIds = new Set<string>();
@@ -86,7 +91,9 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
86
91
  this.enabled = enabled;
87
92
  this.onToolRequest = options.onToolRequest;
88
93
  this.debugRecorder = options.debugRecorder;
89
- this.id = `cursor-pi-bridge-run-${randomUUID()}`;
94
+ const runUuid = randomUUID();
95
+ this.runUuid = runUuid;
96
+ this.id = `cursor-pi-bridge-run-${runUuid}`;
90
97
  this.endpointPath = `${MCP_ENDPOINT_ROOT}/${randomUUID()}/mcp`;
91
98
  this.callTimeoutMs = resolveCursorPiToolBridgeCallTimeoutMs(env);
92
99
  this.knownMcpToolNames = new Set(snapshot.tools.map((tool) => tool.mcpToolName));
@@ -277,11 +284,12 @@ export class CursorPiToolBridgeRunImpl implements CursorPiToolBridgeRun {
277
284
 
278
285
  this.toolCallCounter += 1;
279
286
  const bridgeCallId = `${this.id}-bridge-${this.toolCallCounter}`;
287
+ const piToolCallId = buildCursorPiBridgeToolCallId(this.runUuid, this.toolCallCounter);
280
288
  const request: CursorPiBridgeToolRequest = {
281
289
  runId: this.id,
282
290
  bridgeCallId,
283
291
  cursorMcpCallId,
284
- piToolCallId: `${this.id}-tool-${this.toolCallCounter}`,
292
+ piToolCallId,
285
293
  piToolName,
286
294
  mcpToolName,
287
295
  args: normalizeMcpArgs(argsValue),
@@ -63,7 +63,7 @@ export function buildCursorPiToolBridgeSnapshot(
63
63
 
64
64
  for (const tool of allTools) {
65
65
  if (!activeToolNames.has(tool.name)) continue;
66
- if (isExcludedFromCursorBridgeExposure(tool.name) && isRegisteredCursorNativeToolName(tool.name)) continue;
66
+ if (isExcludedFromCursorBridgeExposure(tool.name) && isRegisteredCursorNativeToolName(tool.name, pi)) continue;
67
67
  if (!exposeOverlappingBuiltins && isOverlappingCursorNativePiToolName(tool.name)) continue;
68
68
 
69
69
  const mcpToolName = createMcpToolName(tool.name, usedMcpToolNames);
@@ -46,6 +46,8 @@ export {
46
46
  buildCursorPiToolBridgeSurfaceSignature,
47
47
  } from "./cursor-pi-tool-bridge-snapshot.js";
48
48
 
49
+ const bridges = new Map<object, CursorPiToolBridgeRegistry>();
50
+ const activeCallsByPi = new Map<object, Set<string>>();
49
51
  let registeredCursorPiToolBridge: CursorPiToolBridgeRegistry | undefined;
50
52
 
51
53
  const WINDOWS_BRIDGE_ABORT_ENV = "PI_CURSOR_BRIDGE_TOOL_CALL_ID";
@@ -89,17 +91,25 @@ Get-CimInstance Win32_Process -Filter "Name = 'bash.exe' OR Name = 'sh.exe'" |
89
91
  }
90
92
 
91
93
  export function registerCursorPiToolBridge(pi: CursorPiToolBridgeExtensionApi): CursorPiToolBridge {
92
- bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge extension reloaded");
93
- void registeredCursorPiToolBridge?.disposeAll("Cursor pi tool bridge extension reloaded");
94
+ const previous = bridges.get(pi);
95
+ if (previous) {
96
+ const reason = "Cursor pi tool bridge extension reloaded";
97
+ for (const id of activeCallsByPi.get(pi) ?? []) bridgeToolExecutionAbortTracker.abort(id, reason);
98
+ void previous.disposeAll(reason);
99
+ }
94
100
  const bridge = new CursorPiToolBridgeRegistry(pi);
95
101
  registeredCursorPiToolBridge = bridge;
102
+ bridges.set(pi, bridge);
103
+ const activeCalls = new Set<string>();
104
+ activeCallsByPi.set(pi, activeCalls);
96
105
  pi.on("tool_call", (event, ctx) => {
97
- if (registeredCursorPiToolBridge !== bridge) return undefined;
106
+ if (bridges.get(pi) !== bridge) return undefined;
98
107
  if (!bridge.hasPendingPiToolCallId(event.toolCallId)) {
99
108
  return isCursorPiBridgeToolCallId(event.toolCallId)
100
109
  ? { block: true, reason: "Cursor pi bridge tool call is no longer pending" }
101
110
  : undefined;
102
111
  }
112
+ activeCalls.add(event.toolCallId);
103
113
  const windowsAbortMarker = installWindowsBridgeBashAbortMarker(event);
104
114
  const trackingStarted = bridgeToolExecutionAbortTracker.track(event.toolCallId, {
105
115
  signal: ctx.signal,
@@ -115,18 +125,23 @@ export function registerCursorPiToolBridge(pi: CursorPiToolBridgeExtensionApi):
115
125
  return { block: true, reason: "Cursor pi bridge tool execution was aborted before it started" };
116
126
  });
117
127
  pi.on("tool_result", (event) => {
118
- bridgeToolExecutionAbortTracker.finish(event.toolCallId);
128
+ if (activeCalls.delete(event.toolCallId)) bridgeToolExecutionAbortTracker.finish(event.toolCallId);
119
129
  });
120
130
  pi.on("session_shutdown", async (event) => {
131
+ if (bridges.get(pi) !== bridge) return;
121
132
  const reason = `Cursor pi tool bridge session shutdown: ${event.reason}`;
122
- bridgeToolExecutionAbortTracker.abortAll(reason);
133
+ for (const id of activeCalls) bridgeToolExecutionAbortTracker.abort(id, reason);
134
+ activeCalls.clear();
135
+ bridges.delete(pi);
136
+ activeCallsByPi.delete(pi);
137
+ if (registeredCursorPiToolBridge === bridge) registeredCursorPiToolBridge = [...bridges.values()].at(-1);
123
138
  await bridge.disposeAll(reason);
124
139
  });
125
140
  return bridge;
126
141
  }
127
142
 
128
- export function getRegisteredCursorPiToolBridge(): CursorPiToolBridge | undefined {
129
- return registeredCursorPiToolBridge;
143
+ export function getRegisteredCursorPiToolBridge(pi?: object): CursorPiToolBridge | undefined {
144
+ return pi ? bridges.get(pi) : registeredCursorPiToolBridge;
130
145
  }
131
146
 
132
147
  export const __testUtils = {
@@ -159,8 +174,10 @@ export const __testUtils = {
159
174
  },
160
175
  resetRegisteredBridgeForTests() {
161
176
  bridgeToolExecutionAbortTracker.abortAll("Cursor pi tool bridge test reset");
162
- const bridge = registeredCursorPiToolBridge;
177
+ const registered = [...bridges.values()];
178
+ bridges.clear();
179
+ activeCallsByPi.clear();
163
180
  registeredCursorPiToolBridge = undefined;
164
- return bridge?.disposeAll("Cursor pi tool bridge test reset") ?? Promise.resolve();
181
+ return Promise.all(registered.map((bridge) => bridge.disposeAll("Cursor pi tool bridge test reset")));
165
182
  },
166
183
  };
@@ -0,0 +1,62 @@
1
+ import type { ExtensionAPI, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
+ import { CURSOR_API_KEY_CONFIG_VALUE } from "./cursor-api-key.js";
3
+ import { captureCursorCloudLifecycleRecorder } from "./cursor-cloud-lifecycle.js";
4
+ import { captureCursorUsageRecorder, type CursorUsageRecorder } from "./cursor-usage-ledger.js";
5
+ import { getCursorNativeToolDisplayState } from "./cursor-native-tool-display-state.js";
6
+ import { getRegisteredCursorPiToolBridge } from "./cursor-pi-tool-bridge.js";
7
+ import { createCursorLazyStream } from "./cursor-provider-lazy.js";
8
+ import { getCursorSessionScopeSnapshot } from "./cursor-session-scope.js";
9
+ import { captureCursorRequestProjection, resolveCursorRequestProvenance, type CursorRequestProjectionSnapshot, type CursorRequestProvenance } from "./cursor-request-provenance.js";
10
+
11
+ export const CURSOR_PROVIDER_OWNERSHIP_ERROR = "Cursor provider ownership is unavailable: this request does not belong to the active Cursor registration. Bind this session's extensions and refresh Cursor models/reload it; use independent ModelRuntime instances for sibling sessions.";
12
+ interface ProviderBinding { active: boolean; closed: boolean }
13
+ interface RequestReceipt {
14
+ binding: ProviderBinding;
15
+ scope: ReturnType<typeof getCursorSessionScopeSnapshot>;
16
+ projection: CursorRequestProjectionSnapshot;
17
+ usageRecorder: CursorUsageRecorder;
18
+ }
19
+ const requestOwners = new WeakMap<object, RequestReceipt>();
20
+
21
+ /** One closure per ExtensionAPI, including direct compaction/tree/bug-report streams. */
22
+ export function registerCursorProviderBinding(pi: Pick<ExtensionAPI, "on" | "registerProvider" | "appendEntry">) {
23
+ const binding: ProviderBinding = { active: false, closed: false };
24
+ const operations = new WeakMap<AbortSignal, CursorRequestProvenance["purpose"]>();
25
+ pi.on("session_before_compact", (event) => { operations.set(event.signal, "compaction"); });
26
+ pi.on("session_before_tree", (event) => { operations.set(event.signal, "tree"); });
27
+ pi.on("before_provider_headers", (event, ctx) => {
28
+ // Pi's SDK supplies this session-owned callback for ordinary and auxiliary
29
+ // streams. ModelRuntime forwards the same real headers object to the provider.
30
+ requestOwners.set(event.headers, {
31
+ binding,
32
+ scope: getCursorSessionScopeSnapshot(pi),
33
+ projection: captureCursorRequestProjection(ctx.sessionManager),
34
+ usageRecorder: captureCursorUsageRecorder(pi, ctx),
35
+ });
36
+ });
37
+ const stream = createCursorLazyStream((model, context, options) => {
38
+ const headers = options?.headers;
39
+ const owner = headers && requestOwners.get(headers);
40
+ if (headers) requestOwners.delete(headers);
41
+ if (!binding.active || binding.closed) throw new Error("Cursor provider binding is not active. Bind this session's extensions, or reload the session after shutdown.");
42
+ if (!owner) throw new Error("Cursor provider ownership is unavailable: this call has no native session request receipt. Bare modelRegistry.streamSimple calls are unsupported for Cursor; use the owning AgentSession's stream path or an independent child AgentSession.");
43
+ if (owner.binding !== binding) throw new Error(CURSOR_PROVIDER_OWNERSHIP_ERROR);
44
+ return {
45
+ request: resolveCursorRequestProvenance(owner.projection, model, context, options?.signal && operations.get(options.signal) || "normal"),
46
+ usageRecorder: owner.usageRecorder,
47
+ scope: owner.scope,
48
+ bridge: getRegisteredCursorPiToolBridge(pi),
49
+ nativeDisplay: getCursorNativeToolDisplayState(pi),
50
+ recordCloudLifecycle: captureCursorCloudLifecycleRecorder(pi),
51
+ };
52
+ });
53
+ pi.on("session_start", () => { if (!binding.closed) binding.active = true; });
54
+ pi.on("session_shutdown", () => { binding.active = false; binding.closed = true; });
55
+ return (models: ProviderModelConfig[]) => {
56
+ if (binding.closed) return;
57
+ pi.registerProvider("cursor", {
58
+ name: "Cursor", baseUrl: "https://cursor.com", apiKey: CURSOR_API_KEY_CONFIG_VALUE,
59
+ api: "cursor-sdk", models, streamSimple: stream,
60
+ });
61
+ };
62
+ }
@@ -6,14 +6,14 @@ import { scrubSensitiveText } from "./cursor-sensitive-text.js";
6
6
  export const MISSING_CURSOR_API_KEY_MESSAGE =
7
7
  "Cursor SDK runs require a Cursor SDK API key. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, set CURSOR_API_KEY before starting pi, or restart pi with --api-key.";
8
8
  const GENERIC_CURSOR_SDK_ERROR_MESSAGE =
9
- "Cursor SDK request failed. The Cursor SDK API key may be missing, invalid, or unauthorized. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
9
+ "Cursor SDK request failed without further error details.";
10
10
  const AUTH_CURSOR_SDK_ERROR_MESSAGE =
11
11
  "Cursor SDK request failed because the Cursor SDK API key may be invalid or unauthorized. Cursor Agent CLI/Desktop login is not reused. Run /login -> Use an API key -> Cursor, verify CURSOR_API_KEY, or pass --api-key, then retry.";
12
12
  const CLOUD_AUTH_CURSOR_SDK_ERROR_MESSAGE =
13
13
  "Cursor Cloud Agents request failed because Cloud API authentication rejected the API key. Use a user API key from Cursor Dashboard -> API Keys or a service account API key from Team settings; Team Admin API keys are not supported as Cursor Cloud Agents credentials. Configure the key with /login -> Use an API key -> Cursor, CURSOR_API_KEY, or --api-key, then retry.";
14
14
  // Keep "Network error" aligned with pi's agent-level retry classifier.
15
15
  const NETWORK_CURSOR_SDK_ERROR_MESSAGE =
16
- "Network error: Cursor SDK request failed during network or service I/O. Check your connection; pi will retry automatically when auto-retry is enabled.";
16
+ "Network error: Cursor SDK request failed during network or service I/O. Check your connection.";
17
17
 
18
18
  // Keep this phrase aligned with pi's agent-level retry classifier (`provider.?returned.?error`).
19
19
  const RETRYABLE_CURSOR_RUN_FAILURE_PREFIX = "Provider returned error: Cursor SDK run failed";
@@ -349,27 +349,47 @@ export function resolveCursorSdkAbortCause(options: {
349
349
  return "unknown";
350
350
  }
351
351
 
352
+ function formatErrorDetail(error: unknown, apiKey: string | undefined, depth = 0): string {
353
+ if (depth >= 3) return "";
354
+ const record = asRecord(error);
355
+ const message = typeof error === "string" ? error : getErrorStringField(record, "message") ?? "";
356
+ const name = getErrorStringField(record, "name");
357
+ const code = getErrorEvidenceField(error, record, "code");
358
+ const scrub = (text: string, limit: number) => scrubSensitiveText(text, apiKey).trim().slice(0, limit);
359
+ const scrubbed = [
360
+ name && name !== "Error" ? `${scrub(name, 120)}:` : "",
361
+ code ? `(code: ${scrub(code, 120)})` : "",
362
+ scrub(message, 1600),
363
+ ].filter(Boolean).join(" ");
364
+ const cause = record?.cause === undefined ? "" : formatErrorDetail(record.cause, apiKey, depth + 1);
365
+ return cause ? `${scrubbed}\nCaused by: ${cause}` : scrubbed;
366
+ }
367
+
352
368
  export function sanitizeCursorProviderError(
353
369
  error: unknown,
354
370
  apiKey?: string,
355
371
  runtimeTarget?: CursorRuntime,
356
372
  ): string {
357
373
  const record = asRecord(error);
358
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
374
+ const message = typeof error === "string" ? error : getErrorStringField(record, "message") ?? "";
359
375
  if (message === MISSING_CURSOR_API_KEY_MESSAGE) return MISSING_CURSOR_API_KEY_MESSAGE;
360
- const scrubbed = scrubSensitiveText(message, apiKey).trim();
376
+ const scrubbed = formatErrorDetail(error, apiKey);
377
+ // A loader failure is not an auth rejection, even inside an authentication wrapper.
378
+ if (/\b(?:ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ResolveMessage)\b|Cannot find (?:module|package)|does not provide an export named/i.test(scrubbed)) return scrubbed;
361
379
  const integrationMessage = runtimeTarget === "cloud"
362
- ? formatIntegrationNotConnectedError(error, record, scrubbed, apiKey)
380
+ ? formatIntegrationNotConnectedError(error, record, scrubSensitiveText(message, apiKey).trim(), apiKey)
363
381
  : undefined;
364
382
  if (integrationMessage) return integrationMessage;
365
383
  const connectClassification = classifyCursorConnectError(error);
366
384
  if (
367
385
  (runtimeTarget === "cloud" && getErrorName(error, record) === "AuthenticationError") ||
368
- connectClassification?.kind === "unauthenticated" ||
369
- isLikelyAuthError(scrubbed)
386
+ /\b(?:invalid|revoked|expired|missing) (?:user )?api key\b|\bapi key (?:is )?(?:invalid|revoked|expired|missing)\b/i.test(scrubbed)
370
387
  ) {
371
388
  return runtimeTarget === "cloud" ? CLOUD_AUTH_CURSOR_SDK_ERROR_MESSAGE : AUTH_CURSOR_SDK_ERROR_MESSAGE;
372
389
  }
390
+ if (connectClassification?.kind === "unauthenticated" || isLikelyAuthError(scrubbed)) {
391
+ return `Cursor SDK authentication failed. ${scrubbed}`;
392
+ }
373
393
  if (connectClassification?.kind === "network" || isCursorSdkConnectionStalledError(error) || isLikelyNetworkTimeout(scrubbed)) return NETWORK_CURSOR_SDK_ERROR_MESSAGE;
374
394
  if (isGenericCursorRunFailureMessage(scrubbed)) return RETRYABLE_CURSOR_RUN_FAILURE_PREFIX;
375
395
  if (isGenericErrorMessage(scrubbed)) return GENERIC_CURSOR_SDK_ERROR_MESSAGE;
@@ -7,7 +7,7 @@ import {
7
7
  type Model,
8
8
  type SimpleStreamOptions,
9
9
  } from "@earendil-works/pi-ai";
10
- import { streamCursor } from "./cursor-provider.js";
10
+ import { streamCursor, type CursorProviderOwnership } from "./cursor-provider.js";
11
11
  import { sanitizeCursorProviderError } from "./cursor-provider-errors.js";
12
12
 
13
13
  function makeProviderRuntimeErrorMessage(model: Model<Api>, error: unknown, apiKey?: string): AssistantMessage {
@@ -31,22 +31,28 @@ function makeProviderRuntimeErrorMessage(model: Model<Api>, error: unknown, apiK
31
31
  };
32
32
  }
33
33
 
34
- export function streamCursorLazy(
35
- model: Model<Api>,
36
- context: Context,
37
- options?: SimpleStreamOptions,
38
- ): AssistantMessageEventStream {
39
- const outer = createAssistantMessageEventStream();
40
- queueMicrotask(async () => {
41
- try {
42
- for await (const event of streamCursor(model, context, options)) {
43
- outer.push(event);
44
- }
45
- } catch (error) {
34
+ export function createCursorLazyStream(capture: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => CursorProviderOwnership) {
35
+ return (model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream => {
36
+ const outer = createAssistantMessageEventStream();
37
+ const fail = (error: unknown) => {
46
38
  const message = makeProviderRuntimeErrorMessage(model, error, options?.apiKey);
47
39
  outer.push({ type: "error", reason: "error", error: message });
48
40
  outer.end(message);
41
+ };
42
+ let ownership: CursorProviderOwnership;
43
+ try {
44
+ ownership = capture(model, context, options);
45
+ } catch (error) {
46
+ fail(error);
47
+ return outer;
49
48
  }
50
- });
51
- return outer;
49
+ queueMicrotask(async () => {
50
+ try {
51
+ for await (const event of streamCursor(model, context, options, ownership)) outer.push(event);
52
+ } catch (error) {
53
+ fail(error);
54
+ }
55
+ });
56
+ return outer;
57
+ };
52
58
  }