pi-cursor-sdk 0.2.0 → 0.3.6
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 +62 -0
- package/README.md +54 -50
- package/dist/bundled-context-windows.js +39 -0
- package/dist/context-window-cache.js +92 -0
- package/dist/context.js +338 -0
- package/dist/cursor-active-tools.js +3 -0
- package/dist/cursor-agent-message-web-tools.js +165 -0
- package/dist/cursor-agents-context-registration.js +17 -0
- package/dist/cursor-agents-context.js +113 -0
- package/dist/cursor-api-key.js +34 -0
- package/dist/cursor-bridge-contract.js +27 -0
- package/dist/cursor-cloud-lifecycle.js +650 -0
- package/dist/cursor-cloud-local-state.js +460 -0
- package/dist/cursor-cloud-options.js +145 -0
- package/dist/cursor-cloud-reporting.js +222 -0
- package/dist/cursor-compact-tool-summary.js +75 -0
- package/dist/cursor-config.js +613 -0
- package/dist/cursor-context-tools.js +4 -0
- package/dist/cursor-display-only-trace.js +11 -0
- package/dist/cursor-display-text.js +18 -0
- package/dist/cursor-durable-fs.js +53 -0
- package/dist/cursor-edit-diff.js +13 -0
- package/dist/cursor-env-boolean.js +19 -0
- package/dist/cursor-fallback-models.generated.js +7760 -0
- package/dist/cursor-fallback-warning.js +15 -0
- package/dist/cursor-http1.js +45 -0
- package/dist/cursor-incomplete-tool-visibility.js +83 -0
- package/dist/cursor-live-run-accounting.js +55 -0
- package/dist/cursor-live-run-coordinator.js +406 -0
- package/dist/cursor-mcp-timeout-override.js +116 -0
- package/dist/cursor-model-lifecycle.js +32 -0
- package/dist/cursor-model.js +5 -0
- package/dist/cursor-native-replay-routing.js +35 -0
- package/dist/cursor-native-replay-trace.js +28 -0
- package/dist/cursor-native-tool-display-registration.js +102 -0
- package/dist/cursor-native-tool-display-replay.js +428 -0
- package/dist/cursor-native-tool-display-state.js +70 -0
- package/dist/cursor-native-tool-display-tools.js +123 -0
- package/dist/cursor-native-tool-names.js +11 -0
- package/dist/cursor-partial-content-emitter.js +118 -0
- package/dist/cursor-pi-tool-bridge-abort.js +114 -0
- package/dist/cursor-pi-tool-bridge-constants.js +6 -0
- package/dist/cursor-pi-tool-bridge-diagnostics.js +125 -0
- package/dist/cursor-pi-tool-bridge-env.js +18 -0
- package/dist/cursor-pi-tool-bridge-mcp.js +100 -0
- package/dist/cursor-pi-tool-bridge-run.js +355 -0
- package/dist/cursor-pi-tool-bridge-server.js +163 -0
- package/dist/cursor-pi-tool-bridge-snapshot.js +63 -0
- package/dist/cursor-pi-tool-bridge-types.js +1 -0
- package/dist/cursor-pi-tool-bridge.js +123 -0
- package/dist/cursor-provider-errors.js +311 -0
- package/dist/cursor-provider-lazy.js +39 -0
- package/dist/cursor-provider-live-run-drain.js +412 -0
- package/dist/cursor-provider-overflow.js +90 -0
- package/dist/cursor-provider-run-finalizer.js +176 -0
- package/dist/cursor-provider-run-outcome.js +76 -0
- package/dist/cursor-provider-turn-coordinator.js +275 -0
- package/dist/cursor-provider-turn-display-router.js +158 -0
- package/dist/cursor-provider-turn-emit.js +35 -0
- package/dist/cursor-provider-turn-finalize.js +154 -0
- package/dist/cursor-provider-turn-lifecycle-emitter.js +126 -0
- package/dist/cursor-provider-turn-prepare.js +374 -0
- package/dist/cursor-provider-turn-runner.js +146 -0
- package/dist/cursor-provider-turn-sdk-normalizer.js +74 -0
- package/dist/cursor-provider-turn-send.js +138 -0
- package/dist/cursor-provider-turn-shell-output.js +117 -0
- package/dist/cursor-provider-turn-tool-ledger.js +99 -0
- package/dist/cursor-provider-turn-types.js +1 -0
- package/dist/cursor-provider.js +72 -0
- package/dist/cursor-question-tool.js +194 -0
- package/dist/cursor-record-utils.js +61 -0
- package/dist/cursor-replay-activity-builders.js +114 -0
- package/dist/cursor-replay-source-names.js +27 -0
- package/dist/cursor-replay-summary-args.js +105 -0
- package/dist/cursor-replay-tool-details.js +167 -0
- package/dist/cursor-ripgrep-path.js +27 -0
- package/dist/cursor-run-final-text.js +63 -0
- package/dist/cursor-runtime-state.js +380 -0
- package/dist/cursor-sdk-billed-usage.js +86 -0
- package/dist/cursor-sdk-event-debug-constants.js +33 -0
- package/dist/cursor-sdk-event-debug-session.js +107 -0
- package/dist/cursor-sdk-event-debug.js +461 -0
- package/dist/cursor-sdk-output-filter.js +1 -0
- package/dist/cursor-sdk-process-error-guard.js +221 -0
- package/dist/cursor-sdk-runtime.js +3 -0
- package/dist/cursor-sensitive-text.js +23 -0
- package/dist/cursor-session-agent-cleanup.js +311 -0
- package/dist/cursor-session-agent-lifecycle.js +32 -0
- package/dist/cursor-session-agent-lineage.js +103 -0
- package/dist/cursor-session-agent-resume.js +417 -0
- package/dist/cursor-session-agent.js +507 -0
- package/dist/cursor-session-compaction-prep.js +21 -0
- package/dist/cursor-session-scope.js +113 -0
- package/dist/cursor-session-send-policy.js +18 -0
- package/dist/cursor-session-store.js +105 -0
- package/dist/cursor-session-turn-queue.js +50 -0
- package/dist/cursor-setting-sources.js +15 -0
- package/dist/cursor-skill-tool.js +230 -0
- package/dist/cursor-state.js +519 -0
- package/dist/cursor-task-presentation.js +59 -0
- package/dist/cursor-tool-lifecycle.js +94 -0
- package/dist/cursor-tool-manifest.js +34 -0
- package/dist/cursor-tool-presentation-registry.js +305 -0
- package/dist/cursor-tool-result-display-readers.js +158 -0
- package/dist/cursor-tool-transcript.js +32 -0
- package/dist/cursor-tool-visibility.js +28 -0
- package/dist/cursor-transcript-tool-formatters.js +618 -0
- package/dist/cursor-transcript-tool-specs.js +284 -0
- package/dist/cursor-transcript-utils.js +170 -0
- package/dist/cursor-usage-accounting.js +166 -0
- package/dist/cursor-web-tool-activity.js +23 -0
- package/dist/cursor-web-tool-args.js +12 -0
- package/dist/index.js +82 -0
- package/dist/model-discovery.js +305 -0
- package/dist/model-list-cache.js +156 -0
- package/docs/cursor-dogfood-checklist.md +2 -1
- package/docs/cursor-live-smoke-checklist.md +13 -12
- package/docs/cursor-model-ux-spec.md +25 -6
- package/docs/cursor-native-tool-replay.md +13 -13
- package/docs/cursor-native-tool-visual-audit.md +6 -3
- package/docs/cursor-testing-lessons.md +7 -7
- package/docs/cursor-tool-surfaces.md +5 -5
- package/docs/platform-smoke.md +3 -3
- package/package.json +19 -7
- package/platform-smoke.config.mjs +1 -1
- package/scripts/build.mjs +126 -0
- package/scripts/cloud-runtime-smoke.mjs +3 -1
- package/scripts/debug-provider-events.mjs +3 -1
- package/scripts/debug-sdk-events.mjs +1 -1
- package/scripts/isolated-cursor-smoke.sh +5 -4
- package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +1 -1
- package/scripts/lib/cursor-visual-render.mjs +1 -1
- package/scripts/lib/ensure-built.mjs +16 -0
- package/scripts/lib/local-resume-smoke-harness.mjs +1 -1
- package/scripts/local-resume-smoke.mjs +3 -1
- package/scripts/platform-smoke/card-detect.mjs +1 -1
- package/scripts/platform-smoke/live-suite-runner.mjs +1 -1
- package/scripts/platform-smoke/render-ansi.mjs +1 -1
- package/scripts/platform-smoke/targets.mjs +2 -2
- package/scripts/prepare.mjs +102 -0
- package/scripts/steering-rpc-smoke.mjs +4 -2
- package/scripts/tmux-live-smoke.sh +11 -11
- package/scripts/visual-tui-smoke-self-test.mjs +4 -1
- package/scripts/visual-tui-smoke.mjs +51 -7
- package/src/bundled-context-windows.ts +4 -1
- package/src/cursor-agents-context-registration.ts +2 -2
- package/src/cursor-agents-context.ts +0 -2
- package/src/cursor-api-key.ts +5 -4
- package/src/cursor-cloud-reporting.ts +23 -2
- package/src/cursor-fallback-models.generated.ts +592 -204
- package/src/cursor-live-run-coordinator.ts +1 -0
- package/src/cursor-native-tool-display-registration.ts +10 -11
- package/src/cursor-provider-errors.ts +1 -1
- package/src/cursor-provider-lazy.ts +5 -4
- package/src/cursor-provider-live-run-drain.ts +1 -0
- package/src/cursor-provider-run-finalizer.ts +1 -0
- package/src/cursor-provider-turn-finalize.ts +12 -0
- package/src/cursor-provider-turn-types.ts +2 -0
- package/src/cursor-sdk-billed-usage.ts +107 -0
- package/src/cursor-sdk-process-error-guard.ts +6 -4
- package/src/cursor-session-agent-lifecycle.ts +8 -9
- package/src/cursor-session-agent-resume.ts +23 -0
- package/src/cursor-session-compaction-prep.ts +4 -0
- package/src/cursor-tool-manifest.ts +1 -1
- package/src/cursor-usage-accounting.ts +70 -9
- package/src/index.ts +1 -1
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +16 -0
package/dist/context.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { convertToLlm } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { CURSOR_PI_BRIDGE_PREFERENCE_TEXT } from "./cursor-bridge-contract.js";
|
|
4
|
+
import { getCursorReplayPromptLabel } from "./cursor-tool-presentation-registry.js";
|
|
5
|
+
export const CURSOR_APPROX_CHARS_PER_TOKEN = 4;
|
|
6
|
+
export const CURSOR_IMAGE_TOKEN_ESTIMATE = 1200;
|
|
7
|
+
const SECTION_SEPARATOR = "\n\n";
|
|
8
|
+
export function getCursorPlanModeToolGuidanceText(agentMode, options = {}) {
|
|
9
|
+
if (agentMode !== "plan")
|
|
10
|
+
return undefined;
|
|
11
|
+
return [
|
|
12
|
+
"Cursor SDK mode is plan for this run. In pi-cursor-sdk, plan mode may still use available Cursor SDK/MCP tools for inspection when needed.",
|
|
13
|
+
"Safe/read-only shell commands that inspect or print information are allowed when Cursor chooses to call Shell; do not say Shell is blocked by plan mode and then call it anyway.",
|
|
14
|
+
options.includePiBridgeGuidance === false
|
|
15
|
+
? undefined
|
|
16
|
+
: "Exposed pi__* bridge tools are also callable in plan mode when the user asks for them or they are needed to answer.",
|
|
17
|
+
].filter((line) => line !== undefined).join("\n");
|
|
18
|
+
}
|
|
19
|
+
export function getCursorToolTailGuardText(options = {}) {
|
|
20
|
+
return [
|
|
21
|
+
"Shell: use explicit `cd` to repo path for project commands; session cwd may differ from tool args.",
|
|
22
|
+
options.includePlanModeGuidance === false
|
|
23
|
+
? undefined
|
|
24
|
+
: getCursorPlanModeToolGuidanceText(options.agentMode, { includePiBridgeGuidance: options.includePiBridgeGuidance }),
|
|
25
|
+
"Exact-output requests: output exactly the requested text; no preamble or checks unless asked.",
|
|
26
|
+
"Tools: call available Cursor SDK/MCP tools; never print tool cards as assistant text.",
|
|
27
|
+
options.includePiBridgeGuidance === false ? undefined : CURSOR_PI_BRIDGE_PREFERENCE_TEXT,
|
|
28
|
+
].filter((line) => line !== undefined).join("\n");
|
|
29
|
+
}
|
|
30
|
+
function getCursorToolBoundaryText(options = {}) {
|
|
31
|
+
const includePiBridgeGuidance = options.includePiBridgeGuidance !== false;
|
|
32
|
+
const includePiAskQuestionGuidance = includePiBridgeGuidance && options.includePiAskQuestionGuidance !== false;
|
|
33
|
+
const lines = [
|
|
34
|
+
"Cursor SDK tool boundary:",
|
|
35
|
+
"Call only Cursor SDK/MCP tools exposed in this run; pi history names, replay labels, and transcript names are not callable.",
|
|
36
|
+
includePiBridgeGuidance
|
|
37
|
+
? "For exposed pi bridge tools, call pi__* MCP names, not pi card/history names."
|
|
38
|
+
: undefined,
|
|
39
|
+
"Do not claim pi-side or WebSearch/WebFetch tools unless Cursor ran an equivalent tool.",
|
|
40
|
+
includePiAskQuestionGuidance ? "Use pi__cursor_ask_question for material choices if exposed." : undefined,
|
|
41
|
+
getCursorPlanModeToolGuidanceText(options.agentMode, { includePiBridgeGuidance }),
|
|
42
|
+
"Images: only latest user images are sent; ask to reattach prior images.",
|
|
43
|
+
].filter((line) => line !== undefined);
|
|
44
|
+
if (options.hasToolManifest) {
|
|
45
|
+
lines.push("See callable surfaces below.");
|
|
46
|
+
}
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
function getCursorBootstrapTailSections(options = {}) {
|
|
50
|
+
return [
|
|
51
|
+
"Answer the latest user request above using the instructions and Cursor SDK capabilities available in this run.",
|
|
52
|
+
getCursorToolTailGuardText({ ...options, includePlanModeGuidance: false }),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
function normalizePiContextMessages(messages) {
|
|
56
|
+
return convertToLlm(messages);
|
|
57
|
+
}
|
|
58
|
+
function isTextBlock(block) {
|
|
59
|
+
return block.type === "text";
|
|
60
|
+
}
|
|
61
|
+
function isImageBlock(block) {
|
|
62
|
+
return block.type === "image";
|
|
63
|
+
}
|
|
64
|
+
function isToolCallBlock(block) {
|
|
65
|
+
return block.type === "toolCall";
|
|
66
|
+
}
|
|
67
|
+
function extractLatestImages(messages) {
|
|
68
|
+
// Find the last user message and extract images only from it
|
|
69
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
70
|
+
const msg = messages[i];
|
|
71
|
+
if (msg.role !== "user")
|
|
72
|
+
continue;
|
|
73
|
+
if (typeof msg.content === "string")
|
|
74
|
+
return [];
|
|
75
|
+
const images = [];
|
|
76
|
+
for (const block of msg.content) {
|
|
77
|
+
if (isImageBlock(block) && block.data && block.mimeType) {
|
|
78
|
+
images.push({ data: block.data, mimeType: block.mimeType });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return images;
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
function formatContentBlocks(content) {
|
|
86
|
+
if (typeof content === "string")
|
|
87
|
+
return content;
|
|
88
|
+
return content
|
|
89
|
+
.map((block) => {
|
|
90
|
+
if (isTextBlock(block))
|
|
91
|
+
return block.text;
|
|
92
|
+
if (isImageBlock(block))
|
|
93
|
+
return "[image omitted from transcript]";
|
|
94
|
+
return "";
|
|
95
|
+
})
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.join("\n");
|
|
98
|
+
}
|
|
99
|
+
function formatToolCall(toolCall) {
|
|
100
|
+
const args = JSON.stringify(toolCall.arguments) ?? "";
|
|
101
|
+
return `Tool call (${getCursorReplayPromptLabel(toolCall.name)}, call ${toolCall.id}): ${args}`;
|
|
102
|
+
}
|
|
103
|
+
function sanitizeSystemPromptForCursor(systemPrompt) {
|
|
104
|
+
let sanitized = systemPrompt;
|
|
105
|
+
sanitized = sanitized.replace(/Available tools:\n[\s\S]*?\n\nIn addition to the tools above, you may have access to other custom tools depending on the project\.\n\n/g, "Pi tool catalog omitted: Cursor can call only Cursor SDK tools exposed in this run.\n\n");
|
|
106
|
+
sanitized = sanitized.replace(/Guidelines:\n[\s\S]*?\n\nPi documentation /g, "Guidelines:\n- Be concise in your responses.\n- Show file paths clearly when working with files.\n\nPi documentation ");
|
|
107
|
+
// Keep the Agent Skills catalog. Cursor-specific skill activation wording is normalized
|
|
108
|
+
// by cursor-skill-tool.ts before this prompt reaches the Cursor SDK provider.
|
|
109
|
+
sanitized = sanitized.replace(/\n+Semantic code intelligence priority:[\s\S]*$/g, "");
|
|
110
|
+
return sanitized.trim();
|
|
111
|
+
}
|
|
112
|
+
function formatMessage(msg) {
|
|
113
|
+
switch (msg.role) {
|
|
114
|
+
case "user": {
|
|
115
|
+
const text = formatContentBlocks(msg.content);
|
|
116
|
+
return text ? `User: ${text}` : undefined;
|
|
117
|
+
}
|
|
118
|
+
case "assistant": {
|
|
119
|
+
const blocks = Array.isArray(msg.content) ? msg.content : [{ type: "text", text: String(msg.content) }];
|
|
120
|
+
const textParts = [];
|
|
121
|
+
for (const block of blocks) {
|
|
122
|
+
if (isTextBlock(block)) {
|
|
123
|
+
textParts.push(block.text);
|
|
124
|
+
}
|
|
125
|
+
else if (isToolCallBlock(block)) {
|
|
126
|
+
textParts.push(formatToolCall(block));
|
|
127
|
+
}
|
|
128
|
+
// Omit thinking content from transcript
|
|
129
|
+
}
|
|
130
|
+
return textParts.length > 0 ? `Assistant: ${textParts.join("\n")}` : undefined;
|
|
131
|
+
}
|
|
132
|
+
case "toolResult": {
|
|
133
|
+
const text = formatContentBlocks(msg.content);
|
|
134
|
+
const label = msg.isError ? "Tool error" : "Tool result";
|
|
135
|
+
return `${label} (${getCursorReplayPromptLabel(msg.toolName)}, call ${msg.toolCallId}): ${text}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function getLatestUserMessageIndex(messages) {
|
|
140
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
141
|
+
if (messages[index].role === "user")
|
|
142
|
+
return index;
|
|
143
|
+
}
|
|
144
|
+
return -1;
|
|
145
|
+
}
|
|
146
|
+
function getSectionCost(section) {
|
|
147
|
+
return section.length + SECTION_SEPARATOR.length;
|
|
148
|
+
}
|
|
149
|
+
function applyPromptBudget(sectionsBeforeMessages, messageSections, sectionsAfterMessages, latestUserMessageIndex, options) {
|
|
150
|
+
const maxInputTokens = options.maxInputTokens;
|
|
151
|
+
if (maxInputTokens === undefined || !Number.isFinite(maxInputTokens) || maxInputTokens <= 0) {
|
|
152
|
+
return [...sectionsBeforeMessages, ...messageSections.map((section) => section.text), ...sectionsAfterMessages];
|
|
153
|
+
}
|
|
154
|
+
const charsPerToken = options.charsPerToken ?? CURSOR_APPROX_CHARS_PER_TOKEN;
|
|
155
|
+
const maxChars = Math.max(1, Math.floor(maxInputTokens * charsPerToken));
|
|
156
|
+
const requiredMessageSections = messageSections.filter((section) => section.index === latestUserMessageIndex);
|
|
157
|
+
const requiredCost = [...sectionsBeforeMessages, ...requiredMessageSections.map((section) => section.text), ...sectionsAfterMessages].reduce((total, section) => total + getSectionCost(section), 0);
|
|
158
|
+
let remainingChars = maxChars - requiredCost;
|
|
159
|
+
const includedMessageIndexes = new Set(requiredMessageSections.map((section) => section.index));
|
|
160
|
+
let omittedMessageCount = 0;
|
|
161
|
+
for (let index = messageSections.length - 1; index >= 0; index -= 1) {
|
|
162
|
+
const section = messageSections[index];
|
|
163
|
+
if (includedMessageIndexes.has(section.index))
|
|
164
|
+
continue;
|
|
165
|
+
const cost = getSectionCost(section.text);
|
|
166
|
+
if (cost <= remainingChars) {
|
|
167
|
+
includedMessageIndexes.add(section.index);
|
|
168
|
+
remainingChars -= cost;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
omittedMessageCount += messageSections
|
|
172
|
+
.slice(0, index + 1)
|
|
173
|
+
.filter((candidate) => !includedMessageIndexes.has(candidate.index)).length;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
const budgetNotice = omittedMessageCount > 0
|
|
177
|
+
? [`[Earlier transcript omitted: ${omittedMessageCount} message${omittedMessageCount === 1 ? "" : "s"} to fit Cursor context budget]`]
|
|
178
|
+
: [];
|
|
179
|
+
const includedMessages = messageSections
|
|
180
|
+
.filter((section) => includedMessageIndexes.has(section.index))
|
|
181
|
+
.map((section) => section.text);
|
|
182
|
+
return [...sectionsBeforeMessages, ...budgetNotice, ...includedMessages, ...sectionsAfterMessages];
|
|
183
|
+
}
|
|
184
|
+
export function estimateCursorTextTokens(text, options = {}) {
|
|
185
|
+
const charsPerToken = options.charsPerToken ?? CURSOR_APPROX_CHARS_PER_TOKEN;
|
|
186
|
+
return Math.ceil(text.length / charsPerToken);
|
|
187
|
+
}
|
|
188
|
+
export function estimateCursorPromptTokens(prompt, options = {}) {
|
|
189
|
+
return estimateCursorTextTokens(prompt.text, options) + prompt.images.length * (options.imageTokenEstimate ?? CURSOR_IMAGE_TOKEN_ESTIMATE);
|
|
190
|
+
}
|
|
191
|
+
export function estimateCursorPromptMessageTokens(message, options = {}) {
|
|
192
|
+
const text = formatMessage(message);
|
|
193
|
+
return text ? estimateCursorTextTokens(text, options) : 0;
|
|
194
|
+
}
|
|
195
|
+
export function estimateCursorContextTokens(context, options = {}) {
|
|
196
|
+
return estimateCursorPromptTokens(buildCursorPrompt(context, options), options);
|
|
197
|
+
}
|
|
198
|
+
function hashCursorContextValue(value) {
|
|
199
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
200
|
+
}
|
|
201
|
+
function serializeMessageForFingerprint(message, index) {
|
|
202
|
+
switch (message.role) {
|
|
203
|
+
case "user": {
|
|
204
|
+
const text = typeof message.content === "string"
|
|
205
|
+
? message.content
|
|
206
|
+
: JSON.stringify(message.content);
|
|
207
|
+
return hashCursorContextValue(`user:${message.timestamp ?? index}:${text}`);
|
|
208
|
+
}
|
|
209
|
+
case "assistant":
|
|
210
|
+
return hashCursorContextValue(`assistant:${message.timestamp ?? index}:${JSON.stringify(message.content)}`);
|
|
211
|
+
case "toolResult":
|
|
212
|
+
return hashCursorContextValue(`toolResult:${message.timestamp ?? index}:${message.toolCallId}:${message.toolName}:${JSON.stringify(message.content)}:${message.isError === true}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function serializeRawPiMessageForFingerprint(message, index) {
|
|
216
|
+
const role = message.role;
|
|
217
|
+
switch (role) {
|
|
218
|
+
case "branchSummary": {
|
|
219
|
+
const entry = message;
|
|
220
|
+
return hashCursorContextValue(`branchSummary:${entry.timestamp ?? index}:${entry.fromId ?? ""}:${entry.summary ?? ""}`);
|
|
221
|
+
}
|
|
222
|
+
case "compactionSummary": {
|
|
223
|
+
const entry = message;
|
|
224
|
+
return hashCursorContextValue(`compactionSummary:${entry.timestamp ?? index}:${entry.tokensBefore ?? ""}:${entry.summary ?? ""}`);
|
|
225
|
+
}
|
|
226
|
+
case "custom": {
|
|
227
|
+
const entry = message;
|
|
228
|
+
return hashCursorContextValue(`custom:${entry.timestamp ?? index}:${entry.customType ?? ""}:${JSON.stringify(entry.content)}`);
|
|
229
|
+
}
|
|
230
|
+
case "bashExecution": {
|
|
231
|
+
const entry = message;
|
|
232
|
+
if (entry.excludeFromContext) {
|
|
233
|
+
return hashCursorContextValue(`bashExecution:excluded:${entry.timestamp ?? index}`);
|
|
234
|
+
}
|
|
235
|
+
return hashCursorContextValue(`bashExecution:${entry.timestamp ?? index}:${entry.command ?? ""}:${entry.output ?? ""}:${entry.exitCode ?? ""}:${entry.cancelled === true}`);
|
|
236
|
+
}
|
|
237
|
+
default:
|
|
238
|
+
return serializeMessageForFingerprint(message, index);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function parseCursorContextFingerprint(fingerprint) {
|
|
242
|
+
try {
|
|
243
|
+
const parsed = JSON.parse(fingerprint);
|
|
244
|
+
if (!parsed || typeof parsed.systemHash !== "string" || !Array.isArray(parsed.messageHashes))
|
|
245
|
+
return undefined;
|
|
246
|
+
if (!parsed.messageHashes.every((entry) => typeof entry === "string"))
|
|
247
|
+
return undefined;
|
|
248
|
+
return parsed;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
export function computeCursorContextFingerprint(context) {
|
|
255
|
+
const payload = {
|
|
256
|
+
systemHash: hashCursorContextValue(context.systemPrompt ?? ""),
|
|
257
|
+
messageHashes: context.messages.map((message, index) => serializeRawPiMessageForFingerprint(message, index)),
|
|
258
|
+
};
|
|
259
|
+
return JSON.stringify(payload);
|
|
260
|
+
}
|
|
261
|
+
export function shouldBootstrapCursorContext(sendState, context) {
|
|
262
|
+
if (!sendState.bootstrapped)
|
|
263
|
+
return true;
|
|
264
|
+
const previous = parseCursorContextFingerprint(sendState.contextFingerprint);
|
|
265
|
+
if (!previous)
|
|
266
|
+
return true;
|
|
267
|
+
const current = parseCursorContextFingerprint(computeCursorContextFingerprint(context));
|
|
268
|
+
if (!current)
|
|
269
|
+
return true;
|
|
270
|
+
if (current.systemHash !== previous.systemHash)
|
|
271
|
+
return true;
|
|
272
|
+
if (current.messageHashes.length < previous.messageHashes.length)
|
|
273
|
+
return true;
|
|
274
|
+
if (current.messageHashes.length > previous.messageHashes.length) {
|
|
275
|
+
for (let index = previous.messageHashes.length; index < context.messages.length; index += 1) {
|
|
276
|
+
const role = context.messages[index].role;
|
|
277
|
+
if (role === "branchSummary" || role === "compactionSummary")
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (let index = 0; index < previous.messageHashes.length; index += 1) {
|
|
282
|
+
if (current.messageHashes[index] !== previous.messageHashes[index])
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
/** @deprecated Use planCursorSessionSend() for send mode and shouldBootstrapCursorContext() for context-only checks. */
|
|
288
|
+
export function shouldBootstrapCursorSend(sendState, context) {
|
|
289
|
+
return shouldBootstrapCursorContext(sendState, context);
|
|
290
|
+
}
|
|
291
|
+
export function buildCursorIncrementalPrompt(context, options = {}) {
|
|
292
|
+
// Incremental sends omit Pi system instructions and the full tool boundary; the session agent retains both from bootstrap.
|
|
293
|
+
const messages = normalizePiContextMessages(context.messages);
|
|
294
|
+
const latestUserMessageIndex = getLatestUserMessageIndex(messages);
|
|
295
|
+
const latestUserMessage = latestUserMessageIndex >= 0 ? messages[latestUserMessageIndex] : undefined;
|
|
296
|
+
const latestUserText = latestUserMessage ? formatMessage(latestUserMessage) : undefined;
|
|
297
|
+
const sectionsBeforeMessages = [
|
|
298
|
+
"Continue the conversation using Cursor SDK capabilities only. Do not list, promise, or call pi-only tools from earlier context as if they were available.",
|
|
299
|
+
];
|
|
300
|
+
const latestUserMessageSections = latestUserText && latestUserMessageIndex >= 0 ? [{ index: latestUserMessageIndex, text: latestUserText }] : [];
|
|
301
|
+
const images = extractLatestImages(messages);
|
|
302
|
+
const imageTokenReserve = images.length * (options.imageTokenEstimate ?? 0);
|
|
303
|
+
const budgetOptions = options.maxInputTokens === undefined
|
|
304
|
+
? options
|
|
305
|
+
: { ...options, maxInputTokens: Math.max(1, options.maxInputTokens - imageTokenReserve) };
|
|
306
|
+
const parts = applyPromptBudget(sectionsBeforeMessages, latestUserMessageSections, [getCursorToolTailGuardText(options)], latestUserMessageIndex, budgetOptions);
|
|
307
|
+
return { text: parts.join(SECTION_SEPARATOR), images };
|
|
308
|
+
}
|
|
309
|
+
export function buildCursorPrompt(context, options = {}) {
|
|
310
|
+
const sectionsBeforeMessages = [getCursorToolBoundaryText({
|
|
311
|
+
agentMode: options.agentMode,
|
|
312
|
+
hasToolManifest: Boolean(options.toolManifest),
|
|
313
|
+
includePiBridgeGuidance: options.includePiBridgeGuidance,
|
|
314
|
+
includePiAskQuestionGuidance: options.includePiAskQuestionGuidance,
|
|
315
|
+
})];
|
|
316
|
+
if (options.toolManifest) {
|
|
317
|
+
sectionsBeforeMessages.push(options.toolManifest);
|
|
318
|
+
}
|
|
319
|
+
if (context.systemPrompt) {
|
|
320
|
+
sectionsBeforeMessages.push(`System instructions from pi:\n${sanitizeSystemPromptForCursor(context.systemPrompt)}`);
|
|
321
|
+
}
|
|
322
|
+
const messages = normalizePiContextMessages(context.messages);
|
|
323
|
+
const messageSections = messages
|
|
324
|
+
.map((msg, index) => {
|
|
325
|
+
const text = formatMessage(msg);
|
|
326
|
+
return text ? { index, text } : undefined;
|
|
327
|
+
})
|
|
328
|
+
.filter((section) => section !== undefined);
|
|
329
|
+
const sectionsAfterMessages = getCursorBootstrapTailSections(options);
|
|
330
|
+
const images = extractLatestImages(messages);
|
|
331
|
+
const imageTokenReserve = images.length * (options.imageTokenEstimate ?? 0);
|
|
332
|
+
const budgetOptions = options.maxInputTokens === undefined
|
|
333
|
+
? options
|
|
334
|
+
: { ...options, maxInputTokens: Math.max(1, options.maxInputTokens - imageTokenReserve) };
|
|
335
|
+
const parts = applyPromptBudget(sectionsBeforeMessages, messageSections, sectionsAfterMessages, getLatestUserMessageIndex(messages), budgetOptions);
|
|
336
|
+
const text = parts.join(SECTION_SEPARATOR);
|
|
337
|
+
return { text, images };
|
|
338
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { asRecord, getArray, getString } from "./cursor-record-utils.js";
|
|
2
|
+
import { stringifyUnknown } from "./cursor-transcript-utils.js";
|
|
3
|
+
import { loadCursorSdk } from "./cursor-sdk-runtime.js";
|
|
4
|
+
const CURSOR_AGENT_MESSAGE_PAGE_LIMIT = 8;
|
|
5
|
+
function getOneofCaseValue(value, caseName) {
|
|
6
|
+
const record = asRecord(value);
|
|
7
|
+
if (!record)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (record.case === caseName)
|
|
10
|
+
return record.value;
|
|
11
|
+
return record[caseName];
|
|
12
|
+
}
|
|
13
|
+
async function hasCursorAgentMessageAt(agentId, cwd, offset, store) {
|
|
14
|
+
const { Agent } = await loadCursorSdk();
|
|
15
|
+
const messages = await Agent.messages.list(agentId, {
|
|
16
|
+
runtime: "local",
|
|
17
|
+
cwd,
|
|
18
|
+
...(store ? { store } : {}),
|
|
19
|
+
limit: 1,
|
|
20
|
+
offset,
|
|
21
|
+
});
|
|
22
|
+
return messages.length > 0;
|
|
23
|
+
}
|
|
24
|
+
export async function countCursorAgentMessages(agentId, cwd, store) {
|
|
25
|
+
let high = 1;
|
|
26
|
+
while (await hasCursorAgentMessageAt(agentId, cwd, high, store)) {
|
|
27
|
+
high *= 2;
|
|
28
|
+
}
|
|
29
|
+
let low = 0;
|
|
30
|
+
while (low < high) {
|
|
31
|
+
const mid = Math.floor((low + high) / 2);
|
|
32
|
+
if (await hasCursorAgentMessageAt(agentId, cwd, mid, store))
|
|
33
|
+
low = mid + 1;
|
|
34
|
+
else
|
|
35
|
+
high = mid;
|
|
36
|
+
}
|
|
37
|
+
return low;
|
|
38
|
+
}
|
|
39
|
+
export async function loadCursorTranscriptWebToolCallsAfterOffset(options) {
|
|
40
|
+
if (options.offset === undefined)
|
|
41
|
+
return [];
|
|
42
|
+
const { Agent } = await loadCursorSdk();
|
|
43
|
+
const messages = await Agent.messages.list(options.agentId, {
|
|
44
|
+
runtime: "local",
|
|
45
|
+
cwd: options.cwd,
|
|
46
|
+
...(options.store ? { store: options.store } : {}),
|
|
47
|
+
limit: CURSOR_AGENT_MESSAGE_PAGE_LIMIT,
|
|
48
|
+
offset: options.offset,
|
|
49
|
+
});
|
|
50
|
+
return collectCursorTranscriptWebToolCalls(messages);
|
|
51
|
+
}
|
|
52
|
+
export function collectCursorTranscriptWebToolCalls(messages) {
|
|
53
|
+
const toolCalls = [];
|
|
54
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
55
|
+
const messageId = message.uuid || `${message.agent_id || "cursor-agent"}:${messageIndex}`;
|
|
56
|
+
const steps = getAgentConversationSteps(message.message);
|
|
57
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
58
|
+
const webTool = getStepWebToolPayload(step);
|
|
59
|
+
if (!webTool)
|
|
60
|
+
continue;
|
|
61
|
+
const converted = convertCursorTranscriptWebTool(webTool);
|
|
62
|
+
if (!converted)
|
|
63
|
+
continue;
|
|
64
|
+
const args = asRecord(converted.args);
|
|
65
|
+
const toolCallId = getString(args, "toolCallId") ?? getString(args, "tool_call_id") ?? `${stepIndex}`;
|
|
66
|
+
toolCalls.push({
|
|
67
|
+
identity: `cursor-transcript:${messageId}:${webTool.kind}:${toolCallId}`,
|
|
68
|
+
toolCall: converted,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return toolCalls;
|
|
73
|
+
}
|
|
74
|
+
function getAgentConversationSteps(message) {
|
|
75
|
+
const record = asRecord(message);
|
|
76
|
+
const turn = getOneofCaseValue(record?.turn, "agentConversationTurn") ?? record?.agentConversationTurn;
|
|
77
|
+
return getArray(asRecord(turn), "steps") ?? [];
|
|
78
|
+
}
|
|
79
|
+
function getStepToolCall(step) {
|
|
80
|
+
const stepRecord = asRecord(step);
|
|
81
|
+
const message = asRecord(stepRecord?.message);
|
|
82
|
+
return getOneofCaseValue(message, "toolCall") ?? stepRecord?.toolCall ?? message?.toolCall;
|
|
83
|
+
}
|
|
84
|
+
function getStepWebToolPayload(step) {
|
|
85
|
+
const toolCall = getStepToolCall(step);
|
|
86
|
+
const toolCallRecord = asRecord(toolCall);
|
|
87
|
+
const tool = toolCallRecord?.tool;
|
|
88
|
+
const webSearchPayload = getOneofCaseValue(tool, "webSearchToolCall") ??
|
|
89
|
+
getOneofCaseValue(toolCall, "webSearchToolCall") ??
|
|
90
|
+
toolCallRecord?.webSearchToolCall;
|
|
91
|
+
if (webSearchPayload)
|
|
92
|
+
return { kind: "webSearch", payload: webSearchPayload };
|
|
93
|
+
const webFetchPayload = getOneofCaseValue(tool, "webFetchToolCall") ??
|
|
94
|
+
getOneofCaseValue(toolCall, "webFetchToolCall") ??
|
|
95
|
+
toolCallRecord?.webFetchToolCall;
|
|
96
|
+
if (webFetchPayload)
|
|
97
|
+
return { kind: "webFetch", payload: webFetchPayload };
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
function convertCursorTranscriptWebTool(webTool) {
|
|
101
|
+
const payload = asRecord(webTool.payload);
|
|
102
|
+
if (!payload)
|
|
103
|
+
return undefined;
|
|
104
|
+
const rawArgs = asRecord(payload.args) ?? {};
|
|
105
|
+
const args = normalizeWebToolArgs(webTool.kind, rawArgs);
|
|
106
|
+
const result = normalizeWebToolResult(payload.result);
|
|
107
|
+
if (!result)
|
|
108
|
+
return undefined;
|
|
109
|
+
return {
|
|
110
|
+
name: webTool.kind,
|
|
111
|
+
args,
|
|
112
|
+
result,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function normalizeWebToolArgs(kind, rawArgs) {
|
|
116
|
+
const args = { ...rawArgs };
|
|
117
|
+
if (kind === "webSearch") {
|
|
118
|
+
const query = getString(args, "searchTerm") ?? getString(args, "search_term") ?? getString(args, "query") ?? getString(args, "q");
|
|
119
|
+
if (query && !args.searchTerm)
|
|
120
|
+
args.searchTerm = query;
|
|
121
|
+
return args;
|
|
122
|
+
}
|
|
123
|
+
const url = getString(args, "url") ?? getString(args, "uri") ?? getString(args, "href");
|
|
124
|
+
if (url && !args.url)
|
|
125
|
+
args.url = url;
|
|
126
|
+
return args;
|
|
127
|
+
}
|
|
128
|
+
function normalizeWebToolResult(result) {
|
|
129
|
+
if (result === undefined)
|
|
130
|
+
return undefined;
|
|
131
|
+
const success = getTranscriptResultCase(result, "success");
|
|
132
|
+
if (success !== undefined) {
|
|
133
|
+
return {
|
|
134
|
+
status: "success",
|
|
135
|
+
value: { content: [{ type: "text", text: transcriptWebSuccessText(success) }] },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const error = getTranscriptResultCase(result, "error");
|
|
139
|
+
if (error !== undefined)
|
|
140
|
+
return { status: "error", error };
|
|
141
|
+
return {
|
|
142
|
+
status: "success",
|
|
143
|
+
value: { content: [{ type: "text", text: transcriptWebSuccessText(result) }] },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function getTranscriptResultCase(result, caseName) {
|
|
147
|
+
const record = asRecord(result);
|
|
148
|
+
return getOneofCaseValue(record?.result, caseName) ?? record?.[caseName];
|
|
149
|
+
}
|
|
150
|
+
function transcriptWebSuccessText(success) {
|
|
151
|
+
const successRecord = asRecord(success);
|
|
152
|
+
const references = getArray(successRecord, "references");
|
|
153
|
+
const chunks = references
|
|
154
|
+
?.map((reference) => getString(asRecord(reference), "chunk"))
|
|
155
|
+
.filter((chunk) => Boolean(chunk?.trim()));
|
|
156
|
+
if (chunks && chunks.length > 0)
|
|
157
|
+
return chunks.join("\n\n");
|
|
158
|
+
const content = getArray(successRecord, "content");
|
|
159
|
+
const text = content
|
|
160
|
+
?.map((entry) => getString(asRecord(entry), "text"))
|
|
161
|
+
.filter((entry) => Boolean(entry?.trim()));
|
|
162
|
+
if (text && text.length > 0)
|
|
163
|
+
return text.join("\n");
|
|
164
|
+
return stringifyUnknown(success).trim() || "Cursor web activity completed.";
|
|
165
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isCursorModel } from "./cursor-model.js";
|
|
2
|
+
import { registerCursorModelLifecycle } from "./cursor-model-lifecycle.js";
|
|
3
|
+
import { resolveEffectiveCursorConfigForContext } from "./cursor-runtime-state.js";
|
|
4
|
+
import { resolveCursorFacingSystemPrompt } from "./cursor-agents-context.js";
|
|
5
|
+
export function registerCursorAgentsContextDedup(pi) {
|
|
6
|
+
registerCursorModelLifecycle(pi, {
|
|
7
|
+
beforeAgentStart: (event, ctx) => {
|
|
8
|
+
if (!isCursorModel(ctx.model))
|
|
9
|
+
return undefined;
|
|
10
|
+
const runtime = resolveEffectiveCursorConfigForContext(ctx).runtime.value;
|
|
11
|
+
const resolved = resolveCursorFacingSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions, undefined, undefined, runtime);
|
|
12
|
+
if (resolved === event.systemPrompt)
|
|
13
|
+
return undefined;
|
|
14
|
+
return { systemPrompt: resolved };
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
3
|
+
import { isCursorModel } from "./cursor-model.js";
|
|
4
|
+
import { cursorSettingSourcesIncludes, getEffectiveCursorSettingSources, resolveCursorSettingSources, } from "./cursor-setting-sources.js";
|
|
5
|
+
export const CURSOR_PRESERVE_PI_AGENTS_MD_ENV = "PI_CURSOR_PRESERVE_PI_AGENTS_MD";
|
|
6
|
+
/** Opening tag prefix pi `buildSystemPrompt()` uses for each context file (path attribute only). */
|
|
7
|
+
export const PI_PROJECT_INSTRUCTIONS_OPEN_PREFIX = '<project_instructions path="';
|
|
8
|
+
const PI_PROJECT_INSTRUCTIONS_CLOSE = "</project_instructions>";
|
|
9
|
+
const PI_PROJECT_CONTEXT_OPEN = "\n\n<project_context>\n\nProject-specific instructions and guidelines:\n\n";
|
|
10
|
+
const PI_PROJECT_CONTEXT_CLOSE = "</project_context>\n";
|
|
11
|
+
function normalizeContextPath(filePath) {
|
|
12
|
+
return filePath.replace(/\\/g, "/");
|
|
13
|
+
}
|
|
14
|
+
function normalizeDirPath(dirPath) {
|
|
15
|
+
const normalized = normalizeContextPath(dirPath).replace(/\/+$/, "");
|
|
16
|
+
return normalized || "/";
|
|
17
|
+
}
|
|
18
|
+
/** Pi context filenames that can overlap Cursor project/user ambient rules. */
|
|
19
|
+
const CURSOR_OVERLAPPING_CONTEXT_BASE_NAMES = new Set(["agents.md", "claude.md"]);
|
|
20
|
+
export function getAgentsContextFileBaseName(filePath) {
|
|
21
|
+
const normalized = normalizeContextPath(filePath);
|
|
22
|
+
return normalized.slice(normalized.lastIndexOf("/") + 1).toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
function isPiAgentDirContextFilePath(filePath, fileName, agentDir = getAgentDir()) {
|
|
25
|
+
const normalized = normalizeContextPath(filePath);
|
|
26
|
+
const expectedPath = `${normalizeDirPath(agentDir)}/${fileName}`;
|
|
27
|
+
return normalized.toLowerCase() === expectedPath.toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
/** Actual pi agent dir `AGENTS.md` — overlaps Cursor `user` setting source (global agent instructions). */
|
|
30
|
+
export function isPiAgentDirAgentsMdPath(filePath, agentDir = getAgentDir()) {
|
|
31
|
+
return isPiAgentDirContextFilePath(filePath, "agents.md", agentDir);
|
|
32
|
+
}
|
|
33
|
+
/** Actual pi agent dir `CLAUDE.md` — kept because Cursor user rules use `~/.claude/CLAUDE.md`. */
|
|
34
|
+
export function isPiAgentDirClaudeMdPath(filePath, agentDir = getAgentDir()) {
|
|
35
|
+
return isPiAgentDirContextFilePath(filePath, "claude.md", agentDir);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Classify whether a pi-loaded context file overlaps Cursor ambient rules.
|
|
39
|
+
* Project/repo `AGENTS.md` and `CLAUDE.md` overlap Cursor `project` sources.
|
|
40
|
+
* Only the actual pi agent dir `AGENTS.md` overlaps Cursor `user`; agent-dir `CLAUDE.md` is kept
|
|
41
|
+
* because Cursor user rules use `~/.claude/CLAUDE.md`, not pi's agent dir path.
|
|
42
|
+
*/
|
|
43
|
+
export function classifyContextFileOverlap(filePath, agentDir = getAgentDir()) {
|
|
44
|
+
const base = getAgentsContextFileBaseName(filePath);
|
|
45
|
+
if (!CURSOR_OVERLAPPING_CONTEXT_BASE_NAMES.has(base))
|
|
46
|
+
return "none";
|
|
47
|
+
if (base === "agents.md" && isPiAgentDirAgentsMdPath(filePath, agentDir))
|
|
48
|
+
return "cursor-user-agents";
|
|
49
|
+
if (base === "claude.md" && isPiAgentDirClaudeMdPath(filePath, agentDir))
|
|
50
|
+
return "none";
|
|
51
|
+
return "cursor-project-rules";
|
|
52
|
+
}
|
|
53
|
+
export function shouldRemovePiAgentsContextFile(file, settingSources, agentDir) {
|
|
54
|
+
switch (classifyContextFileOverlap(file.path, agentDir)) {
|
|
55
|
+
case "cursor-user-agents":
|
|
56
|
+
return cursorSettingSourcesIncludes(settingSources, "user");
|
|
57
|
+
case "cursor-project-rules":
|
|
58
|
+
return cursorSettingSourcesIncludes(settingSources, "project");
|
|
59
|
+
default:
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function shouldSuppressPiAgentsContext(model, contextFiles, settingSources, agentDir) {
|
|
64
|
+
if (!isCursorModel(model))
|
|
65
|
+
return false;
|
|
66
|
+
if (parseEnvBoolean(process.env[CURSOR_PRESERVE_PI_AGENTS_MD_ENV], false))
|
|
67
|
+
return false;
|
|
68
|
+
if (contextFiles.length === 0)
|
|
69
|
+
return false;
|
|
70
|
+
return contextFiles.some((file) => shouldRemovePiAgentsContextFile(file, settingSources, agentDir));
|
|
71
|
+
}
|
|
72
|
+
/** Exact pi `buildSystemPrompt()` serialization for one context file block (including trailing blank line). */
|
|
73
|
+
export function serializePiProjectInstructionsBlock(file) {
|
|
74
|
+
return `${PI_PROJECT_INSTRUCTIONS_OPEN_PREFIX}${file.path}">\n${file.content}\n${PI_PROJECT_INSTRUCTIONS_CLOSE}\n\n`;
|
|
75
|
+
}
|
|
76
|
+
/** Exact pi `buildSystemPrompt()` serialization for the full project context section. */
|
|
77
|
+
export function serializePiProjectContextSection(contextFiles) {
|
|
78
|
+
if (contextFiles.length === 0)
|
|
79
|
+
return "";
|
|
80
|
+
return `${PI_PROJECT_CONTEXT_OPEN}${contextFiles.map(serializePiProjectInstructionsBlock).join("")}${PI_PROJECT_CONTEXT_CLOSE}`;
|
|
81
|
+
}
|
|
82
|
+
/** Remove pi context blocks that overlap Cursor setting sources. */
|
|
83
|
+
export function removePiAgentsContextFromSystemPrompt(systemPrompt, contextFiles, settingSources, agentDir) {
|
|
84
|
+
const retainedContextFiles = [];
|
|
85
|
+
let removedAny = false;
|
|
86
|
+
for (const file of contextFiles) {
|
|
87
|
+
if (shouldRemovePiAgentsContextFile(file, settingSources, agentDir)) {
|
|
88
|
+
removedAny = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
retainedContextFiles.push(file);
|
|
92
|
+
}
|
|
93
|
+
if (!removedAny)
|
|
94
|
+
return systemPrompt;
|
|
95
|
+
const originalSection = serializePiProjectContextSection(contextFiles);
|
|
96
|
+
const start = systemPrompt.indexOf(originalSection);
|
|
97
|
+
if (start < 0)
|
|
98
|
+
return systemPrompt;
|
|
99
|
+
const replacementSection = serializePiProjectContextSection(retainedContextFiles);
|
|
100
|
+
return systemPrompt.slice(0, start) + replacementSection + systemPrompt.slice(start + originalSection.length);
|
|
101
|
+
}
|
|
102
|
+
export function resolveCursorFacingSystemPrompt(systemPrompt, model, systemPromptOptions, settingSourcesRaw, agentDir, runtime = "local") {
|
|
103
|
+
if (runtime === "cloud" || !systemPromptOptions)
|
|
104
|
+
return systemPrompt;
|
|
105
|
+
const contextFiles = systemPromptOptions.contextFiles ?? [];
|
|
106
|
+
const settingSources = settingSourcesRaw === undefined
|
|
107
|
+
? getEffectiveCursorSettingSources()
|
|
108
|
+
: resolveCursorSettingSources(settingSourcesRaw);
|
|
109
|
+
if (!shouldSuppressPiAgentsContext(model, contextFiles, settingSources, agentDir)) {
|
|
110
|
+
return systemPrompt;
|
|
111
|
+
}
|
|
112
|
+
return removePiAgentsContextFromSystemPrompt(systemPrompt, contextFiles, settingSources, agentDir);
|
|
113
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export const CURSOR_API_KEY_ENV_VAR = "CURSOR_API_KEY";
|
|
3
|
+
const CURSOR_PROVIDER_ID = "cursor";
|
|
4
|
+
// Non-secret literal sentinel for pi's provider registry. Pi 0.77 treats `$ENV_VAR`
|
|
5
|
+
// values as unconfigured when the env var is absent, which hides fallback models
|
|
6
|
+
// before `/login`. Keep the provider available and resolve the real key in the
|
|
7
|
+
// Cursor provider turn path from pi auth or CURSOR_API_KEY.
|
|
8
|
+
export const CURSOR_API_KEY_CONFIG_VALUE = "pi-cursor-sdk-cursor-api-key-placeholder";
|
|
9
|
+
const CURSOR_API_KEY_PLACEHOLDERS = new Set([
|
|
10
|
+
CURSOR_API_KEY_ENV_VAR,
|
|
11
|
+
`$${CURSOR_API_KEY_ENV_VAR}`,
|
|
12
|
+
`\${${CURSOR_API_KEY_ENV_VAR}}`,
|
|
13
|
+
CURSOR_API_KEY_CONFIG_VALUE,
|
|
14
|
+
]);
|
|
15
|
+
export function resolveCursorApiKey(apiKey) {
|
|
16
|
+
const trimmed = apiKey?.trim();
|
|
17
|
+
if (!trimmed)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (CURSOR_API_KEY_PLACEHOLDERS.has(trimmed))
|
|
20
|
+
return process.env.CURSOR_API_KEY?.trim() || undefined;
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
function getStoredCursorApiKey() {
|
|
24
|
+
try {
|
|
25
|
+
const credential = readStoredCredential(CURSOR_PROVIDER_ID);
|
|
26
|
+
return resolveCursorApiKey(credential?.type === "api_key" ? credential.key : undefined);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function resolveCursorRuntimeApiKey() {
|
|
33
|
+
return Promise.resolve(getStoredCursorApiKey() ?? resolveCursorApiKey(process.env.CURSOR_API_KEY));
|
|
34
|
+
}
|