pi-cursor-sdk 0.1.8 → 0.1.10
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 +39 -0
- package/README.md +31 -12
- package/docs/cursor-model-ux-spec.md +33 -54
- package/package.json +4 -2
- package/scripts/refresh-cursor-model-snapshots.mjs +234 -0
- package/src/context-window-cache.ts +6 -0
- package/src/context.ts +128 -35
- package/src/cursor-fallback-models.generated.ts +145 -0
- package/src/cursor-native-tool-display.ts +156 -15
- package/src/cursor-provider.ts +137 -20
- package/src/cursor-state.ts +10 -1
- package/src/cursor-tool-transcript.ts +53 -11
- package/src/index.ts +35 -8
- package/src/model-discovery.ts +59 -154
package/src/cursor-provider.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type AssistantMessage,
|
|
9
9
|
} from "@earendil-works/pi-ai";
|
|
10
10
|
import { Agent, createAgentPlatform } from "@cursor/sdk";
|
|
11
|
-
import type { InteractionUpdate, SDKAgent } from "@cursor/sdk";
|
|
11
|
+
import type { InteractionUpdate, SDKAgent, SettingSource } from "@cursor/sdk";
|
|
12
12
|
import { buildCursorPrompt, type CursorPrompt } from "./context.js";
|
|
13
13
|
import { getEffectiveFastForModelId } from "./cursor-state.js";
|
|
14
14
|
import { buildCursorModelSelection } from "./model-discovery.js";
|
|
@@ -17,6 +17,7 @@ import { buildCursorPiToolDisplay, formatCursorToolTranscript, mergeCursorToolCa
|
|
|
17
17
|
import {
|
|
18
18
|
canRenderCursorToolNatively,
|
|
19
19
|
isCursorNativeToolDisplayRuntimeEnabled,
|
|
20
|
+
deleteCursorNativeToolDisplay,
|
|
20
21
|
recordCursorNativeToolDisplay,
|
|
21
22
|
type CursorNativeToolDisplayItem,
|
|
22
23
|
} from "./cursor-native-tool-display.js";
|
|
@@ -58,7 +59,9 @@ const AUTH_CURSOR_SDK_ERROR_MESSAGE =
|
|
|
58
59
|
const APPROX_CHARS_PER_TOKEN = 4;
|
|
59
60
|
const IMAGE_TOKEN_ESTIMATE = 1200;
|
|
60
61
|
const CURSOR_ACTIVITY_TRACE_MAX_CHARS = 50000;
|
|
62
|
+
const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
|
|
61
63
|
const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
|
|
64
|
+
const CURSOR_SETTING_SOURCES_ENV = "PI_CURSOR_SETTING_SOURCES";
|
|
62
65
|
|
|
63
66
|
type CursorNativeQueuedEvent =
|
|
64
67
|
| { type: "thinking-delta"; text: string }
|
|
@@ -73,10 +76,13 @@ interface CursorNativeLiveRun {
|
|
|
73
76
|
promptInputTokensReported: boolean;
|
|
74
77
|
pendingEvents: CursorNativeQueuedEvent[];
|
|
75
78
|
textDeltas: string[];
|
|
79
|
+
recordedToolDisplayIds: string[];
|
|
76
80
|
finalText?: string;
|
|
77
81
|
done: boolean;
|
|
78
82
|
cancelled: boolean;
|
|
83
|
+
disposed: boolean;
|
|
79
84
|
errorMessage?: string;
|
|
85
|
+
idleDisposeTimer?: ReturnType<typeof setTimeout>;
|
|
80
86
|
waiters: Set<() => void>;
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -88,6 +94,7 @@ interface CursorNativeTurnState {
|
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
let cursorNativeReplayCounter = 0;
|
|
97
|
+
let cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
|
|
91
98
|
const pendingCursorNativeRuns = new Map<string, CursorNativeLiveRun>();
|
|
92
99
|
|
|
93
100
|
function escapeRegExp(value: string): string {
|
|
@@ -125,6 +132,18 @@ function resolveCursorApiKey(apiKey?: string): string | undefined {
|
|
|
125
132
|
return trimmed;
|
|
126
133
|
}
|
|
127
134
|
|
|
135
|
+
function resolveCursorSettingSources(): SettingSource[] | undefined {
|
|
136
|
+
const raw = process.env[CURSOR_SETTING_SOURCES_ENV]?.trim();
|
|
137
|
+
if (!raw) return undefined;
|
|
138
|
+
const normalized = raw.toLowerCase();
|
|
139
|
+
if (["0", "false", "off", "none", "omit", "disabled"].includes(normalized)) return undefined;
|
|
140
|
+
if (["1", "true", "on", "all"].includes(normalized)) return ["all"];
|
|
141
|
+
return raw
|
|
142
|
+
.split(",")
|
|
143
|
+
.map((entry) => entry.trim())
|
|
144
|
+
.filter((entry): entry is SettingSource => Boolean(entry));
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
function sanitizeError(error: unknown, apiKey?: string): string {
|
|
129
148
|
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
130
149
|
if (message === MISSING_API_KEY_MESSAGE) return MISSING_API_KEY_MESSAGE;
|
|
@@ -166,6 +185,11 @@ function estimatePromptInputTokens(prompt: CursorPrompt): number {
|
|
|
166
185
|
return estimateTextTokens(prompt.text) + prompt.images.length * IMAGE_TOKEN_ESTIMATE;
|
|
167
186
|
}
|
|
168
187
|
|
|
188
|
+
function getPromptInputTokenBudget(model: Model<Api>): number {
|
|
189
|
+
const outputReserveTokens = Math.min(model.maxTokens, Math.max(1, Math.floor(model.contextWindow * 0.2)));
|
|
190
|
+
return Math.max(1, model.contextWindow - outputReserveTokens);
|
|
191
|
+
}
|
|
192
|
+
|
|
169
193
|
function setApproximateUsage(partial: AssistantMessage, promptInputTokens: number, outputText: string): void {
|
|
170
194
|
partial.usage.input = promptInputTokens;
|
|
171
195
|
partial.usage.output = estimateTextTokens(outputText);
|
|
@@ -264,6 +288,21 @@ function queueCursorNativeEvent(run: CursorNativeLiveRun, event: CursorNativeQue
|
|
|
264
288
|
notifyCursorNativeRun(run);
|
|
265
289
|
}
|
|
266
290
|
|
|
291
|
+
function clearCursorNativeRunIdleDispose(run: CursorNativeLiveRun): void {
|
|
292
|
+
if (!run.idleDisposeTimer) return;
|
|
293
|
+
clearTimeout(run.idleDisposeTimer);
|
|
294
|
+
run.idleDisposeTimer = undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function scheduleCursorNativeRunIdleDispose(run: CursorNativeLiveRun): void {
|
|
298
|
+
if (run.disposed) return;
|
|
299
|
+
clearCursorNativeRunIdleDispose(run);
|
|
300
|
+
run.idleDisposeTimer = setTimeout(() => {
|
|
301
|
+
void disposeCursorNativeRun(run);
|
|
302
|
+
}, cursorNativeReplayIdleDisposeMs);
|
|
303
|
+
run.idleDisposeTimer.unref?.();
|
|
304
|
+
}
|
|
305
|
+
|
|
267
306
|
function isCursorNativeRunReady(run: CursorNativeLiveRun): boolean {
|
|
268
307
|
return run.pendingEvents.length > 0 || run.done || run.cancelled || run.errorMessage !== undefined;
|
|
269
308
|
}
|
|
@@ -310,12 +349,13 @@ function closeCursorNativeThinkingBlock(turn: CursorNativeTurnState): void {
|
|
|
310
349
|
|
|
311
350
|
function closeCursorNativeTextBlock(turn: CursorNativeTurnState): string {
|
|
312
351
|
if (turn.textContentIndex < 0) return "";
|
|
313
|
-
const
|
|
352
|
+
const contentIndex = turn.textContentIndex;
|
|
353
|
+
const block = turn.partial.content[contentIndex];
|
|
314
354
|
turn.textContentIndex = -1;
|
|
315
355
|
if (block.type !== "text") return "";
|
|
316
356
|
turn.stream.push({
|
|
317
357
|
type: "text_end",
|
|
318
|
-
contentIndex
|
|
358
|
+
contentIndex,
|
|
319
359
|
content: block.text,
|
|
320
360
|
partial: turn.partial,
|
|
321
361
|
});
|
|
@@ -402,15 +442,24 @@ function emitCursorNativeToolUseTurn(
|
|
|
402
442
|
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(tool.args), partial });
|
|
403
443
|
const block = partial.content[contentIndex];
|
|
404
444
|
if (block.type === "toolCall") stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial });
|
|
405
|
-
recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate })
|
|
445
|
+
if (recordCursorNativeToolDisplay({ ...tool, terminate: shouldTerminate })) {
|
|
446
|
+
run.recordedToolDisplayIds.push(tool.id);
|
|
447
|
+
}
|
|
406
448
|
}
|
|
407
449
|
setApproximateUsage(partial, takeCursorNativePromptInputTokens(run), outputText);
|
|
408
450
|
partial.stopReason = "toolUse";
|
|
409
451
|
stream.push({ type: "done", reason: "toolUse", message: partial });
|
|
452
|
+
scheduleCursorNativeRunIdleDispose(run);
|
|
410
453
|
}
|
|
411
454
|
|
|
412
455
|
async function disposeCursorNativeRun(run: CursorNativeLiveRun): Promise<void> {
|
|
456
|
+
if (run.disposed) return;
|
|
457
|
+
run.disposed = true;
|
|
413
458
|
pendingCursorNativeRuns.delete(run.id);
|
|
459
|
+
clearCursorNativeRunIdleDispose(run);
|
|
460
|
+
for (const toolDisplayId of run.recordedToolDisplayIds) deleteCursorNativeToolDisplay(toolDisplayId);
|
|
461
|
+
run.recordedToolDisplayIds = [];
|
|
462
|
+
run.waiters.clear();
|
|
414
463
|
try {
|
|
415
464
|
await run.agent[Symbol.asyncDispose]();
|
|
416
465
|
} catch {
|
|
@@ -484,7 +533,13 @@ async function replayPendingCursorNativeRun(
|
|
|
484
533
|
if (!replayId) return false;
|
|
485
534
|
const run = pendingCursorNativeRuns.get(replayId);
|
|
486
535
|
if (!run) return false;
|
|
487
|
-
|
|
536
|
+
clearCursorNativeRunIdleDispose(run);
|
|
537
|
+
try {
|
|
538
|
+
await emitCursorNativeRunNextTurn(stream, partial, run, signal);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (error instanceof CursorAbortError) await disposeCursorNativeRun(run);
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
488
543
|
return true;
|
|
489
544
|
}
|
|
490
545
|
|
|
@@ -498,6 +553,7 @@ export function streamCursor(
|
|
|
498
553
|
(async () => {
|
|
499
554
|
const partial = makeInitialMessage(model);
|
|
500
555
|
let agent: SDKAgent | null = null;
|
|
556
|
+
let activeNativeRun: CursorNativeLiveRun | undefined;
|
|
501
557
|
let resolvedApiKey: string | undefined;
|
|
502
558
|
let abortSignal: AbortSignal | undefined;
|
|
503
559
|
let abortListener: (() => void) | undefined;
|
|
@@ -519,20 +575,25 @@ export function streamCursor(
|
|
|
519
575
|
if (!apiKey) throw new Error(MISSING_API_KEY_MESSAGE);
|
|
520
576
|
resolvedApiKey = apiKey;
|
|
521
577
|
|
|
578
|
+
// pi-ai Context/SimpleStreamOptions do not currently expose ExtensionContext.cwd;
|
|
579
|
+
// provider calls use the process cwd until pi exposes a session cwd to streamSimple.
|
|
522
580
|
const cwd = process.cwd();
|
|
523
581
|
const fastEnabled = getEffectiveFastForModelId(model.id);
|
|
524
582
|
const selection = buildCursorModelSelection(model.id, options?.reasoning ?? "off", fastEnabled);
|
|
583
|
+
const settingSources = resolveCursorSettingSources();
|
|
525
584
|
|
|
526
585
|
agent = await Agent.create({
|
|
527
586
|
apiKey,
|
|
528
587
|
model: selection,
|
|
529
|
-
|
|
530
|
-
// setting/rule loading INFO logs directly to process output, which corrupts pi's TUI.
|
|
531
|
-
local: { cwd },
|
|
588
|
+
local: settingSources ? { cwd, settingSources } : { cwd },
|
|
532
589
|
});
|
|
533
590
|
throwIfAborted();
|
|
534
591
|
|
|
535
|
-
const prompt = buildCursorPrompt(context
|
|
592
|
+
const prompt = buildCursorPrompt(context, {
|
|
593
|
+
maxInputTokens: getPromptInputTokenBudget(model),
|
|
594
|
+
charsPerToken: APPROX_CHARS_PER_TOKEN,
|
|
595
|
+
imageTokenEstimate: IMAGE_TOKEN_ESTIMATE,
|
|
596
|
+
});
|
|
536
597
|
const promptInputTokens = estimatePromptInputTokens(prompt);
|
|
537
598
|
let thinkingContentIndex = -1;
|
|
538
599
|
let activityTraceChars = 0;
|
|
@@ -551,14 +612,21 @@ export function streamCursor(
|
|
|
551
612
|
promptInputTokensReported: false,
|
|
552
613
|
pendingEvents: [],
|
|
553
614
|
textDeltas,
|
|
615
|
+
recordedToolDisplayIds: [],
|
|
554
616
|
done: false,
|
|
555
617
|
cancelled: false,
|
|
618
|
+
disposed: false,
|
|
556
619
|
waiters: new Set(),
|
|
557
620
|
}
|
|
558
621
|
: undefined;
|
|
559
|
-
if (liveRun)
|
|
622
|
+
if (liveRun) {
|
|
623
|
+
pendingCursorNativeRuns.set(liveRun.id, liveRun);
|
|
624
|
+
activeNativeRun = liveRun;
|
|
625
|
+
}
|
|
560
626
|
const startedToolCalls = new Map<string, unknown>();
|
|
561
|
-
const
|
|
627
|
+
const completedToolIdentities = new Set<string>();
|
|
628
|
+
const completedStartedToolFingerprints = new Set<string>();
|
|
629
|
+
const completedFallbackToolFingerprints = new Set<string>();
|
|
562
630
|
|
|
563
631
|
const appendLiveTextDelta = (text: string): void => {
|
|
564
632
|
if (textContentIndex < 0) {
|
|
@@ -652,14 +720,31 @@ export function streamCursor(
|
|
|
652
720
|
}
|
|
653
721
|
};
|
|
654
722
|
|
|
655
|
-
const handleCompletedToolCall = (
|
|
723
|
+
const handleCompletedToolCall = (
|
|
724
|
+
toolCall: unknown,
|
|
725
|
+
options: { identity?: string; source?: "started" | "fallback" } = {},
|
|
726
|
+
): void => {
|
|
656
727
|
const transcript = scrubSensitiveText(formatCursorToolTranscript(toolCall, { cwd }), resolvedApiKey);
|
|
657
728
|
const display = buildCursorPiToolDisplay(toolCall, { cwd });
|
|
658
729
|
const fingerprint = getToolFingerprint({ toolName: display.toolName, args: display.args, result: display.result });
|
|
659
|
-
if (
|
|
660
|
-
|
|
730
|
+
if (options.identity && completedToolIdentities.has(options.identity)) return;
|
|
731
|
+
if (options.source === "started") {
|
|
732
|
+
if (completedFallbackToolFingerprints.has(fingerprint)) return;
|
|
733
|
+
} else if (completedStartedToolFingerprints.has(fingerprint) || completedFallbackToolFingerprints.has(fingerprint)) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (options.identity) completedToolIdentities.add(options.identity);
|
|
737
|
+
if (options.source === "started") {
|
|
738
|
+
completedStartedToolFingerprints.add(fingerprint);
|
|
739
|
+
} else {
|
|
740
|
+
completedFallbackToolFingerprints.add(fingerprint);
|
|
741
|
+
}
|
|
661
742
|
|
|
662
743
|
if (useNativeToolReplay && canRenderCursorToolNatively(display.toolName) && liveRun) {
|
|
744
|
+
if (!nativeToolReplayStarted && textDeltas.length > 0) {
|
|
745
|
+
for (const text of textDeltas) queueCursorNativeEvent(liveRun, { type: "text-delta", text });
|
|
746
|
+
textDeltas.length = 0;
|
|
747
|
+
}
|
|
663
748
|
nativeToolReplayStarted = true;
|
|
664
749
|
const id = `${nativeReplayId}-tool-${++nativeToolDisplayCounter}`;
|
|
665
750
|
queueCursorNativeEvent(liveRun, {
|
|
@@ -704,7 +789,11 @@ export function streamCursor(
|
|
|
704
789
|
} else if (update.type === "tool-call-completed") {
|
|
705
790
|
const mergedToolCall = mergeCursorToolCalls(startedToolCalls.get(update.callId), update.toolCall);
|
|
706
791
|
startedToolCalls.delete(update.callId);
|
|
707
|
-
|
|
792
|
+
const identity = typeof update.callId === "string" ? `cursor-tool:${update.callId}` : undefined;
|
|
793
|
+
handleCompletedToolCall(mergedToolCall, {
|
|
794
|
+
identity,
|
|
795
|
+
source: identity ? "started" : "fallback",
|
|
796
|
+
});
|
|
708
797
|
} else if (update.type === "summary") {
|
|
709
798
|
const summary = `Cursor summary: ${truncateSingleLine(update.summary)}\n`;
|
|
710
799
|
if (liveRun && nativeToolReplayStarted) {
|
|
@@ -723,7 +812,12 @@ export function streamCursor(
|
|
|
723
812
|
const step = getObjectField(args.step, "message") ? args.step : undefined;
|
|
724
813
|
if (getObjectField(args.step, "type") !== "toolCall") return;
|
|
725
814
|
const toolCall = getObjectField(step, "message");
|
|
726
|
-
|
|
815
|
+
const stepId = getObjectField(args.step, "id") ?? getObjectField(toolCall, "id") ?? getObjectField(toolCall, "callId");
|
|
816
|
+
if (toolCall) {
|
|
817
|
+
handleCompletedToolCall(toolCall, {
|
|
818
|
+
identity: typeof stepId === "string" ? `cursor-tool:${stepId}` : undefined,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
727
821
|
};
|
|
728
822
|
|
|
729
823
|
// Handle abort signal
|
|
@@ -750,21 +844,31 @@ export function streamCursor(
|
|
|
750
844
|
void run
|
|
751
845
|
.wait()
|
|
752
846
|
.then(async (result) => {
|
|
847
|
+
if (liveRun.disposed) return;
|
|
753
848
|
await cacheSdkContextWindow(liveRun.agent.agentId, model.id);
|
|
849
|
+
if (liveRun.disposed) return;
|
|
754
850
|
liveRun.cancelled = result.status === "cancelled";
|
|
755
851
|
liveRun.finalText = hasUsableText(result.result) ? result.result : liveRun.textDeltas.join("");
|
|
756
852
|
liveRun.done = true;
|
|
757
853
|
notifyCursorNativeRun(liveRun);
|
|
854
|
+
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
758
855
|
})
|
|
759
856
|
.catch((error: unknown) => {
|
|
857
|
+
if (liveRun.disposed) return;
|
|
760
858
|
liveRun.errorMessage = sanitizeError(error, resolvedApiKey ?? options?.apiKey);
|
|
761
859
|
notifyCursorNativeRun(liveRun);
|
|
860
|
+
scheduleCursorNativeRunIdleDispose(liveRun);
|
|
762
861
|
});
|
|
763
862
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
863
|
+
try {
|
|
864
|
+
await waitForCursorNativeRunProgress(liveRun, options?.signal);
|
|
865
|
+
await settleCursorNativeToolBatch(liveRun);
|
|
866
|
+
closeTraceBlock();
|
|
867
|
+
await emitCursorNativeRunNextTurn(stream, partial, liveRun, options?.signal);
|
|
868
|
+
} catch (error) {
|
|
869
|
+
if (error instanceof CursorAbortError) await disposeCursorNativeRun(liveRun);
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
768
872
|
agent = null;
|
|
769
873
|
return;
|
|
770
874
|
}
|
|
@@ -794,6 +898,8 @@ export function streamCursor(
|
|
|
794
898
|
stream.push({ type: "error", reason: "error", error: partial });
|
|
795
899
|
}
|
|
796
900
|
} finally {
|
|
901
|
+
if (activeNativeRun?.disposed) agent = null;
|
|
902
|
+
|
|
797
903
|
if (abortSignal && abortListener) {
|
|
798
904
|
abortSignal.removeEventListener("abort", abortListener);
|
|
799
905
|
}
|
|
@@ -813,3 +919,14 @@ export function streamCursor(
|
|
|
813
919
|
|
|
814
920
|
return stream;
|
|
815
921
|
}
|
|
922
|
+
|
|
923
|
+
export const __testUtils = {
|
|
924
|
+
DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS,
|
|
925
|
+
pendingCursorNativeRunCount: () => pendingCursorNativeRuns.size,
|
|
926
|
+
setCursorNativeReplayIdleDisposeMs: (value: number) => {
|
|
927
|
+
cursorNativeReplayIdleDisposeMs = value;
|
|
928
|
+
},
|
|
929
|
+
resetCursorNativeReplayIdleDisposeMs: () => {
|
|
930
|
+
cursorNativeReplayIdleDisposeMs = DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS;
|
|
931
|
+
},
|
|
932
|
+
};
|
package/src/cursor-state.ts
CHANGED
|
@@ -105,16 +105,25 @@ function restoreMapValue(map: Map<string, boolean>, key: string, previous: boole
|
|
|
105
105
|
function persistFastPreference(pi: ExtensionAPI, baseModelId: string, fast: boolean): void {
|
|
106
106
|
const previousSession = sessionFastPreferences.get(baseModelId);
|
|
107
107
|
const previousGlobal = globalFastPreferences.get(baseModelId);
|
|
108
|
+
let savedGlobal = false;
|
|
108
109
|
sessionFastPreferences.set(baseModelId, fast);
|
|
109
110
|
globalFastPreferences.set(baseModelId, fast);
|
|
110
111
|
try {
|
|
111
112
|
saveGlobalFastPreferences();
|
|
113
|
+
savedGlobal = true;
|
|
114
|
+
pi.appendEntry<CursorFastEntryData>(FAST_ENTRY_TYPE, { baseModelId, fast });
|
|
112
115
|
} catch (error) {
|
|
113
116
|
restoreMapValue(sessionFastPreferences, baseModelId, previousSession);
|
|
114
117
|
restoreMapValue(globalFastPreferences, baseModelId, previousGlobal);
|
|
118
|
+
if (savedGlobal) {
|
|
119
|
+
try {
|
|
120
|
+
saveGlobalFastPreferences();
|
|
121
|
+
} catch {
|
|
122
|
+
// Preserve the original append failure reported to the user.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
115
125
|
throw error;
|
|
116
126
|
}
|
|
117
|
-
pi.appendEntry<CursorFastEntryData>(FAST_ENTRY_TYPE, { baseModelId, fast });
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
export function getEffectiveFastForModelId(modelId: string): boolean | undefined {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
1
|
+
import { closeSync, openSync, readSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
|
|
4
4
|
const DEFAULT_MAX_TRANSCRIPT_CHARS = 24000;
|
|
@@ -6,6 +6,8 @@ const DEFAULT_MAX_TRANSCRIPT_LINES = 800;
|
|
|
6
6
|
const DEFAULT_MAX_LIST_ITEMS = 200;
|
|
7
7
|
const DEFAULT_READ_TRANSCRIPT_CHARS = 4000;
|
|
8
8
|
const DEFAULT_READ_TRANSCRIPT_LINES = 12;
|
|
9
|
+
const LOCAL_READ_PREVIEW_NOTICE =
|
|
10
|
+
"[local file preview at transcript time; Cursor read result content was unavailable]";
|
|
9
11
|
|
|
10
12
|
interface TranscriptOptions {
|
|
11
13
|
maxChars?: number;
|
|
@@ -194,15 +196,18 @@ function isSensitivePreviewPath(filePath: string): boolean {
|
|
|
194
196
|
function readFilePreview(path: string, options: TranscriptOptions): string | undefined {
|
|
195
197
|
const cwd = options.cwd ?? process.cwd();
|
|
196
198
|
const filePath = resolveFilePath(path, cwd);
|
|
197
|
-
if (!isPathWithinCwd(filePath, cwd) || isSensitivePreviewPath(filePath)) return undefined;
|
|
198
199
|
|
|
199
200
|
const maxChars = options.maxChars ?? DEFAULT_READ_TRANSCRIPT_CHARS;
|
|
200
201
|
const maxBytes = Math.max(8192, maxChars * 4);
|
|
201
202
|
let fd: number | undefined;
|
|
202
203
|
try {
|
|
203
|
-
const
|
|
204
|
+
const realCwd = realpathSync(cwd);
|
|
205
|
+
const realFilePath = realpathSync(filePath);
|
|
206
|
+
if (!isPathWithinCwd(realFilePath, realCwd) || isSensitivePreviewPath(filePath) || isSensitivePreviewPath(realFilePath)) return undefined;
|
|
207
|
+
|
|
208
|
+
const stat = statSync(realFilePath);
|
|
204
209
|
if (!stat.isFile()) return undefined;
|
|
205
|
-
fd = openSync(
|
|
210
|
+
fd = openSync(realFilePath, "r");
|
|
206
211
|
const buffer = Buffer.alloc(Math.min(stat.size, maxBytes));
|
|
207
212
|
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
208
213
|
const text = buffer.toString("utf8", 0, bytesRead);
|
|
@@ -229,7 +234,10 @@ function getReadContent(args: Record<string, unknown>, result: NormalizedResult,
|
|
|
229
234
|
};
|
|
230
235
|
const value = asRecord(result.value);
|
|
231
236
|
const resultContent = getString(value, "content");
|
|
232
|
-
|
|
237
|
+
if (resultContent && resultContent.length > 0) return resultContent;
|
|
238
|
+
if (!rawPath) return stringifyUnknown(result.value);
|
|
239
|
+
const localPreview = readFilePreview(rawPath, readOptions);
|
|
240
|
+
return localPreview ? `${LOCAL_READ_PREVIEW_NOTICE}\n${localPreview}` : stringifyUnknown(result.value);
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
function formatRead(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
@@ -281,15 +289,18 @@ function renderTreeNode(node: unknown, depth = 0, lines: string[] = []): string[
|
|
|
281
289
|
return lines;
|
|
282
290
|
}
|
|
283
291
|
|
|
284
|
-
function
|
|
285
|
-
const path = formatPathArg(args, options) ?? ".";
|
|
286
|
-
if (result.status === "error") return joinSections(`ls ${path}`, formatError(result.error));
|
|
287
|
-
|
|
292
|
+
function getLsBody(result: NormalizedResult, options: TranscriptOptions): string {
|
|
288
293
|
const value = asRecord(result.value);
|
|
289
294
|
const root = value?.directoryTreeRoot ?? result.value;
|
|
290
295
|
const treeLines = renderTreeNode(root);
|
|
291
296
|
const body = treeLines.length > 0 ? treeLines.join("\n") : stringifyUnknown(result.value);
|
|
292
|
-
return
|
|
297
|
+
return limitText(body, options);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function formatLs(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
301
|
+
const path = formatPathArg(args, options) ?? ".";
|
|
302
|
+
if (result.status === "error") return joinSections(`ls ${path}`, formatError(result.error));
|
|
303
|
+
return joinSections(`ls ${path}`, getLsBody(result, options));
|
|
293
304
|
}
|
|
294
305
|
|
|
295
306
|
function formatGlob(args: Record<string, unknown>, result: NormalizedResult, options: TranscriptOptions): string {
|
|
@@ -528,7 +539,38 @@ export function buildCursorPiToolDisplay(toolCall: unknown, options: TranscriptO
|
|
|
528
539
|
return {
|
|
529
540
|
toolName: "ls",
|
|
530
541
|
args,
|
|
531
|
-
result: textToolResult(result.status === "error" ? formatError(result.error) :
|
|
542
|
+
result: textToolResult(result.status === "error" ? formatError(result.error) : getLsBody(result, options).trim()),
|
|
543
|
+
isError: result.status === "error",
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (name === "edit") {
|
|
548
|
+
const value = asRecord(result.value);
|
|
549
|
+
return {
|
|
550
|
+
toolName: "cursor_edit",
|
|
551
|
+
args,
|
|
552
|
+
result: textToolResult(formatEdit(args, result, options), {
|
|
553
|
+
cursorToolName: "edit",
|
|
554
|
+
path: typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined,
|
|
555
|
+
linesAdded: getNumber(value, "linesAdded"),
|
|
556
|
+
linesRemoved: getNumber(value, "linesRemoved"),
|
|
557
|
+
diffString: getString(value, "diffString"),
|
|
558
|
+
}),
|
|
559
|
+
isError: result.status === "error",
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (name === "write") {
|
|
564
|
+
const value = asRecord(result.value);
|
|
565
|
+
return {
|
|
566
|
+
toolName: "cursor_write",
|
|
567
|
+
args,
|
|
568
|
+
result: textToolResult(formatWrite(args, result, options), {
|
|
569
|
+
cursorToolName: "write",
|
|
570
|
+
path: typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined,
|
|
571
|
+
linesCreated: getNumber(value, "linesCreated"),
|
|
572
|
+
fileSize: getNumber(value, "fileSize"),
|
|
573
|
+
}),
|
|
532
574
|
isError: result.status === "error",
|
|
533
575
|
};
|
|
534
576
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ProviderConfig, ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { discoverModels, type CursorModelFallbackIssue } from "./model-discovery.js";
|
|
3
3
|
import { registerCursorFastControls } from "./cursor-state.js";
|
|
4
4
|
import { registerCursorNativeToolDisplay } from "./cursor-native-tool-display.js";
|
|
5
5
|
import { streamCursor } from "./cursor-provider.js";
|
|
6
6
|
|
|
7
|
+
function createCursorProviderConfig(models: ProviderModelConfig[]): ProviderConfig {
|
|
8
|
+
return {
|
|
9
|
+
name: "Cursor",
|
|
10
|
+
baseUrl: "https://cursor.com",
|
|
11
|
+
apiKey: "CURSOR_API_KEY",
|
|
12
|
+
api: "cursor-sdk",
|
|
13
|
+
models,
|
|
14
|
+
streamSimple: streamCursor,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function registerCursorProvider(pi: ExtensionAPI, models: ProviderModelConfig[]): void {
|
|
19
|
+
pi.registerProvider("cursor", createCursorProviderConfig(models));
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
export default async function (pi: ExtensionAPI) {
|
|
8
23
|
registerCursorFastControls(pi);
|
|
9
24
|
registerCursorNativeToolDisplay(pi);
|
|
@@ -21,12 +36,24 @@ export default async function (pi: ExtensionAPI) {
|
|
|
21
36
|
});
|
|
22
37
|
}
|
|
23
38
|
|
|
24
|
-
pi.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
pi.registerCommand("cursor-refresh-models", {
|
|
40
|
+
description: "Refresh the live Cursor model catalog without restarting pi",
|
|
41
|
+
handler: async (_args, ctx) => {
|
|
42
|
+
let refreshFallbackIssue: CursorModelFallbackIssue | undefined;
|
|
43
|
+
const refreshedModels = await discoverModels({
|
|
44
|
+
onFallback: (issue) => {
|
|
45
|
+
refreshFallbackIssue = issue;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
registerCursorProvider(pi, refreshedModels);
|
|
49
|
+
if (!ctx.hasUI) return;
|
|
50
|
+
if (refreshFallbackIssue) {
|
|
51
|
+
ctx.ui.notify(`Cursor model catalog refresh still using fallback models: ${refreshFallbackIssue.message}`, "warning");
|
|
52
|
+
} else {
|
|
53
|
+
ctx.ui.notify(`Cursor model catalog refreshed with ${refreshedModels.length} model${refreshedModels.length === 1 ? "" : "s"}.`, "info");
|
|
54
|
+
}
|
|
55
|
+
},
|
|
31
56
|
});
|
|
57
|
+
|
|
58
|
+
registerCursorProvider(pi, models);
|
|
32
59
|
}
|