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.
- package/CHANGELOG.md +30 -0
- package/README.md +65 -13
- package/dist/context-window-cache.js +0 -6
- package/dist/context.js +8 -0
- package/dist/cursor-cloud-lifecycle.js +122 -98
- package/dist/cursor-durable-fs.js +97 -3
- package/dist/cursor-fallback-models.generated.js +288 -1
- package/dist/cursor-fallback-warning.js +2 -2
- package/dist/cursor-http1.js +8 -8
- package/dist/cursor-live-run-coordinator.js +9 -13
- package/dist/cursor-native-replay-routing.js +1 -1
- package/dist/cursor-native-tool-display-registration.js +46 -22
- package/dist/cursor-native-tool-display-state.js +57 -22
- package/dist/cursor-native-tool-display-tools.js +5 -6
- package/dist/cursor-pi-tool-bridge-constants.js +23 -2
- package/dist/cursor-pi-tool-bridge-run.js +7 -3
- package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
- package/dist/cursor-pi-tool-bridge.js +31 -9
- package/dist/cursor-provider-binding.js +58 -0
- package/dist/cursor-provider-errors.js +28 -7
- package/dist/cursor-provider-lazy.js +23 -11
- package/dist/cursor-provider-live-run-drain.js +15 -12
- package/dist/cursor-provider-run-finalizer.js +19 -8
- package/dist/cursor-provider-turn-coordinator.js +1 -0
- package/dist/cursor-provider-turn-display-router.js +4 -0
- package/dist/cursor-provider-turn-emit.js +1 -0
- package/dist/cursor-provider-turn-finalize.js +29 -20
- package/dist/cursor-provider-turn-prepare.js +24 -11
- package/dist/cursor-provider-turn-runner.js +37 -11
- package/dist/cursor-provider-turn-send.js +22 -9
- package/dist/cursor-provider-turn-summary.js +89 -0
- package/dist/cursor-provider.js +9 -3
- package/dist/cursor-request-provenance.js +42 -0
- package/dist/cursor-ripgrep-path.js +14 -3
- package/dist/cursor-runtime-state.js +50 -43
- package/dist/cursor-sdk-billed-usage.js +54 -75
- package/dist/cursor-sdk-event-debug-session.js +5 -6
- package/dist/cursor-sdk-event-debug.js +9 -6
- package/dist/cursor-sdk-process-error-guard.js +8 -7
- package/dist/cursor-sdk-runtime.js +13 -0
- package/dist/cursor-session-agent-cleanup.js +2 -2
- package/dist/cursor-session-agent-lifecycle.js +18 -9
- package/dist/cursor-session-agent-lineage.js +28 -12
- package/dist/cursor-session-agent-resume.js +51 -35
- package/dist/cursor-session-agent.js +9 -9
- package/dist/cursor-session-compaction-prep.js +2 -7
- package/dist/cursor-session-scope.js +71 -39
- package/dist/cursor-session-settings.js +61 -0
- package/dist/cursor-skill-tool.js +32 -49
- package/dist/cursor-state.js +72 -62
- package/dist/cursor-usage-accounting.js +13 -70
- package/dist/cursor-usage-command.js +89 -0
- package/dist/cursor-usage-ledger.js +512 -0
- package/dist/index.js +11 -19
- package/dist/model-discovery.js +11 -19
- package/docs/cursor-dogfood-checklist.md +3 -3
- package/docs/cursor-live-smoke-checklist.md +15 -15
- package/docs/cursor-model-ux-spec.md +59 -18
- package/docs/cursor-native-tool-replay.md +5 -3
- package/docs/cursor-native-tool-visual-audit.md +8 -8
- package/docs/cursor-testing-lessons.md +47 -23
- package/docs/cursor-tool-surfaces.md +1 -1
- package/docs/platform-smoke-implementation.md +10 -6
- package/docs/platform-smoke.md +35 -46
- package/package.json +6 -6
- package/scripts/cloud-runtime-smoke.mjs +4 -2
- package/scripts/local-resume-smoke.mjs +1 -1
- package/scripts/platform-smoke/card-detect.mjs +2 -8
- package/scripts/platform-smoke/targets.mjs +8 -15
- package/scripts/platform-smoke/visual-evidence.mjs +7 -2
- package/scripts/tmux-live-smoke.sh +57 -90
- package/src/context-window-cache.ts +0 -6
- package/src/context.ts +8 -0
- package/src/cursor-cloud-lifecycle.ts +124 -98
- package/src/cursor-durable-fs.ts +65 -3
- package/src/cursor-fallback-models.generated.ts +288 -1
- package/src/cursor-fallback-warning.ts +2 -2
- package/src/cursor-http1.ts +8 -8
- package/src/cursor-live-run-coordinator.ts +12 -13
- package/src/cursor-native-replay-routing.ts +2 -1
- package/src/cursor-native-tool-display-registration.ts +48 -24
- package/src/cursor-native-tool-display-state.ts +65 -21
- package/src/cursor-native-tool-display-tools.ts +7 -4
- package/src/cursor-pi-tool-bridge-constants.ts +27 -2
- package/src/cursor-pi-tool-bridge-run.ts +11 -3
- package/src/cursor-pi-tool-bridge-snapshot.ts +1 -1
- package/src/cursor-pi-tool-bridge.ts +26 -9
- package/src/cursor-provider-binding.ts +62 -0
- package/src/cursor-provider-errors.ts +27 -7
- package/src/cursor-provider-lazy.ts +21 -15
- package/src/cursor-provider-live-run-drain.ts +20 -13
- package/src/cursor-provider-run-finalizer.ts +24 -15
- package/src/cursor-provider-turn-coordinator.ts +2 -0
- package/src/cursor-provider-turn-display-router.ts +5 -0
- package/src/cursor-provider-turn-emit.ts +3 -1
- package/src/cursor-provider-turn-finalize.ts +38 -23
- package/src/cursor-provider-turn-prepare.ts +23 -14
- package/src/cursor-provider-turn-runner.ts +40 -13
- package/src/cursor-provider-turn-send.ts +23 -15
- package/src/cursor-provider-turn-summary.ts +94 -0
- package/src/cursor-provider-turn-types.ts +37 -4
- package/src/cursor-provider.ts +25 -3
- package/src/cursor-request-provenance.ts +64 -0
- package/src/cursor-ripgrep-path.ts +15 -5
- package/src/cursor-runtime-state.ts +53 -48
- package/src/cursor-sdk-billed-usage.ts +54 -102
- package/src/cursor-sdk-event-debug-session.ts +5 -5
- package/src/cursor-sdk-event-debug.ts +10 -6
- package/src/cursor-sdk-process-error-guard.ts +7 -7
- package/src/cursor-sdk-runtime.ts +14 -0
- package/src/cursor-session-agent-cleanup.ts +2 -2
- package/src/cursor-session-agent-lifecycle.ts +14 -9
- package/src/cursor-session-agent-lineage.ts +26 -12
- package/src/cursor-session-agent-resume.ts +49 -36
- package/src/cursor-session-agent.ts +13 -10
- package/src/cursor-session-compaction-prep.ts +2 -7
- package/src/cursor-session-scope.ts +87 -39
- package/src/cursor-session-settings.ts +72 -0
- package/src/cursor-skill-tool.ts +37 -61
- package/src/cursor-state.ts +74 -67
- package/src/cursor-usage-accounting.ts +17 -82
- package/src/cursor-usage-command.ts +76 -0
- package/src/cursor-usage-ledger.ts +500 -0
- package/src/index.ts +14 -23
- 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
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
72
|
+
return getCursorNativeToolDisplayState().registeredNativeToolSources.size > 0;
|
|
39
73
|
}
|
|
40
74
|
|
|
41
|
-
export function isCursorNativeToolDisplayRuntimeEnabled(): boolean {
|
|
42
|
-
return
|
|
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
|
|
80
|
+
return getCursorNativeToolDisplayState().registeredNativeToolSources.has(toolName);
|
|
47
81
|
}
|
|
48
82
|
|
|
49
|
-
export function isRegisteredCursorNativeToolName(toolName: string): boolean {
|
|
50
|
-
return
|
|
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 (!
|
|
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)
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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,
|
|
216
|
-
pi.registerTool(wrapNativeCursorTool(definition, () => createNativeCursorToolDefinition(toolName,
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
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 (
|
|
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.
|
|
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
|
|
177
|
+
const registered = [...bridges.values()];
|
|
178
|
+
bridges.clear();
|
|
179
|
+
activeCallsByPi.clear();
|
|
163
180
|
registeredCursorPiToolBridge = undefined;
|
|
164
|
-
return bridge
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
|
35
|
-
model: Model<Api>,
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
}
|