pi-cursor-sdk 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +59 -1
- package/docs/cursor-live-smoke-checklist.md +4 -1
- package/docs/cursor-model-ux-spec.md +7 -5
- package/docs/cursor-native-tool-replay.md +99 -3
- package/docs/cursor-testing-lessons.md +234 -5
- package/package.json +10 -2
- package/scripts/debug-provider-events.mjs +403 -0
- package/scripts/debug-sdk-events.mjs +413 -0
- package/scripts/lib/cursor-probe-utils.mjs +52 -0
- package/scripts/lib/cursor-sdk-output-filter.mjs +86 -0
- package/scripts/probe-mcp-coldstart.mjs +244 -0
- package/scripts/validate-smoke-jsonl.mjs +27 -3
- package/src/context.ts +45 -32
- package/src/cursor-agent-message-web-tools.ts +172 -0
- package/src/cursor-agents-context.ts +176 -0
- package/src/cursor-incomplete-tool-visibility.ts +124 -0
- package/src/cursor-live-run-coordinator.ts +18 -7
- package/src/cursor-mcp-timeout-override.ts +66 -11
- package/src/cursor-model.ts +12 -0
- package/src/cursor-native-tool-display-registration.ts +1 -4
- package/src/cursor-native-tool-display-replay.ts +65 -6
- package/src/cursor-native-tool-display-tools.ts +20 -0
- package/src/cursor-pi-tool-bridge-diagnostics.ts +11 -1
- package/src/cursor-pi-tool-bridge-run.ts +16 -1
- package/src/cursor-pi-tool-bridge-types.ts +3 -0
- package/src/cursor-provider-errors.ts +96 -0
- package/src/cursor-provider-live-run-drain.ts +181 -62
- package/src/cursor-provider-turn-coordinator.ts +220 -33
- package/src/cursor-provider.ts +302 -93
- package/src/cursor-question-tool.ts +1 -4
- package/src/cursor-sdk-abort-error-guard.ts +109 -0
- package/src/cursor-sdk-event-debug-constants.ts +40 -0
- package/src/cursor-sdk-event-debug-session.ts +163 -0
- package/src/cursor-sdk-event-debug.ts +602 -0
- package/src/cursor-sensitive-text.ts +27 -7
- package/src/cursor-session-agent.ts +279 -82
- package/src/cursor-session-send-policy.ts +43 -0
- package/src/cursor-setting-sources.ts +29 -0
- package/src/cursor-state.ts +1 -5
- package/src/cursor-tool-lifecycle.ts +85 -0
- package/src/cursor-tool-names.ts +39 -0
- package/src/cursor-tool-transcript.ts +4 -2
- package/src/cursor-tool-visibility.ts +63 -0
- package/src/cursor-transcript-tool-formatters.ts +228 -5
- package/src/cursor-transcript-tool-specs.ts +135 -24
- package/src/cursor-transcript-utils.ts +12 -0
- package/src/cursor-web-tool-activity.ts +84 -0
- package/src/index.ts +4 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BeforeAgentStartEvent,
|
|
3
|
+
BeforeAgentStartEventResult,
|
|
4
|
+
BuildSystemPromptOptions,
|
|
5
|
+
ExtensionAPI,
|
|
6
|
+
ExtensionContext,
|
|
7
|
+
ExtensionHandler,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
11
|
+
import { isCursorModel } from "./cursor-model.js";
|
|
12
|
+
import {
|
|
13
|
+
cursorSettingSourcesLoadProjectAgentsRules,
|
|
14
|
+
cursorSettingSourcesLoadUserAgentsRules,
|
|
15
|
+
getEffectiveCursorSettingSources,
|
|
16
|
+
} from "./cursor-setting-sources.js";
|
|
17
|
+
import type { SettingSource } from "@cursor/sdk";
|
|
18
|
+
|
|
19
|
+
export const CURSOR_PRESERVE_PI_AGENTS_MD_ENV = "PI_CURSOR_PRESERVE_PI_AGENTS_MD";
|
|
20
|
+
|
|
21
|
+
/** Opening tag prefix pi `buildSystemPrompt()` uses for each context file (path attribute only). */
|
|
22
|
+
export const PI_PROJECT_INSTRUCTIONS_OPEN_PREFIX = '<project_instructions path="';
|
|
23
|
+
const PI_PROJECT_INSTRUCTIONS_CLOSE = "</project_instructions>";
|
|
24
|
+
const PI_PROJECT_CONTEXT_OPEN = "\n\n<project_context>\n\nProject-specific instructions and guidelines:\n\n";
|
|
25
|
+
const PI_PROJECT_CONTEXT_CLOSE = "</project_context>\n";
|
|
26
|
+
|
|
27
|
+
function normalizeContextPath(filePath: string): string {
|
|
28
|
+
return filePath.replace(/\\/g, "/");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeDirPath(dirPath: string): string {
|
|
32
|
+
const normalized = normalizeContextPath(dirPath).replace(/\/+$/, "");
|
|
33
|
+
return normalized || "/";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type PiAgentsContextFile = {
|
|
37
|
+
path: string;
|
|
38
|
+
content: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Overlap classes for pi context files that Cursor also loads via `settingSources`. */
|
|
42
|
+
export type PiAgentsContextOverlap = "none" | "cursor-user-agents" | "cursor-project-rules";
|
|
43
|
+
|
|
44
|
+
/** Pi context filenames that can overlap Cursor project/user ambient rules. */
|
|
45
|
+
const CURSOR_OVERLAPPING_CONTEXT_BASE_NAMES = new Set(["agents.md", "claude.md"]);
|
|
46
|
+
|
|
47
|
+
export function getAgentsContextFileBaseName(filePath: string): string {
|
|
48
|
+
const normalized = normalizeContextPath(filePath);
|
|
49
|
+
return normalized.slice(normalized.lastIndexOf("/") + 1).toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Actual pi agent dir `AGENTS.md` — overlaps Cursor `user` setting source (global agent instructions). */
|
|
53
|
+
export function isPiAgentDirAgentsMdPath(filePath: string, agentDir: string = getAgentDir()): boolean {
|
|
54
|
+
const normalized = normalizeContextPath(filePath);
|
|
55
|
+
const agentsMdPath = `${normalizeDirPath(agentDir)}/agents.md`;
|
|
56
|
+
return normalized.toLowerCase() === agentsMdPath.toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Actual pi agent dir `CLAUDE.md` — kept because Cursor user rules use `~/.claude/CLAUDE.md`. */
|
|
60
|
+
export function isPiAgentDirClaudeMdPath(filePath: string, agentDir: string = getAgentDir()): boolean {
|
|
61
|
+
const normalized = normalizeContextPath(filePath);
|
|
62
|
+
const claudeMdPath = `${normalizeDirPath(agentDir)}/claude.md`;
|
|
63
|
+
return normalized.toLowerCase() === claudeMdPath.toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Classify whether a pi-loaded context file overlaps Cursor ambient rules.
|
|
68
|
+
* Project/repo `AGENTS.md` and `CLAUDE.md` overlap Cursor `project` sources.
|
|
69
|
+
* Only the actual pi agent dir `AGENTS.md` overlaps Cursor `user`; agent-dir `CLAUDE.md` is kept
|
|
70
|
+
* because Cursor user rules use `~/.claude/CLAUDE.md`, not pi's agent dir path.
|
|
71
|
+
*/
|
|
72
|
+
export function classifyContextFileOverlap(
|
|
73
|
+
filePath: string,
|
|
74
|
+
agentDir: string = getAgentDir(),
|
|
75
|
+
): PiAgentsContextOverlap {
|
|
76
|
+
const base = getAgentsContextFileBaseName(filePath);
|
|
77
|
+
if (!CURSOR_OVERLAPPING_CONTEXT_BASE_NAMES.has(base)) return "none";
|
|
78
|
+
if (base === "agents.md" && isPiAgentDirAgentsMdPath(filePath, agentDir)) return "cursor-user-agents";
|
|
79
|
+
if (base === "claude.md" && isPiAgentDirClaudeMdPath(filePath, agentDir)) return "none";
|
|
80
|
+
return "cursor-project-rules";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function shouldRemovePiAgentsContextFile(
|
|
84
|
+
file: PiAgentsContextFile,
|
|
85
|
+
settingSources: SettingSource[] | undefined,
|
|
86
|
+
agentDir?: string,
|
|
87
|
+
): boolean {
|
|
88
|
+
switch (classifyContextFileOverlap(file.path, agentDir)) {
|
|
89
|
+
case "cursor-user-agents":
|
|
90
|
+
return cursorSettingSourcesLoadUserAgentsRules(settingSources);
|
|
91
|
+
case "cursor-project-rules":
|
|
92
|
+
return cursorSettingSourcesLoadProjectAgentsRules(settingSources);
|
|
93
|
+
default:
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function shouldSuppressPiAgentsContext(
|
|
99
|
+
model: ExtensionContext["model"],
|
|
100
|
+
contextFiles: readonly PiAgentsContextFile[],
|
|
101
|
+
settingSources: SettingSource[] | undefined,
|
|
102
|
+
agentDir?: string,
|
|
103
|
+
): boolean {
|
|
104
|
+
if (!isCursorModel(model)) return false;
|
|
105
|
+
if (parseEnvBoolean(process.env[CURSOR_PRESERVE_PI_AGENTS_MD_ENV], false)) return false;
|
|
106
|
+
if (contextFiles.length === 0) return false;
|
|
107
|
+
return contextFiles.some((file) => shouldRemovePiAgentsContextFile(file, settingSources, agentDir));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Exact pi `buildSystemPrompt()` serialization for one context file block (including trailing blank line). */
|
|
111
|
+
export function serializePiProjectInstructionsBlock(file: PiAgentsContextFile): string {
|
|
112
|
+
return `${PI_PROJECT_INSTRUCTIONS_OPEN_PREFIX}${file.path}">\n${file.content}\n${PI_PROJECT_INSTRUCTIONS_CLOSE}\n\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Exact pi `buildSystemPrompt()` serialization for the full project context section. */
|
|
116
|
+
export function serializePiProjectContextSection(contextFiles: readonly PiAgentsContextFile[]): string {
|
|
117
|
+
if (contextFiles.length === 0) return "";
|
|
118
|
+
return `${PI_PROJECT_CONTEXT_OPEN}${contextFiles.map(serializePiProjectInstructionsBlock).join("")}${PI_PROJECT_CONTEXT_CLOSE}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Remove pi context blocks that overlap Cursor setting sources. */
|
|
122
|
+
export function removePiAgentsContextFromSystemPrompt(
|
|
123
|
+
systemPrompt: string,
|
|
124
|
+
contextFiles: readonly PiAgentsContextFile[],
|
|
125
|
+
settingSources: SettingSource[] | undefined,
|
|
126
|
+
agentDir?: string,
|
|
127
|
+
): string {
|
|
128
|
+
const retainedContextFiles: PiAgentsContextFile[] = [];
|
|
129
|
+
let removedAny = false;
|
|
130
|
+
for (const file of contextFiles) {
|
|
131
|
+
if (shouldRemovePiAgentsContextFile(file, settingSources, agentDir)) {
|
|
132
|
+
removedAny = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
retainedContextFiles.push(file);
|
|
136
|
+
}
|
|
137
|
+
if (!removedAny) return systemPrompt;
|
|
138
|
+
|
|
139
|
+
const originalSection = serializePiProjectContextSection(contextFiles);
|
|
140
|
+
const start = systemPrompt.indexOf(originalSection);
|
|
141
|
+
if (start < 0) return systemPrompt;
|
|
142
|
+
|
|
143
|
+
const replacementSection = serializePiProjectContextSection(retainedContextFiles);
|
|
144
|
+
return systemPrompt.slice(0, start) + replacementSection + systemPrompt.slice(start + originalSection.length);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function resolveCursorFacingSystemPrompt(
|
|
148
|
+
systemPrompt: string,
|
|
149
|
+
model: ExtensionContext["model"],
|
|
150
|
+
systemPromptOptions?: BuildSystemPromptOptions,
|
|
151
|
+
settingSourcesRaw?: string,
|
|
152
|
+
agentDir?: string,
|
|
153
|
+
): string {
|
|
154
|
+
if (!systemPromptOptions) return systemPrompt;
|
|
155
|
+
const contextFiles = systemPromptOptions.contextFiles ?? [];
|
|
156
|
+
const settingSources = getEffectiveCursorSettingSources(settingSourcesRaw);
|
|
157
|
+
if (!shouldSuppressPiAgentsContext(model, contextFiles, settingSources, agentDir)) {
|
|
158
|
+
return systemPrompt;
|
|
159
|
+
}
|
|
160
|
+
return removePiAgentsContextFromSystemPrompt(systemPrompt, contextFiles, settingSources, agentDir);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
type CursorAgentsContextExtensionApi = Pick<ExtensionAPI, "on">;
|
|
164
|
+
|
|
165
|
+
export function registerCursorAgentsContextDedup(pi: CursorAgentsContextExtensionApi): void {
|
|
166
|
+
const handler: ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult> = (event, ctx) => {
|
|
167
|
+
const resolved = resolveCursorFacingSystemPrompt(
|
|
168
|
+
event.systemPrompt,
|
|
169
|
+
ctx.model,
|
|
170
|
+
event.systemPromptOptions,
|
|
171
|
+
);
|
|
172
|
+
if (resolved === event.systemPrompt) return;
|
|
173
|
+
return { systemPrompt: resolved };
|
|
174
|
+
};
|
|
175
|
+
pi.on("before_agent_start", handler);
|
|
176
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { CURSOR_REPLAY_ACTIVITY_TOOL_NAME } from "./cursor-tool-names.js";
|
|
2
|
+
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
3
|
+
import { scrubSensitiveText } from "./cursor-sensitive-text.js";
|
|
4
|
+
import {
|
|
5
|
+
DISCARDED_INCOMPLETE_TOOL_CALL_REASON,
|
|
6
|
+
type DiscardedIncompleteStartedToolCallReason,
|
|
7
|
+
} from "./cursor-sdk-event-debug.js";
|
|
8
|
+
import { truncateArg, type CursorPiToolDisplay } from "./cursor-transcript-utils.js";
|
|
9
|
+
import { classifyCursorToolVisibility } from "./cursor-tool-visibility.js";
|
|
10
|
+
|
|
11
|
+
export type IncompleteCursorToolDiscardReason = DiscardedIncompleteStartedToolCallReason;
|
|
12
|
+
|
|
13
|
+
export interface IncompleteCursorToolRunOutcome {
|
|
14
|
+
reason: IncompleteCursorToolDiscardReason;
|
|
15
|
+
assistantTextProduced: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface IncompleteCursorToolRunOutcomeInput {
|
|
19
|
+
reason?: IncompleteCursorToolDiscardReason;
|
|
20
|
+
status?: string;
|
|
21
|
+
signalAborted?: boolean;
|
|
22
|
+
assistantTextProduced?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type IncompleteCursorToolVisibilityDecision = "emit" | "suppress" | "debugOnly";
|
|
26
|
+
|
|
27
|
+
export function buildIncompleteCursorToolRunOutcome(
|
|
28
|
+
outcome: IncompleteCursorToolRunOutcomeInput = {},
|
|
29
|
+
): IncompleteCursorToolRunOutcome {
|
|
30
|
+
return {
|
|
31
|
+
reason:
|
|
32
|
+
outcome.reason ??
|
|
33
|
+
(outcome.status === "cancelled" || outcome.signalAborted
|
|
34
|
+
? "abort"
|
|
35
|
+
: outcome.status === "error"
|
|
36
|
+
? "sdk-failure"
|
|
37
|
+
: DISCARDED_INCOMPLETE_TOOL_CALL_REASON),
|
|
38
|
+
assistantTextProduced: outcome.assistantTextProduced ?? false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveIncompleteCursorToolVisibility(
|
|
43
|
+
toolCall: unknown,
|
|
44
|
+
outcome: IncompleteCursorToolRunOutcome,
|
|
45
|
+
): IncompleteCursorToolVisibilityDecision {
|
|
46
|
+
const visibility = classifyCursorToolVisibility(toolCall);
|
|
47
|
+
if (
|
|
48
|
+
outcome.reason === DISCARDED_INCOMPLETE_TOOL_CALL_REASON &&
|
|
49
|
+
outcome.assistantTextProduced &&
|
|
50
|
+
visibility.fastLocalDiscovery
|
|
51
|
+
) {
|
|
52
|
+
return "debugOnly";
|
|
53
|
+
}
|
|
54
|
+
return "emit";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildGenericIncompleteActivityTitle(displayName: string): string {
|
|
58
|
+
if (!displayName || displayName === "unknown") return "Cursor tool";
|
|
59
|
+
return `Cursor ${truncateArg(displayName)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function formatIncompleteCursorToolReasonText(reason: IncompleteCursorToolDiscardReason): string {
|
|
63
|
+
switch (reason) {
|
|
64
|
+
case DISCARDED_INCOMPLETE_TOOL_CALL_REASON:
|
|
65
|
+
return "missing completion";
|
|
66
|
+
case "abort":
|
|
67
|
+
return "aborted";
|
|
68
|
+
case "sdk-failure":
|
|
69
|
+
return "SDK run failed";
|
|
70
|
+
case "run-drain":
|
|
71
|
+
return "run ended during drain";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getIncompleteCursorToolActivityTitle(toolCall: unknown): string {
|
|
76
|
+
const visibility = classifyCursorToolVisibility(toolCall);
|
|
77
|
+
return visibility.incompleteTitle ?? buildGenericIncompleteActivityTitle(visibility.displayName);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildIncompleteCursorToolDisplay(
|
|
81
|
+
toolCall: unknown,
|
|
82
|
+
reason: IncompleteCursorToolDiscardReason,
|
|
83
|
+
options: { apiKey?: string } = {},
|
|
84
|
+
): CursorPiToolDisplay {
|
|
85
|
+
const visibility = classifyCursorToolVisibility(toolCall);
|
|
86
|
+
const activityTitle = getIncompleteCursorToolActivityTitle(toolCall);
|
|
87
|
+
const headline = `${activityTitle} did not complete`;
|
|
88
|
+
const reasonText = scrubSensitiveText(formatIncompleteCursorToolReasonText(reason), options.apiKey);
|
|
89
|
+
const contentText = `${headline}\n${reasonText}`;
|
|
90
|
+
return {
|
|
91
|
+
toolName: CURSOR_REPLAY_ACTIVITY_TOOL_NAME,
|
|
92
|
+
args: {
|
|
93
|
+
cursorToolName: visibility.normalizedName,
|
|
94
|
+
activityTitle,
|
|
95
|
+
activitySummary: reasonText,
|
|
96
|
+
incomplete: true,
|
|
97
|
+
},
|
|
98
|
+
result: {
|
|
99
|
+
content: [{ type: "text", text: contentText }],
|
|
100
|
+
details: {
|
|
101
|
+
cursorToolName: visibility.normalizedName,
|
|
102
|
+
title: headline,
|
|
103
|
+
summary: reasonText,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
isError: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function formatIncompleteCursorToolTrace(display: CursorPiToolDisplay): string {
|
|
111
|
+
const details = display.result.details;
|
|
112
|
+
const detailRecord = details && typeof details === "object" ? (details as Record<string, unknown>) : undefined;
|
|
113
|
+
const argsRecord = display.args;
|
|
114
|
+
const title =
|
|
115
|
+
(typeof detailRecord?.title === "string" && detailRecord.title.trim()) ||
|
|
116
|
+
(typeof argsRecord.activityTitle === "string" && argsRecord.activityTitle.trim()
|
|
117
|
+
? `${argsRecord.activityTitle} did not complete`
|
|
118
|
+
: "Cursor tool did not complete");
|
|
119
|
+
const summary =
|
|
120
|
+
(typeof detailRecord?.summary === "string" && detailRecord.summary.trim()) ||
|
|
121
|
+
(typeof argsRecord.activitySummary === "string" && argsRecord.activitySummary.trim()) ||
|
|
122
|
+
formatIncompleteCursorToolReasonText(DISCARDED_INCOMPLETE_TOOL_CALL_REASON);
|
|
123
|
+
return `${truncateCursorDisplayLine(title)}: ${truncateCursorDisplayLine(summary)}\n`;
|
|
124
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import type { CursorNativeToolDisplayItem } from "./cursor-native-tool-display.js";
|
|
11
11
|
import type { CursorPiBridgeToolRequest, CursorPiToolBridgeRun } from "./cursor-pi-tool-bridge.js";
|
|
12
12
|
import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
13
|
+
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
13
14
|
|
|
14
15
|
export class CursorLiveRunAbortError extends Error {
|
|
15
16
|
constructor() {
|
|
@@ -46,7 +47,9 @@ export interface CursorLiveRun {
|
|
|
46
47
|
cancelled: boolean;
|
|
47
48
|
disposed: boolean;
|
|
48
49
|
errorMessage?: string;
|
|
50
|
+
abortMessage?: string;
|
|
49
51
|
chainUserInputAfterCompletion: boolean;
|
|
52
|
+
debugRecorder?: CursorSdkEventDebugRecorder;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
export interface CursorLiveRunCreateParams {
|
|
@@ -57,6 +60,7 @@ export interface CursorLiveRunCreateParams {
|
|
|
57
60
|
sessionAgentScopeKey?: string;
|
|
58
61
|
promptInputTokens: number;
|
|
59
62
|
textDeltas?: string[];
|
|
63
|
+
debugRecorder?: CursorSdkEventDebugRecorder;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
export interface CursorLiveRunCoordinatorDeps {
|
|
@@ -70,7 +74,7 @@ export interface CursorLiveRunCoordinator {
|
|
|
70
74
|
start(params: CursorLiveRunCreateParams): CursorLiveRun;
|
|
71
75
|
attachSdkRun(run: CursorLiveRun, sdkRun: CursorLiveSdkRun): void;
|
|
72
76
|
markFinished(run: CursorLiveRun, finalText: string): void;
|
|
73
|
-
markCancelled(run: CursorLiveRun): void;
|
|
77
|
+
markCancelled(run: CursorLiveRun, abortMessage?: string): void;
|
|
74
78
|
markError(run: CursorLiveRun, errorMessage: string): void;
|
|
75
79
|
queueEvent(run: CursorLiveRun, event: CursorLiveQueuedEvent): void;
|
|
76
80
|
peekEvent(run: CursorLiveRun): CursorLiveQueuedEvent | undefined;
|
|
@@ -268,6 +272,7 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
268
272
|
cancelled: false,
|
|
269
273
|
disposed: false,
|
|
270
274
|
chainUserInputAfterCompletion: false,
|
|
275
|
+
debugRecorder: params.debugRecorder,
|
|
271
276
|
};
|
|
272
277
|
privateStates.set(run, {
|
|
273
278
|
waiters: new Set(),
|
|
@@ -294,9 +299,10 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
294
299
|
coordinator.requestIdleDispose(run);
|
|
295
300
|
},
|
|
296
301
|
|
|
297
|
-
markCancelled(run): void {
|
|
302
|
+
markCancelled(run, abortMessage): void {
|
|
298
303
|
if (run.disposed) return;
|
|
299
304
|
run.cancelled = true;
|
|
305
|
+
run.abortMessage = abortMessage;
|
|
300
306
|
run.done = true;
|
|
301
307
|
notifyProgress(run);
|
|
302
308
|
coordinator.requestIdleDispose(run);
|
|
@@ -313,6 +319,7 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
313
319
|
queueEvent(run, event): void {
|
|
314
320
|
if (run.disposed) return;
|
|
315
321
|
run.pendingEvents.push(event);
|
|
322
|
+
run.debugRecorder?.recordLiveRunEvent(event);
|
|
316
323
|
notifyProgress(run);
|
|
317
324
|
},
|
|
318
325
|
|
|
@@ -433,7 +440,9 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
433
440
|
if (state.leased || state.leaseQueue.length > 0) return;
|
|
434
441
|
state.idleDisposeRequested = false;
|
|
435
442
|
state.idleDisposeTimer = setTimeout(() => {
|
|
436
|
-
void coordinator.release(run)
|
|
443
|
+
void coordinator.release(run).catch(() => {
|
|
444
|
+
// Idle dispose must not leave release failures as unhandled rejections.
|
|
445
|
+
});
|
|
437
446
|
}, deps.getIdleDisposeMs());
|
|
438
447
|
state.idleDisposeTimer.unref?.();
|
|
439
448
|
},
|
|
@@ -463,10 +472,12 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
463
472
|
}
|
|
464
473
|
}
|
|
465
474
|
if (abandoned) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
475
|
+
if (!run.done) {
|
|
476
|
+
try {
|
|
477
|
+
await run.sdkRun?.cancel();
|
|
478
|
+
} catch {
|
|
479
|
+
// cancellation failure should not block session-agent abandonment
|
|
480
|
+
}
|
|
470
481
|
}
|
|
471
482
|
await deps.abandonSessionAgent(run.sessionAgentScopeKey);
|
|
472
483
|
}
|
|
@@ -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;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const CURSOR_PROVIDER = "cursor";
|
|
4
|
+
export const CURSOR_SDK_API = "cursor-sdk";
|
|
5
|
+
|
|
6
|
+
export type CursorModelRef =
|
|
7
|
+
| Pick<NonNullable<ExtensionContext["model"]>, "provider" | "api">
|
|
8
|
+
| undefined;
|
|
9
|
+
|
|
10
|
+
export function isCursorModel(model: CursorModelRef): boolean {
|
|
11
|
+
return model?.provider === CURSOR_PROVIDER || model?.api === CURSOR_SDK_API;
|
|
12
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
registerNativeCursorTool,
|
|
7
7
|
type NativeCursorToolName,
|
|
8
8
|
} from "./cursor-native-tool-display-tools.js";
|
|
9
|
+
import { isCursorModel } from "./cursor-model.js";
|
|
9
10
|
import {
|
|
10
11
|
isCursorNativeToolDisplayRequested,
|
|
11
12
|
isCursorNativeToolRegistrationRequested,
|
|
@@ -33,10 +34,6 @@ function hasNonBuiltinTool(pi: Pick<ExtensionAPI, "getAllTools">, toolName: Nati
|
|
|
33
34
|
|
|
34
35
|
type NativeRegistrationContext = { hasUI: boolean; ui: Pick<ExtensionContext["ui"], "notify">; model?: ExtensionContext["model"] };
|
|
35
36
|
|
|
36
|
-
function isCursorModel(model: ExtensionContext["model"]): boolean {
|
|
37
|
-
return model?.provider === "cursor" || model?.api === "cursor-sdk";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
37
|
export function syncRegisteredNativeCursorToolsForModel(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">, model: ExtensionContext["model"]): void {
|
|
41
38
|
if (registeredNativeToolNames.size === 0) return;
|
|
42
39
|
const activeToolNames = new Set(pi.getActiveTools());
|