pi-cursor-sdk 0.3.6 → 0.3.8
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 +25 -0
- package/README.md +16 -5
- package/dist/context.js +19 -4
- package/dist/cursor-agents-context.js +22 -8
- package/dist/cursor-context-tools.js +3 -1
- package/dist/cursor-live-run-coordinator.js +9 -6
- package/dist/cursor-partial-content-emitter.js +12 -0
- package/dist/cursor-pi-context.js +30 -0
- package/dist/cursor-provider-live-run-drain.js +19 -6
- package/dist/cursor-provider-turn-coordinator.js +18 -0
- package/dist/cursor-provider-turn-emit.js +6 -0
- package/dist/cursor-provider-turn-prepare.js +10 -5
- package/dist/cursor-record-utils.js +19 -0
- package/dist/cursor-skill-tool.js +1 -1
- package/docs/cursor-dogfood-checklist.md +1 -1
- package/docs/cursor-live-smoke-checklist.md +1 -1
- package/docs/cursor-model-ux-spec.md +2 -0
- package/docs/cursor-testing-lessons.md +25 -1
- package/docs/platform-smoke-implementation.md +2 -0
- package/docs/platform-smoke.md +1 -1
- package/package.json +2 -2
- package/scripts/build.mjs +2 -1
- package/scripts/lib/local-resume-smoke-harness.mjs +1 -6
- package/scripts/local-resume-smoke.mjs +43 -4
- package/scripts/platform-smoke/card-detect.mjs +2 -4
- package/scripts/platform-smoke/live-suite-runner.mjs +5 -7
- package/scripts/platform-smoke/pty-capture.mjs +3 -10
- package/scripts/platform-smoke/render-ansi.mjs +2 -5
- package/src/context.ts +27 -4
- package/src/cursor-agents-context.ts +24 -8
- package/src/cursor-context-tools.ts +3 -1
- package/src/cursor-live-run-coordinator.ts +10 -6
- package/src/cursor-partial-content-emitter.ts +13 -0
- package/src/cursor-pi-context.ts +44 -0
- package/src/cursor-provider-live-run-drain.ts +16 -9
- package/src/cursor-provider-turn-coordinator.ts +15 -0
- package/src/cursor-provider-turn-emit.ts +6 -0
- package/src/cursor-provider-turn-prepare.ts +10 -5
- package/src/cursor-record-utils.ts +19 -0
- package/src/cursor-skill-tool.ts +1 -1
|
@@ -21,13 +21,14 @@ import {
|
|
|
21
21
|
import { type CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge.js";
|
|
22
22
|
import { resetSessionCursorAgent } from "./cursor-session-agent.js";
|
|
23
23
|
import { applyCursorUsage } from "./cursor-usage-accounting.js";
|
|
24
|
-
import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
24
|
+
import { CURSOR_TEXT_MESSAGE_SEPARATOR, CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
25
25
|
import { emitDisplayOnlyTraceBlock } from "./cursor-display-only-trace.js";
|
|
26
26
|
import { trimCurrentTurnAlreadyEmittedCursorText } from "./cursor-run-final-text.js";
|
|
27
27
|
import { formatCursorSdkAbortMessage, resolveCursorSdkAbortCause } from "./cursor-provider-errors.js";
|
|
28
28
|
import { formatInactiveCursorReplayTrace } from "./cursor-native-replay-trace.js";
|
|
29
29
|
import { partitionNativeToolsByActiveContext } from "./cursor-native-replay-routing.js";
|
|
30
30
|
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
31
|
+
import { isJsonObject } from "./cursor-record-utils.js";
|
|
31
32
|
|
|
32
33
|
export const DEFAULT_CURSOR_NATIVE_REPLAY_IDLE_DISPOSE_MS = 5 * 60 * 1000;
|
|
33
34
|
const CURSOR_NATIVE_REPLAY_TOOL_ID_PATTERN = /^(cursor-replay-\d+-\d+)-tool-\d+$/;
|
|
@@ -88,11 +89,9 @@ function splitTextIntoReplayDeltas(text: string): string[] {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
async function emitTextDeltas(
|
|
91
|
-
|
|
92
|
-
partial: AssistantMessage,
|
|
92
|
+
emitter: CursorPartialContentEmitter,
|
|
93
93
|
deltas: string[],
|
|
94
94
|
): Promise<string> {
|
|
95
|
-
const emitter = new CursorPartialContentEmitter(stream, partial, -1, true);
|
|
96
95
|
for (const delta of deltas) {
|
|
97
96
|
emitter.appendTextDelta(delta);
|
|
98
97
|
await Promise.resolve();
|
|
@@ -121,11 +120,11 @@ export function flushPendingCursorLiveRunTraceEventsToStream(
|
|
|
121
120
|
stream: AssistantMessageEventStream,
|
|
122
121
|
partial: AssistantMessage,
|
|
123
122
|
run: CursorLiveRun,
|
|
124
|
-
options?: { includeTracesBehindQueuedTools?: boolean },
|
|
123
|
+
options?: { includeTracesBehindQueuedTools?: boolean; emitter?: CursorPartialContentEmitter },
|
|
125
124
|
): void {
|
|
126
125
|
if (run.disposed) return;
|
|
127
126
|
const turn: CursorLiveTurnState = {
|
|
128
|
-
emitter: new CursorPartialContentEmitter(stream, partial, -1, true),
|
|
127
|
+
emitter: options?.emitter ?? new CursorPartialContentEmitter(stream, partial, -1, true),
|
|
129
128
|
emittedText: "",
|
|
130
129
|
};
|
|
131
130
|
while (true) {
|
|
@@ -157,6 +156,12 @@ function emitCursorLiveQueuedEvent(
|
|
|
157
156
|
turn.emitter.appendThinkingDelta(event.text);
|
|
158
157
|
} else if (event.type === "thinking-completed") {
|
|
159
158
|
turn.emitter.closeThinking();
|
|
159
|
+
} else if (event.type === "text-completed") {
|
|
160
|
+
turn.emitter.completeTextMessage();
|
|
161
|
+
// Logical separators also keep suffix/prefix dedup aware of SDK messages,
|
|
162
|
+
// including boundaries queued across Pi tool-use turns.
|
|
163
|
+
if (turn.emittedText) turn.emittedText += CURSOR_TEXT_MESSAGE_SEPARATOR;
|
|
164
|
+
if (run?.emittedText) run.emittedText += CURSOR_TEXT_MESSAGE_SEPARATOR;
|
|
160
165
|
} else if (event.type === "text-delta") {
|
|
161
166
|
turn.emittedText += event.text;
|
|
162
167
|
if (run) run.emittedText += event.text;
|
|
@@ -176,6 +181,7 @@ function emitCursorNativeToolUseTurn(
|
|
|
176
181
|
): void {
|
|
177
182
|
const shouldTerminate = run.done && !run.finalText?.trim() && !cursorLiveRuns.peekEvent(run);
|
|
178
183
|
for (const tool of tools) {
|
|
184
|
+
if (!isJsonObject(tool.args)) throw new Error("Cursor replay tool arguments must be a JSON object");
|
|
179
185
|
const contentIndex = partial.content.length;
|
|
180
186
|
partial.content.push({
|
|
181
187
|
type: "toolCall",
|
|
@@ -232,6 +238,7 @@ function emitCursorBridgeToolUseTurn(
|
|
|
232
238
|
requests: CursorPiBridgeToolRequest[],
|
|
233
239
|
): void {
|
|
234
240
|
for (const request of requests) {
|
|
241
|
+
if (!isJsonObject(request.args)) throw new Error("Cursor bridge tool arguments must be a JSON object");
|
|
235
242
|
const contentIndex = partial.content.length;
|
|
236
243
|
partial.content.push({
|
|
237
244
|
type: "toolCall",
|
|
@@ -296,7 +303,7 @@ export async function drainCursorLiveRunTurn(
|
|
|
296
303
|
context: Context,
|
|
297
304
|
run: CursorLiveRun,
|
|
298
305
|
toolResultInputTokens: number,
|
|
299
|
-
options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal; debugRecorder?: CursorSdkEventDebugRecorder },
|
|
306
|
+
options: { mode: CursorLiveRunDrainMode; signal?: AbortSignal; debugRecorder?: CursorSdkEventDebugRecorder; emitter?: CursorPartialContentEmitter },
|
|
300
307
|
): Promise<CursorLiveRunDrainOutcome> {
|
|
301
308
|
const debugRecorder = options.debugRecorder ?? run.debugRecorder;
|
|
302
309
|
debugRecorder?.recordDrainEvent("turn_start", {
|
|
@@ -308,7 +315,7 @@ export async function drainCursorLiveRunTurn(
|
|
|
308
315
|
let outcome: CursorLiveRunDrainOutcome | undefined;
|
|
309
316
|
let outcomeDetails: Record<string, unknown> = {};
|
|
310
317
|
const turn: CursorLiveTurnState = {
|
|
311
|
-
emitter: new CursorPartialContentEmitter(stream, partial, -1, true),
|
|
318
|
+
emitter: options.emitter ?? new CursorPartialContentEmitter(stream, partial, -1, true),
|
|
312
319
|
emittedText: "",
|
|
313
320
|
};
|
|
314
321
|
|
|
@@ -378,7 +385,7 @@ export async function drainCursorLiveRunTurn(
|
|
|
378
385
|
turn.emitter.closeAll();
|
|
379
386
|
const finalText = trimCurrentTurnAlreadyEmittedCursorText(run.finalText ?? run.textDeltas.join(""), turn.emittedText, run.emittedText);
|
|
380
387
|
if (finalText) {
|
|
381
|
-
await emitTextDeltas(
|
|
388
|
+
await emitTextDeltas(turn.emitter, splitTextIntoReplayDeltas(finalText));
|
|
382
389
|
}
|
|
383
390
|
applyCursorUsage(partial, model, context, cursorLiveRuns.takeTurnInputTokens(run, toolResultInputTokens), {
|
|
384
391
|
runtime: "local",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AssistantMessage, AssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { InteractionUpdate } from "@cursor/sdk";
|
|
3
|
+
import { CURSOR_TEXT_MESSAGE_SEPARATOR } from "./cursor-partial-content-emitter.js";
|
|
3
4
|
import type { CursorLiveRun } from "./cursor-live-run-coordinator.js";
|
|
4
5
|
import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
|
|
5
6
|
import { truncateCursorDisplayLine } from "./cursor-display-text.js";
|
|
@@ -65,6 +66,7 @@ export class CursorSdkTurnCoordinator {
|
|
|
65
66
|
private readonly lifecycleEmitter: CursorToolLifecycleEmitter;
|
|
66
67
|
private readonly contentEmitter;
|
|
67
68
|
private sdkTurnUsage?: CursorSdkTurnUsage;
|
|
69
|
+
private hasPendingTextMessage = false;
|
|
68
70
|
|
|
69
71
|
constructor(options: CursorSdkTurnCoordinatorOptions) {
|
|
70
72
|
this.stream = options.stream;
|
|
@@ -153,6 +155,7 @@ export class CursorSdkTurnCoordinator {
|
|
|
153
155
|
cursorLiveRuns.recordSdkTurnEnded(this.liveRun, sdkTurnUsage);
|
|
154
156
|
}
|
|
155
157
|
if (update.type === "text-delta") {
|
|
158
|
+
if (update.text) this.hasPendingTextMessage = true;
|
|
156
159
|
this.textDeltas.push(update.text);
|
|
157
160
|
if (this.liveRun) {
|
|
158
161
|
cursorLiveRuns.queueEvent(this.liveRun, { type: "text-delta", text: update.text });
|
|
@@ -247,6 +250,18 @@ export class CursorSdkTurnCoordinator {
|
|
|
247
250
|
|
|
248
251
|
handleStep(stepEnvelope: unknown): void {
|
|
249
252
|
const stepType = getField(stepEnvelope, "type");
|
|
253
|
+
if (stepType === "assistantMessage") {
|
|
254
|
+
// SDK onStep completes a message; onDelta chunks within it are tokens,
|
|
255
|
+
// not separate messages. Keep the boundary in both output and final-text
|
|
256
|
+
// deduplication, without replaying the completed step's text a second time.
|
|
257
|
+
if (this.hasPendingTextMessage) {
|
|
258
|
+
this.hasPendingTextMessage = false;
|
|
259
|
+
this.textDeltas.push(CURSOR_TEXT_MESSAGE_SEPARATOR);
|
|
260
|
+
if (this.liveRun) cursorLiveRuns.queueEvent(this.liveRun, { type: "text-completed" });
|
|
261
|
+
else this.contentEmitter.completeTextMessage();
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
250
265
|
const step = getField(stepEnvelope, "message") ? stepEnvelope : undefined;
|
|
251
266
|
const rawStepToolCall = getField(step, "message");
|
|
252
267
|
if (stepType !== "toolCall") return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CursorLiveRunAbortError } from "./cursor-live-run-coordinator.js";
|
|
2
|
+
import { CursorPartialContentEmitter } from "./cursor-partial-content-emitter.js";
|
|
2
3
|
import {
|
|
3
4
|
cursorLiveRuns,
|
|
4
5
|
drainCursorLiveRunTurn,
|
|
@@ -28,6 +29,9 @@ export async function emitCursorLiveTurn(emitParams: EmitCursorLiveTurnParams):
|
|
|
28
29
|
const { liveRun, turnCoordinator } = prepared.runtime;
|
|
29
30
|
|
|
30
31
|
const { options, model } = params;
|
|
32
|
+
// Drain and abort recovery write to the same partial. Keep its open text block
|
|
33
|
+
// and deferred message boundary together; a new Pi turn still gets a new emitter.
|
|
34
|
+
const emitter = new CursorPartialContentEmitter(params.stream, params.partial, -1, true);
|
|
31
35
|
try {
|
|
32
36
|
await cursorLiveRuns.withRunLease(liveRun, options?.signal, async () => {
|
|
33
37
|
await cursorLiveRuns.waitForProgress(liveRun, options?.signal);
|
|
@@ -35,6 +39,7 @@ export async function emitCursorLiveTurn(emitParams: EmitCursorLiveTurnParams):
|
|
|
35
39
|
turnCoordinator.closeTraceBlock();
|
|
36
40
|
await drainCursorLiveRunTurn(params.stream, params.partial, model, params.context, liveRun, 0, {
|
|
37
41
|
mode: "emit",
|
|
42
|
+
emitter,
|
|
38
43
|
signal: options?.signal,
|
|
39
44
|
debugRecorder: sdkEventDebug,
|
|
40
45
|
});
|
|
@@ -45,6 +50,7 @@ export async function emitCursorLiveTurn(emitParams: EmitCursorLiveTurnParams):
|
|
|
45
50
|
turnCoordinator.closeTraceBlock();
|
|
46
51
|
flushPendingCursorLiveRunTraceEventsToStream(params.stream, params.partial, liveRun, {
|
|
47
52
|
includeTracesBehindQueuedTools: true,
|
|
53
|
+
emitter,
|
|
48
54
|
});
|
|
49
55
|
}
|
|
50
56
|
throw caught;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Context, SimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
2
|
+
import { getCursorConversationMessages, resolveCursorPiContext } from "./cursor-pi-context.js";
|
|
2
3
|
import type { AgentModeOption, ModelSelection, SDKAgent } from "@cursor/sdk";
|
|
3
4
|
import { configureCursorSdkHttp1 } from "./cursor-http1.js";
|
|
4
5
|
import { installCursorMcpToolTimeoutOverride } from "./cursor-mcp-timeout-override.js";
|
|
@@ -80,11 +81,15 @@ interface PrepareCursorProviderTurnContext extends PrepareCursorProviderTurnPara
|
|
|
80
81
|
|
|
81
82
|
function buildCursorCloudPromptContext(context: Context, handoff: "fresh" | "bootstrap" | "never"): Context {
|
|
82
83
|
if (handoff === "bootstrap") return context;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// Fresh cloud runs omit history, not the current Pi instructions. Replay before
|
|
85
|
+
// selecting the user message, otherwise transcript-only inputs lose the prompt.
|
|
86
|
+
const current = resolveCursorPiContext(context);
|
|
87
|
+
const messages = getCursorConversationMessages(context);
|
|
88
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
89
|
+
const message = messages[index];
|
|
90
|
+
if (message.role === "user") return { ...current, messages: [message] };
|
|
86
91
|
}
|
|
87
|
-
return { ...
|
|
92
|
+
return { ...current, messages: messages.slice(-1) };
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
const CLOUD_SEND_PLAN: CursorSessionSendPlan = { mode: "bootstrap", resetAgent: false, reason: "initial" };
|
|
@@ -135,7 +140,7 @@ async function prepareCursorCloudProviderTurn(
|
|
|
135
140
|
repo: resolvedConfig.cloud.repo.value,
|
|
136
141
|
branch: resolvedConfig.cloud.branch.value,
|
|
137
142
|
}),
|
|
138
|
-
hasPriorContext: context.
|
|
143
|
+
hasPriorContext: getCursorConversationMessages(context).length > 1,
|
|
139
144
|
});
|
|
140
145
|
if (!preflight.ok) throw new Error(formatCursorCloudPreflightError(preflight));
|
|
141
146
|
if (getPendingCursorLiveRun(context) || getActiveCursorLiveRunForCurrentScope()) {
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
/** Narrow untyped SDK/MCP arguments at the Pi transcript boundary without rewriting them. */
|
|
4
|
+
export function isJsonObject(value: unknown): value is Record<string, JsonValue> {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
6
|
+
&& isJsonValue(value, new Set());
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isJsonValue(value: unknown, ancestors: Set<object>): boolean {
|
|
10
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
11
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
12
|
+
if (typeof value !== "object" || ancestors.has(value)) return false;
|
|
13
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
|
|
14
|
+
ancestors.add(value);
|
|
15
|
+
const valid = Object.values(value).every(entry => isJsonValue(entry, ancestors));
|
|
16
|
+
ancestors.delete(value);
|
|
17
|
+
return valid;
|
|
18
|
+
}
|
|
19
|
+
|
|
1
20
|
export function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
2
21
|
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
3
22
|
}
|
package/src/cursor-skill-tool.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { resolveEffectiveCursorConfigForContext } from "./cursor-runtime-state.j
|
|
|
18
18
|
export const CURSOR_ACTIVATE_SKILL_TOOL_NAME = "cursor_activate_skill";
|
|
19
19
|
export const CURSOR_ACTIVATE_SKILL_MCP_NAME = "pi__cursor_activate_skill";
|
|
20
20
|
|
|
21
|
-
const AVAILABLE_SKILLS_SECTION_PATTERN =
|
|
21
|
+
const AVAILABLE_SKILLS_SECTION_PATTERN = /<skills>\nThe following skills provide specialized instructions for specific tasks\.[\s\S]*?<\/available_skills>\n<\/skills>|\n\nThe following skills provide specialized instructions for specific tasks\.[\s\S]*?<\/available_skills>/;
|
|
22
22
|
const MAX_SKILL_RESOURCES = 80;
|
|
23
23
|
const RESOURCE_DIR_NAMES = ["scripts", "references", "assets"] as const;
|
|
24
24
|
|