pi-cursor-sdk 0.1.19 → 0.1.21
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 +52 -0
- package/README.md +72 -11
- package/docs/cursor-dogfood-checklist.md +57 -0
- package/docs/cursor-live-smoke-checklist.md +116 -10
- package/docs/cursor-model-ux-spec.md +60 -19
- package/docs/cursor-native-tool-replay.md +21 -11
- package/docs/cursor-native-tool-visual-audit.md +104 -59
- package/docs/cursor-testing-lessons.md +10 -5
- package/docs/cursor-tool-surfaces.md +69 -0
- package/package.json +37 -11
- package/scripts/debug-provider-events.d.mts +59 -0
- package/scripts/debug-provider-events.mjs +70 -175
- package/scripts/debug-sdk-events.d.mts +90 -0
- package/scripts/debug-sdk-events.mjs +36 -98
- package/scripts/fixtures/plan-strip-shim/index.ts +12 -0
- package/scripts/isolated-cursor-smoke.sh +264 -102
- package/scripts/lib/cursor-child-process.d.mts +10 -0
- package/scripts/lib/cursor-child-process.mjs +50 -0
- package/scripts/lib/cursor-cli-args.d.mts +63 -0
- package/scripts/lib/cursor-cli-args.mjs +129 -0
- package/scripts/lib/cursor-script-fail.d.mts +1 -0
- package/scripts/lib/cursor-script-fail.mjs +13 -0
- package/scripts/lib/cursor-sdk-output-filter.d.mts +5 -0
- package/scripts/lib/cursor-smoke-env.d.mts +38 -0
- package/scripts/lib/cursor-smoke-env.mjs +81 -0
- package/scripts/lib/cursor-smoke-shell.sh +174 -0
- package/scripts/lib/cursor-visual-render.d.mts +15 -0
- package/scripts/lib/cursor-visual-render.mjs +131 -0
- package/scripts/probe-mcp-coldstart.mjs +226 -0
- package/scripts/refresh-cursor-model-snapshots.mjs +29 -65
- package/scripts/steering-rpc-smoke.mjs +170 -65
- package/scripts/tmux-live-smoke.sh +152 -98
- package/scripts/visual-tui-smoke.mjs +659 -0
- package/shared/cursor-sdk-event-debug-env.d.mts +12 -0
- package/shared/cursor-sdk-event-debug-env.mjs +13 -0
- package/shared/cursor-sensitive-text.d.mts +1 -0
- package/{scripts/lib/cursor-probe-utils.mjs → shared/cursor-sensitive-text.mjs} +1 -13
- package/shared/cursor-setting-sources.d.mts +5 -0
- package/shared/cursor-setting-sources.mjs +22 -0
- package/src/context.ts +21 -12
- package/src/cursor-bridge-contract.ts +1 -3
- package/src/cursor-incomplete-tool-visibility.ts +72 -49
- package/src/cursor-mcp-timeout-override.ts +66 -11
- package/src/cursor-native-tool-display-registration.ts +63 -27
- package/src/cursor-native-tool-display-replay.ts +246 -143
- package/src/cursor-native-tool-display-state.ts +2 -0
- package/src/cursor-native-tool-display-tools.ts +149 -41
- package/src/cursor-provider-live-run-drain.ts +1 -52
- package/src/cursor-provider-run-finalizer.ts +235 -0
- package/src/cursor-provider-run-outcome.ts +149 -0
- package/src/cursor-provider-turn-api-key.ts +8 -0
- package/src/cursor-provider-turn-coordinator.ts +113 -440
- package/src/cursor-provider-turn-display-router.ts +216 -0
- package/src/cursor-provider-turn-emit.ts +59 -0
- package/src/cursor-provider-turn-finalize.ts +119 -0
- package/src/cursor-provider-turn-lifecycle-emitter.ts +97 -0
- package/src/cursor-provider-turn-message-offset.ts +15 -0
- package/src/cursor-provider-turn-prepare.ts +216 -0
- package/src/cursor-provider-turn-runner.ts +138 -0
- package/src/cursor-provider-turn-sdk-normalizer.ts +88 -0
- package/src/cursor-provider-turn-send.ts +103 -0
- package/src/cursor-provider-turn-shell-output.ts +107 -0
- package/src/cursor-provider-turn-tool-ledger.ts +126 -0
- package/src/cursor-provider-turn-types.ts +87 -0
- package/src/cursor-provider.ts +16 -482
- package/src/cursor-replay-activity-builders.ts +276 -0
- package/src/cursor-replay-source-names.ts +33 -0
- package/src/cursor-replay-summary-args.ts +191 -0
- package/src/cursor-replay-tool-details.ts +464 -0
- package/src/cursor-run-final-text.ts +56 -0
- package/src/cursor-sdk-abort-error-guard.ts +4 -0
- package/src/cursor-sdk-event-debug-constants.ts +14 -5
- package/src/cursor-sdk-event-debug.ts +8 -2
- package/src/cursor-sensitive-text.ts +3 -36
- package/src/cursor-session-agent.ts +265 -88
- package/src/cursor-setting-sources.ts +7 -10
- package/src/cursor-state.ts +232 -28
- package/src/cursor-tool-lifecycle.ts +17 -42
- package/src/cursor-tool-manifest.ts +41 -0
- package/src/cursor-tool-names.ts +18 -79
- package/src/cursor-tool-presentation-registry.ts +556 -0
- package/src/cursor-tool-transcript.ts +1 -1
- package/src/cursor-tool-visibility.ts +39 -0
- package/src/cursor-transcript-tool-formatters.ts +0 -59
- package/src/cursor-transcript-tool-specs.ts +169 -232
- package/src/cursor-transcript-utils.ts +0 -44
- package/src/cursor-web-tool-activity.ts +10 -60
- package/src/cursor-web-tool-args.ts +39 -0
- package/src/index.ts +4 -10
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function scrubSensitiveText(text: string, apiKey?: string): string;
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/** Canonical secret/bridge scrubbing (parity-tested by provider runtime and maintainer scripts). */
|
|
2
2
|
|
|
3
3
|
function escapeRegExp(value) {
|
|
4
4
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export function resolveCursorSettingSources(raw) {
|
|
8
|
-
const trimmed = raw?.trim();
|
|
9
|
-
if (!trimmed) return ["all"];
|
|
10
|
-
const normalized = trimmed.toLowerCase();
|
|
11
|
-
if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
|
|
12
|
-
if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
|
|
13
|
-
return trimmed
|
|
14
|
-
.split(",")
|
|
15
|
-
.map((entry) => entry.trim())
|
|
16
|
-
.filter(Boolean);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
7
|
const BRIDGE_ENDPOINT_ROOT = "/cursor-pi-tool-bridge";
|
|
20
8
|
const BRIDGE_ENDPOINT_TOKEN_PATTERN = "[^/\\s\"'<>]+";
|
|
21
9
|
const BRIDGE_LOOPBACK_HOST_PATTERN = "127\\.0\\.0\\.1(?::\\d+)?";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const CURSOR_SETTING_SOURCES_ENV: "PI_CURSOR_SETTING_SOURCES";
|
|
2
|
+
|
|
3
|
+
export declare function resolveCursorSettingSources(raw?: string): string[] | undefined;
|
|
4
|
+
|
|
5
|
+
export declare function serializeCursorSettingSources(settingSources: string[] | undefined): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Canonical Cursor settingSources parsing (parity-tested by provider runtime and maintainer scripts). */
|
|
2
|
+
export const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
|
|
3
|
+
|
|
4
|
+
export function resolveCursorSettingSources(raw) {
|
|
5
|
+
const trimmed = raw?.trim();
|
|
6
|
+
if (!trimmed) return ["all"];
|
|
7
|
+
const normalized = trimmed.toLowerCase();
|
|
8
|
+
if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
|
|
9
|
+
if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
|
|
10
|
+
const sources = trimmed
|
|
11
|
+
.split(",")
|
|
12
|
+
.map((entry) => entry.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
if (sources.length === 0) return undefined;
|
|
15
|
+
return sources;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Serialize parsed settingSources for PI_CURSOR_SETTING_SOURCES (undefined => explicit none). */
|
|
19
|
+
export function serializeCursorSettingSources(settingSources) {
|
|
20
|
+
if (settingSources === undefined || settingSources.length === 0) return "none";
|
|
21
|
+
return settingSources.join(",");
|
|
22
|
+
}
|
package/src/context.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import type { Context, Message, ToolCall } from "@earendil-works/pi-ai";
|
|
3
3
|
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { SDKImage } from "@cursor/sdk";
|
|
5
|
-
import { getCursorPiBridgeContractText } from "./cursor-bridge-contract.js";
|
|
6
5
|
import { getCursorReplayPromptLabel } from "./cursor-tool-names.js";
|
|
7
6
|
|
|
8
7
|
export interface CursorPrompt {
|
|
@@ -14,6 +13,8 @@ export interface CursorPromptOptions {
|
|
|
14
13
|
maxInputTokens?: number;
|
|
15
14
|
charsPerToken?: number;
|
|
16
15
|
imageTokenEstimate?: number;
|
|
16
|
+
/** Compact callable-surface summary; included on bootstrap prompts when set. */
|
|
17
|
+
toolManifest?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export const CURSOR_APPROX_CHARS_PER_TOKEN = 4;
|
|
@@ -21,20 +22,25 @@ export const CURSOR_IMAGE_TOKEN_ESTIMATE = 1200;
|
|
|
21
22
|
const SECTION_SEPARATOR = "\n\n";
|
|
22
23
|
|
|
23
24
|
export function getCursorToolTailGuardText(): string {
|
|
24
|
-
return
|
|
25
|
+
return [
|
|
26
|
+
"Shell: use an explicit `cd` to the repo path when running project commands; session cwd may not match paths in tool args.",
|
|
27
|
+
"Tool boundary reminder: If a tool is needed, call an available Cursor SDK/MCP tool. Never print a tool card (for example Tool call/Shell/command) as assistant text.",
|
|
28
|
+
].join("\n");
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
function getCursorToolBoundaryText(): string {
|
|
28
|
-
|
|
31
|
+
function getCursorToolBoundaryText(options: { hasToolManifest?: boolean } = {}): string {
|
|
32
|
+
const lines = [
|
|
29
33
|
"Cursor SDK tool boundary:",
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
"
|
|
34
|
+
"Call only tools exposed by Cursor SDK in this run. Pi tool names, replay labels, and transcript names are context only—not callable.",
|
|
35
|
+
"Bridged pi tools: call pi__* MCP names when exposed, not the pi card name in history. Replay activity is display-only.",
|
|
36
|
+
"Do not claim pi-side or WebSearch/WebFetch tools unless Cursor executes an equivalent tool.",
|
|
33
37
|
"Use pi__cursor_ask_question for material choices if exposed.",
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
"Images: only the latest user message's images are sent as bytes; ask to reattach or describe prior images.",
|
|
39
|
+
];
|
|
40
|
+
if (options.hasToolManifest) {
|
|
41
|
+
lines.push("See callable tool surfaces block below.");
|
|
42
|
+
}
|
|
43
|
+
return lines.join("\n");
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
function getCursorBootstrapTailSections(): string[] {
|
|
@@ -370,7 +376,10 @@ export function buildCursorIncrementalPrompt(context: Context, options: CursorPr
|
|
|
370
376
|
}
|
|
371
377
|
|
|
372
378
|
export function buildCursorPrompt(context: Context, options: CursorPromptOptions = {}): CursorPrompt {
|
|
373
|
-
const sectionsBeforeMessages: string[] = [getCursorToolBoundaryText()];
|
|
379
|
+
const sectionsBeforeMessages: string[] = [getCursorToolBoundaryText({ hasToolManifest: Boolean(options.toolManifest) })];
|
|
380
|
+
if (options.toolManifest) {
|
|
381
|
+
sectionsBeforeMessages.push(options.toolManifest);
|
|
382
|
+
}
|
|
374
383
|
|
|
375
384
|
if (context.systemPrompt) {
|
|
376
385
|
sectionsBeforeMessages.push(`System instructions from pi:\n${sanitizeSystemPromptForCursor(context.systemPrompt)}`);
|
|
@@ -20,8 +20,6 @@ export function buildCursorPiBridgeMcpToolDescription(options: {
|
|
|
20
20
|
}): string {
|
|
21
21
|
return [
|
|
22
22
|
options.piToolDescription,
|
|
23
|
-
|
|
24
|
-
getCursorPiBridgeContractText(),
|
|
25
|
-
`This run exposes real pi tool ${options.piToolName} as Cursor MCP tool ${options.mcpToolName}.`,
|
|
23
|
+
`Call MCP name ${options.mcpToolName} (pi tool: ${options.piToolName}). Full tool-surface rules are in the session bootstrap prompt.`,
|
|
26
24
|
].join("\n");
|
|
27
25
|
}
|
|
@@ -1,34 +1,63 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
|
|
3
|
-
getCursorReplayDisplayLabel,
|
|
4
|
-
type CursorReplayLegacyToolName,
|
|
5
|
-
} from "./cursor-tool-names.js";
|
|
1
|
+
import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME } from "./cursor-tool-names.js";
|
|
6
2
|
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
7
3
|
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
8
4
|
import {
|
|
9
5
|
DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
|
|
10
6
|
type DiscardedIncompleteStartedToolCallReason,
|
|
11
7
|
} from "./cursor-sdk-event-debug.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
8
|
+
import {
|
|
9
|
+
assembleCursorReplayActivityDetails,
|
|
10
|
+
parseCursorReplayToolDetails,
|
|
11
|
+
resolveIncompleteReplayActivitySourceToolName,
|
|
12
|
+
} from "./cursor-replay-tool-details.js";
|
|
13
|
+
import { truncateArg, type CursorPiToolDisplay } from "./cursor-transcript-utils.js";
|
|
14
|
+
import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
|
|
14
15
|
|
|
15
16
|
export type IncompleteCursorToolDiscardReason = DiscardedIncompleteStartedToolCallReason;
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
export interface IncompleteCursorToolRunOutcome {
|
|
19
|
+
reason: IncompleteCursorToolDiscardReason;
|
|
20
|
+
assistantTextProduced: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IncompleteCursorToolRunOutcomeInput {
|
|
24
|
+
reason?: IncompleteCursorToolDiscardReason;
|
|
25
|
+
status?: string;
|
|
26
|
+
signalAborted?: boolean;
|
|
27
|
+
assistantTextProduced?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type IncompleteCursorToolVisibilityDecision = "emit" | "suppress" | "debugOnly";
|
|
31
|
+
|
|
32
|
+
export function buildIncompleteCursorToolRunOutcome(
|
|
33
|
+
outcome: IncompleteCursorToolRunOutcomeInput = {},
|
|
34
|
+
): IncompleteCursorToolRunOutcome {
|
|
35
|
+
return {
|
|
36
|
+
reason:
|
|
37
|
+
outcome.reason ??
|
|
38
|
+
(outcome.status === "cancelled" || outcome.signalAborted
|
|
39
|
+
? "abort"
|
|
40
|
+
: outcome.status === "error"
|
|
41
|
+
? "sdk-failure"
|
|
42
|
+
: DISCARDED_INCOMPLETE_TOOL_CALL_REASON),
|
|
43
|
+
assistantTextProduced: outcome.assistantTextProduced ?? false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function resolveIncompleteCursorToolVisibility(
|
|
48
|
+
toolCall: unknown,
|
|
49
|
+
outcome: IncompleteCursorToolRunOutcome,
|
|
50
|
+
): IncompleteCursorToolVisibilityDecision {
|
|
51
|
+
const visibility = classifyCursorToolVisibility(toolCall);
|
|
52
|
+
if (
|
|
53
|
+
outcome.reason === DISCARDED_INCOMPLETE_TOOL_CALL_REASON &&
|
|
54
|
+
outcome.assistantTextProduced &&
|
|
55
|
+
visibility.fastLocalDiscovery
|
|
56
|
+
) {
|
|
57
|
+
return "debugOnly";
|
|
58
|
+
}
|
|
59
|
+
return "emit";
|
|
60
|
+
}
|
|
32
61
|
|
|
33
62
|
function buildGenericIncompleteActivityTitle(displayName: string): string {
|
|
34
63
|
if (!displayName || displayName === "unknown") return "Cursor tool";
|
|
@@ -49,25 +78,8 @@ export function formatIncompleteCursorToolReasonText(reason: IncompleteCursorToo
|
|
|
49
78
|
}
|
|
50
79
|
|
|
51
80
|
export function getIncompleteCursorToolActivityTitle(toolCall: unknown): string {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const normalized = normalizeToolName(name).toLowerCase();
|
|
55
|
-
const labelKey = INCOMPLETE_TITLE_KEYS[normalized];
|
|
56
|
-
if (labelKey) return getCursorReplayDisplayLabel(labelKey);
|
|
57
|
-
switch (normalized) {
|
|
58
|
-
case "read":
|
|
59
|
-
return "Cursor read";
|
|
60
|
-
case "shell":
|
|
61
|
-
return "Cursor shell";
|
|
62
|
-
case "grep":
|
|
63
|
-
return "Cursor grep";
|
|
64
|
-
case "glob":
|
|
65
|
-
return "Cursor find";
|
|
66
|
-
case "ls":
|
|
67
|
-
return "Cursor ls";
|
|
68
|
-
default:
|
|
69
|
-
return buildGenericIncompleteActivityTitle(name);
|
|
70
|
-
}
|
|
81
|
+
const visibility = classifyCursorToolVisibility(toolCall);
|
|
82
|
+
return visibility.incompleteTitle ?? buildGenericIncompleteActivityTitle(visibility.displayName);
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
export function buildIncompleteCursorToolDisplay(
|
|
@@ -75,33 +87,44 @@ export function buildIncompleteCursorToolDisplay(
|
|
|
75
87
|
reason: IncompleteCursorToolDiscardReason,
|
|
76
88
|
options: { apiKey?: string } = {},
|
|
77
89
|
): CursorPiToolDisplay {
|
|
78
|
-
const
|
|
79
|
-
const transcriptName = resolveTranscriptToolName(getToolName(toolCall), args);
|
|
90
|
+
const visibility = classifyCursorToolVisibility(toolCall);
|
|
80
91
|
const activityTitle = getIncompleteCursorToolActivityTitle(toolCall);
|
|
81
92
|
const headline = `${activityTitle} did not complete`;
|
|
82
93
|
const reasonText = scrubSensitiveText(formatIncompleteCursorToolReasonText(reason), options.apiKey);
|
|
83
94
|
const contentText = `${headline}\n${reasonText}`;
|
|
95
|
+
const details = assembleCursorReplayActivityDetails(
|
|
96
|
+
resolveIncompleteReplayActivitySourceToolName(visibility.normalizedName),
|
|
97
|
+
headline,
|
|
98
|
+
{ summary: reasonText, expandedText: contentText },
|
|
99
|
+
contentText,
|
|
100
|
+
true,
|
|
101
|
+
reasonText,
|
|
102
|
+
);
|
|
84
103
|
return {
|
|
85
104
|
toolName: CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
|
|
86
105
|
args: {
|
|
87
|
-
cursorToolName:
|
|
106
|
+
cursorToolName: visibility.normalizedName,
|
|
88
107
|
activityTitle,
|
|
89
108
|
activitySummary: reasonText,
|
|
90
109
|
incomplete: true,
|
|
91
110
|
},
|
|
92
111
|
result: {
|
|
93
112
|
content: [{ type: "text", text: contentText }],
|
|
94
|
-
details
|
|
95
|
-
cursorToolName: normalizeToolName(transcriptName),
|
|
96
|
-
title: headline,
|
|
97
|
-
summary: reasonText,
|
|
98
|
-
},
|
|
113
|
+
details,
|
|
99
114
|
},
|
|
100
115
|
isError: true,
|
|
101
116
|
};
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
export function formatIncompleteCursorToolTrace(display: CursorPiToolDisplay): string {
|
|
120
|
+
const parsed = parseCursorReplayToolDetails(display.result.details);
|
|
121
|
+
if (parsed?.variant === "activity") {
|
|
122
|
+
const summary =
|
|
123
|
+
parsed.summary?.trim() ||
|
|
124
|
+
(typeof display.args.activitySummary === "string" && display.args.activitySummary.trim()) ||
|
|
125
|
+
formatIncompleteCursorToolReasonText(DISCARDED_INCOMPLETE_TOOL_CALL_REASON);
|
|
126
|
+
return `${truncateCursorDisplayLine(parsed.title)}: ${truncateCursorDisplayLine(summary)}\n`;
|
|
127
|
+
}
|
|
105
128
|
const details = display.result.details;
|
|
106
129
|
const detailRecord = details && typeof details === "object" ? (details as Record<string, unknown>) : undefined;
|
|
107
130
|
const argsRecord = display.args;
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
const CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS = 60_000;
|
|
2
2
|
const DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS = 3_600_000;
|
|
3
|
+
const DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS = 10_000;
|
|
4
|
+
const MIN_CURSOR_MCP_CONNECT_TIMEOUT_MS = 1_000;
|
|
3
5
|
const MAX_NODE_TIMER_DELAY_MS = 2_147_483_647;
|
|
4
6
|
const CURSOR_MCP_TOOL_TIMEOUT_MS_ENV = "PI_CURSOR_MCP_TOOL_TIMEOUT_MS";
|
|
5
7
|
const CURSOR_MCP_TOOL_TIMEOUT_SECONDS_ENV = "PI_CURSOR_MCP_TOOL_TIMEOUT_SECONDS";
|
|
8
|
+
const CURSOR_MCP_CONNECT_TIMEOUT_MS_ENV = "PI_CURSOR_MCP_CONNECT_TIMEOUT_MS";
|
|
9
|
+
const CURSOR_MCP_CONNECT_TIMEOUT_SECONDS_ENV = "PI_CURSOR_MCP_CONNECT_TIMEOUT_SECONDS";
|
|
6
10
|
|
|
7
11
|
interface CursorMcpToolTimeoutOverrideOptions {
|
|
8
12
|
timeoutMs?: number;
|
|
13
|
+
connectTimeoutMs?: number;
|
|
9
14
|
env?: Record<string, string | undefined>;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
interface CursorMcpToolTimeoutOverrideState {
|
|
13
18
|
installed: boolean;
|
|
14
19
|
timeoutMs: number;
|
|
20
|
+
connectTimeoutMs: number;
|
|
15
21
|
sdkDefaultTimeoutMs: number;
|
|
16
22
|
}
|
|
17
23
|
|
|
@@ -20,7 +26,8 @@ type SetTimeoutHandler = Parameters<GlobalSetTimeout>[0];
|
|
|
20
26
|
type SetTimeoutDelay = Parameters<GlobalSetTimeout>[1];
|
|
21
27
|
|
|
22
28
|
let originalSetTimeout: GlobalSetTimeout | undefined;
|
|
23
|
-
let
|
|
29
|
+
let installedToolTimeoutMs = DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS;
|
|
30
|
+
let installedConnectTimeoutMs = DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
|
|
24
31
|
|
|
25
32
|
function parsePositiveNumber(value: string | undefined): number | undefined {
|
|
26
33
|
const trimmed = value?.trim();
|
|
@@ -46,15 +53,47 @@ export function resolveCursorMcpToolTimeoutMs(
|
|
|
46
53
|
return DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
function normalizeConnectTimeoutMs(timeoutMs: number): number {
|
|
57
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
|
|
58
|
+
return Math.min(
|
|
59
|
+
Math.max(Math.trunc(timeoutMs), MIN_CURSOR_MCP_CONNECT_TIMEOUT_MS),
|
|
60
|
+
CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function resolveCursorMcpConnectTimeoutMs(
|
|
65
|
+
env: Record<string, string | undefined> = process.env,
|
|
66
|
+
): number {
|
|
67
|
+
const explicitMs = parsePositiveNumber(env[CURSOR_MCP_CONNECT_TIMEOUT_MS_ENV]);
|
|
68
|
+
if (explicitMs !== undefined) return normalizeConnectTimeoutMs(explicitMs);
|
|
69
|
+
|
|
70
|
+
const explicitSeconds = parsePositiveNumber(env[CURSOR_MCP_CONNECT_TIMEOUT_SECONDS_ENV]);
|
|
71
|
+
if (explicitSeconds !== undefined) return normalizeConnectTimeoutMs(explicitSeconds * 1000);
|
|
72
|
+
|
|
73
|
+
return DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isCursorSdkMcpProtocolTimeoutStack(stack: string | undefined): boolean {
|
|
50
77
|
if (!stack) return false;
|
|
51
78
|
return (
|
|
52
79
|
/(?:node_modules[/\\]@cursor[/\\]sdk|node_modules\/\@cursor\/sdk|@cursor\/sdk\/dist)/.test(stack) &&
|
|
53
|
-
/\b_setupTimeout\b|\bProtocol\._setupTimeout\b/.test(stack)
|
|
80
|
+
/\b_setupTimeout\b|\bProtocol\._setupTimeout\b/.test(stack)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isCursorSdkMcpToolTimeoutStack(stack: string | undefined): boolean {
|
|
85
|
+
if (!stack) return false;
|
|
86
|
+
return (
|
|
87
|
+
isCursorSdkMcpProtocolTimeoutStack(stack) &&
|
|
54
88
|
/\bcallTool\b|\bClient\.callTool\b|\bMcpSdkClient\.callTool\b/.test(stack)
|
|
55
89
|
);
|
|
56
90
|
}
|
|
57
91
|
|
|
92
|
+
export function isCursorSdkMcpConnectTimeoutStack(stack: string | undefined): boolean {
|
|
93
|
+
if (!stack || !isCursorSdkMcpProtocolTimeoutStack(stack)) return false;
|
|
94
|
+
return /\bClient\.(?:connect|listTools)\b|\bMcpSdkClient\.getTools\b/.test(stack);
|
|
95
|
+
}
|
|
96
|
+
|
|
58
97
|
function isCursorSdkDefaultMcpTimeout(delay: SetTimeoutDelay): boolean {
|
|
59
98
|
return typeof delay === "number" && delay === CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS;
|
|
60
99
|
}
|
|
@@ -67,10 +106,15 @@ function patchedSetTimeout(
|
|
|
67
106
|
const delegate = originalSetTimeout;
|
|
68
107
|
if (!delegate) throw new Error("Cursor MCP timeout override installed without original setTimeout");
|
|
69
108
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
109
|
+
let nextDelay = delay;
|
|
110
|
+
if (isCursorSdkDefaultMcpTimeout(delay)) {
|
|
111
|
+
const stack = new Error().stack;
|
|
112
|
+
if (isCursorSdkMcpToolTimeoutStack(stack)) {
|
|
113
|
+
nextDelay = installedToolTimeoutMs;
|
|
114
|
+
} else if (isCursorSdkMcpConnectTimeoutStack(stack)) {
|
|
115
|
+
nextDelay = installedConnectTimeoutMs;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
74
118
|
|
|
75
119
|
return Reflect.apply(delegate, globalThis, [handler, nextDelay, ...args]) as ReturnType<GlobalSetTimeout>;
|
|
76
120
|
}
|
|
@@ -78,9 +122,12 @@ function patchedSetTimeout(
|
|
|
78
122
|
export function installCursorMcpToolTimeoutOverride(
|
|
79
123
|
options: CursorMcpToolTimeoutOverrideOptions = {},
|
|
80
124
|
): CursorMcpToolTimeoutOverrideState {
|
|
81
|
-
|
|
125
|
+
installedToolTimeoutMs = normalizeOverrideTimeoutMs(
|
|
82
126
|
options.timeoutMs ?? resolveCursorMcpToolTimeoutMs(options.env),
|
|
83
127
|
);
|
|
128
|
+
installedConnectTimeoutMs = normalizeConnectTimeoutMs(
|
|
129
|
+
options.connectTimeoutMs ?? resolveCursorMcpConnectTimeoutMs(options.env),
|
|
130
|
+
);
|
|
84
131
|
|
|
85
132
|
if (!originalSetTimeout) {
|
|
86
133
|
originalSetTimeout = globalThis.setTimeout;
|
|
@@ -89,23 +136,31 @@ export function installCursorMcpToolTimeoutOverride(
|
|
|
89
136
|
|
|
90
137
|
return {
|
|
91
138
|
installed: true,
|
|
92
|
-
timeoutMs:
|
|
139
|
+
timeoutMs: installedToolTimeoutMs,
|
|
140
|
+
connectTimeoutMs: installedConnectTimeoutMs,
|
|
93
141
|
sdkDefaultTimeoutMs: CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS,
|
|
94
142
|
};
|
|
95
143
|
}
|
|
96
144
|
|
|
97
|
-
export function
|
|
145
|
+
export function restoreCursorMcpToolTimeoutOverride(): void {
|
|
98
146
|
if (originalSetTimeout) {
|
|
99
147
|
globalThis.setTimeout = originalSetTimeout;
|
|
100
148
|
originalSetTimeout = undefined;
|
|
101
149
|
}
|
|
102
|
-
|
|
150
|
+
installedToolTimeoutMs = DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS;
|
|
151
|
+
installedConnectTimeoutMs = DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS;
|
|
103
152
|
}
|
|
104
153
|
|
|
154
|
+
export const restoreCursorMcpToolTimeoutOverrideForTests = restoreCursorMcpToolTimeoutOverride;
|
|
155
|
+
|
|
105
156
|
export const cursorMcpToolTimeoutOverrideDefaults = {
|
|
106
157
|
cursorSdkDefaultTimeoutMs: CURSOR_SDK_MCP_DEFAULT_TIMEOUT_MS,
|
|
107
158
|
defaultOverrideTimeoutMs: DEFAULT_CURSOR_MCP_TOOL_TIMEOUT_MS,
|
|
159
|
+
defaultConnectTimeoutMs: DEFAULT_CURSOR_MCP_CONNECT_TIMEOUT_MS,
|
|
160
|
+
minConnectTimeoutMs: MIN_CURSOR_MCP_CONNECT_TIMEOUT_MS,
|
|
108
161
|
maxNodeTimerDelayMs: MAX_NODE_TIMER_DELAY_MS,
|
|
109
162
|
timeoutMsEnv: CURSOR_MCP_TOOL_TIMEOUT_MS_ENV,
|
|
110
163
|
timeoutSecondsEnv: CURSOR_MCP_TOOL_TIMEOUT_SECONDS_ENV,
|
|
164
|
+
connectTimeoutMsEnv: CURSOR_MCP_CONNECT_TIMEOUT_MS_ENV,
|
|
165
|
+
connectTimeoutSecondsEnv: CURSOR_MCP_CONNECT_TIMEOUT_SECONDS_ENV,
|
|
111
166
|
} as const;
|
|
@@ -13,12 +13,19 @@ import {
|
|
|
13
13
|
NATIVE_CURSOR_TOOL_DISPLAY_ENV,
|
|
14
14
|
readBooleanEnv,
|
|
15
15
|
registeredNativeToolNames,
|
|
16
|
+
skippedNativeToolNames,
|
|
16
17
|
} from "./cursor-native-tool-display-state.js";
|
|
17
18
|
import { isCursorReplayToolName } from "./cursor-tool-names.js";
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
+
export const CURSOR_CORE_PI_REPLAY_TOOL_NAMES = ["read", "bash", "edit", "write"] as const;
|
|
21
|
+
const CORE_PI_TOOL_NAMES = new Set<string>(CURSOR_CORE_PI_REPLAY_TOOL_NAMES);
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
function isCursorCorePiReplayToolName(toolName: string): toolName is (typeof CURSOR_CORE_PI_REPLAY_TOOL_NAMES)[number] {
|
|
24
|
+
return CORE_PI_TOOL_NAMES.has(toolName);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type CursorNativeToolActivationApi = Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">;
|
|
28
|
+
type CursorNativeToolRegistryApi = CursorNativeToolActivationApi & Pick<ExtensionAPI, "getAllTools" | "registerTool">;
|
|
22
29
|
|
|
23
30
|
export interface CursorNativeToolDisplayExtensionApi extends CursorNativeToolRegistryApi {
|
|
24
31
|
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
|
@@ -34,7 +41,40 @@ function hasNonBuiltinTool(pi: Pick<ExtensionAPI, "getAllTools">, toolName: Nati
|
|
|
34
41
|
|
|
35
42
|
type NativeRegistrationContext = { hasUI: boolean; ui: Pick<ExtensionContext["ui"], "notify">; model?: ExtensionContext["model"] };
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
function registerNativeCursorToolsFromSet(
|
|
45
|
+
pi: CursorNativeToolRegistryApi,
|
|
46
|
+
toolNames: readonly NativeCursorToolName[],
|
|
47
|
+
): NativeCursorToolName[] {
|
|
48
|
+
const newlySkippedToolNames: NativeCursorToolName[] = [];
|
|
49
|
+
for (const toolName of toolNames) {
|
|
50
|
+
if (registeredNativeToolNames.has(toolName) || skippedNativeToolNames.has(toolName)) continue;
|
|
51
|
+
if (hasNonBuiltinTool(pi, toolName)) {
|
|
52
|
+
skippedNativeToolNames.add(toolName);
|
|
53
|
+
newlySkippedToolNames.push(toolName);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
registerNativeCursorTool(pi, toolName);
|
|
57
|
+
registeredNativeToolNames.add(toolName);
|
|
58
|
+
}
|
|
59
|
+
return newlySkippedToolNames;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function notifySkippedNativeCursorToolsIfNeeded(ctx: NativeRegistrationContext, skippedToolNames: readonly NativeCursorToolName[]): void {
|
|
63
|
+
if (skippedToolNames.length === 0 || readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) !== true || !ctx.hasUI) return;
|
|
64
|
+
ctx.ui.notify(
|
|
65
|
+
`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.`,
|
|
66
|
+
"warning",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hasAttemptedNativeCursorToolRegistration(): boolean {
|
|
71
|
+
return registeredNativeToolNames.size > 0 || skippedNativeToolNames.size > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function syncRegisteredNativeCursorToolsForModel(
|
|
75
|
+
pi: CursorNativeToolActivationApi,
|
|
76
|
+
model: ExtensionContext["model"],
|
|
77
|
+
): void {
|
|
38
78
|
if (registeredNativeToolNames.size === 0) return;
|
|
39
79
|
const activeToolNames = new Set(pi.getActiveTools());
|
|
40
80
|
let changed = false;
|
|
@@ -47,7 +87,7 @@ export function syncRegisteredNativeCursorToolsForModel(pi: Pick<ExtensionAPI, "
|
|
|
47
87
|
}
|
|
48
88
|
} else {
|
|
49
89
|
for (const toolName of registeredNativeToolNames) {
|
|
50
|
-
if (
|
|
90
|
+
if (isCursorCorePiReplayToolName(toolName)) continue;
|
|
51
91
|
if (!activeToolNames.delete(toolName)) continue;
|
|
52
92
|
changed = true;
|
|
53
93
|
}
|
|
@@ -55,45 +95,41 @@ export function syncRegisteredNativeCursorToolsForModel(pi: Pick<ExtensionAPI, "
|
|
|
55
95
|
if (changed) pi.setActiveTools([...activeToolNames]);
|
|
56
96
|
}
|
|
57
97
|
|
|
58
|
-
function
|
|
98
|
+
function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
|
|
59
99
|
if (!isCursorNativeToolRegistrationRequested()) {
|
|
60
100
|
registeredNativeToolNames.clear();
|
|
101
|
+
skippedNativeToolNames.clear();
|
|
61
102
|
return;
|
|
62
103
|
}
|
|
104
|
+
if (!isCursorModel(ctx.model) || hasAttemptedNativeCursorToolRegistration()) return;
|
|
63
105
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
registerNativeCursorTool(pi, toolName);
|
|
72
|
-
registeredNativeToolNames.add(toolName);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
syncRegisteredNativeCursorToolsForModel(pi, ctx.model);
|
|
106
|
+
const nonCoreToolNames = NATIVE_CURSOR_TOOL_NAMES.filter((toolName) => !isCursorCorePiReplayToolName(toolName));
|
|
107
|
+
const skippedToolNames = [
|
|
108
|
+
...registerNativeCursorToolsFromSet(pi, nonCoreToolNames),
|
|
109
|
+
...registerNativeCursorToolsFromSet(pi, CURSOR_CORE_PI_REPLAY_TOOL_NAMES),
|
|
110
|
+
];
|
|
111
|
+
notifySkippedNativeCursorToolsIfNeeded(ctx, skippedToolNames);
|
|
112
|
+
}
|
|
76
113
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"warning",
|
|
81
|
-
);
|
|
114
|
+
function ensureThenSyncNativeCursorToolsForModel(pi: CursorNativeToolRegistryApi, ctx: NativeRegistrationContext): void {
|
|
115
|
+
if (isCursorModel(ctx.model) && !hasAttemptedNativeCursorToolRegistration()) {
|
|
116
|
+
ensureNativeCursorToolsRegisteredForModel(pi, ctx);
|
|
82
117
|
}
|
|
118
|
+
syncRegisteredNativeCursorToolsForModel(pi, ctx.model);
|
|
83
119
|
}
|
|
84
120
|
|
|
85
121
|
export function registerCursorNativeToolDisplay(pi: CursorNativeToolDisplayExtensionApi): void {
|
|
86
122
|
pi.on("session_start", (_event, ctx) => {
|
|
87
|
-
|
|
123
|
+
ensureThenSyncNativeCursorToolsForModel(pi, ctx);
|
|
88
124
|
});
|
|
89
125
|
pi.on("before_agent_start", (_event, ctx) => {
|
|
90
|
-
|
|
126
|
+
ensureThenSyncNativeCursorToolsForModel(pi, ctx);
|
|
91
127
|
});
|
|
92
128
|
pi.on("turn_start", (_event, ctx) => {
|
|
93
|
-
|
|
129
|
+
ensureThenSyncNativeCursorToolsForModel(pi, ctx);
|
|
94
130
|
});
|
|
95
|
-
pi.on("model_select", (event) => {
|
|
96
|
-
|
|
131
|
+
pi.on("model_select", (event, ctx) => {
|
|
132
|
+
ensureThenSyncNativeCursorToolsForModel(pi, { ...ctx, model: event.model });
|
|
97
133
|
});
|
|
98
134
|
}
|
|
99
135
|
|