pi-cursor-sdk 0.1.32 → 0.1.34
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 +18 -0
- package/README.md +23 -3
- package/docs/cursor-live-smoke-checklist.md +4 -4
- package/docs/cursor-model-ux-spec.md +1 -1
- package/docs/cursor-native-tool-visual-audit.md +3 -3
- package/docs/cursor-testing-lessons.md +2 -2
- package/docs/platform-smoke.md +36 -14
- package/package.json +8 -9
- package/platform-smoke.config.mjs +6 -1
- package/scripts/platform-smoke/crabbox-runner.mjs +5 -4
- package/scripts/platform-smoke/doctor.mjs +36 -17
- package/scripts/platform-smoke/platform-build-windows.ps1 +0 -4
- package/scripts/platform-smoke/targets.mjs +2 -2
- package/scripts/platform-smoke.mjs +24 -19
- package/src/cursor-live-run-coordinator.ts +13 -1
- package/src/cursor-native-tool-display-registration.ts +5 -3
- package/src/cursor-provider-errors.ts +3 -1
- package/src/cursor-question-tool.ts +4 -15
- package/src/cursor-sdk-process-error-guard.ts +4 -2
- package/src/cursor-skill-tool.ts +7 -20
- package/src/cursor-state.ts +3 -3
- package/docs/crabbox-platform-testing-lessons.md +0 -508
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { resolve, dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { accessSync, constants
|
|
7
|
-
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { accessSync, constants } from "node:fs";
|
|
8
7
|
|
|
9
8
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
10
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -47,11 +46,11 @@ Environment:
|
|
|
47
46
|
PLATFORM_SMOKE_MAC_HOST macOS SSH host (default: localhost)
|
|
48
47
|
PLATFORM_SMOKE_MAC_USER macOS SSH user (default: \$USER)
|
|
49
48
|
PLATFORM_SMOKE_MAC_WORK_ROOT macOS work root
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT Windows native work root
|
|
49
|
+
PLATFORM_SMOKE_UBUNTU_IMAGE Ubuntu container image
|
|
50
|
+
PLATFORM_SMOKE_WINDOWS_VM Parallels source VM override (default from config)
|
|
51
|
+
PLATFORM_SMOKE_WINDOWS_SNAPSHOT Snapshot override (default from config)
|
|
52
|
+
PLATFORM_SMOKE_WINDOWS_USER Windows SSH user override (default: \$USER)
|
|
53
|
+
PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT Windows native work root override (default from config)
|
|
55
54
|
`);
|
|
56
55
|
}
|
|
57
56
|
|
|
@@ -90,17 +89,17 @@ function parseArgs(argv) {
|
|
|
90
89
|
return args;
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
function
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
92
|
+
function validateSelections(targets, suites) {
|
|
93
|
+
const allowedTargets = new Set(config.requiredTargets ?? []);
|
|
94
|
+
const allowedSuites = new Set(config.requiredSuites ?? []);
|
|
95
|
+
const badTargets = targets.filter((target) => !allowedTargets.has(target));
|
|
96
|
+
const badSuites = suites.filter((suite) => !allowedSuites.has(suite));
|
|
97
|
+
if (badTargets.length > 0) {
|
|
98
|
+
throw new Error(`unknown target(s): ${badTargets.join(", ")}; allowed: ${[...allowedTargets].join(", ")}`);
|
|
99
|
+
}
|
|
100
|
+
if (badSuites.length > 0) {
|
|
101
|
+
throw new Error(`unknown suite(s): ${badSuites.join(", ")}; allowed: ${[...allowedSuites].join(", ")}`);
|
|
102
|
+
}
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
// ── commands ───────────────────────────────────────────────────────────────
|
|
@@ -158,7 +157,6 @@ async function main() {
|
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
if (args.command === "run") {
|
|
161
|
-
assertHostReleaseVersionGuard();
|
|
162
160
|
const targets = args.target
|
|
163
161
|
? args.target.split(",").map((s) => s.trim()).filter(Boolean)
|
|
164
162
|
: config.requiredTargets;
|
|
@@ -167,6 +165,13 @@ async function main() {
|
|
|
167
165
|
? [args.suite]
|
|
168
166
|
: config.requiredSuites;
|
|
169
167
|
|
|
168
|
+
try {
|
|
169
|
+
validateSelections(targets, suites);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error(err.message);
|
|
172
|
+
process.exit(2);
|
|
173
|
+
}
|
|
174
|
+
|
|
170
175
|
const targetRuns = targets.map(async (targetName) => {
|
|
171
176
|
console.log(`\n=== Target: ${targetName} ===`);
|
|
172
177
|
const result = args.suite
|
|
@@ -11,6 +11,7 @@ import type { CursorNativeToolDisplayItem } from "./cursor-native-tool-display.j
|
|
|
11
11
|
import type { CursorPiBridgeToolRequest, CursorPiToolBridgeRun } from "./cursor-pi-tool-bridge.js";
|
|
12
12
|
import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
|
|
13
13
|
import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
|
|
14
|
+
import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
|
|
14
15
|
|
|
15
16
|
export class CursorLiveRunAbortError extends Error {
|
|
16
17
|
constructor() {
|
|
@@ -118,6 +119,17 @@ interface LeaseWaiter {
|
|
|
118
119
|
onAbort?: () => void;
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
async function cancelCursorLiveSdkRun(run: CursorLiveRun): Promise<void> {
|
|
123
|
+
if (!run.sdkRun) return;
|
|
124
|
+
const guard = installCursorSdkProcessErrorGuard();
|
|
125
|
+
guard.suppressAbortErrors();
|
|
126
|
+
try {
|
|
127
|
+
await run.sdkRun.cancel();
|
|
128
|
+
} finally {
|
|
129
|
+
guard.dispose();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
121
133
|
interface CursorLiveRunPrivateState {
|
|
122
134
|
waiters: Set<ProgressWaiter>;
|
|
123
135
|
idleDisposeTimer?: ReturnType<typeof setTimeout>;
|
|
@@ -474,7 +486,7 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
|
|
|
474
486
|
if (abandoned) {
|
|
475
487
|
if (!run.done) {
|
|
476
488
|
try {
|
|
477
|
-
await run
|
|
489
|
+
await cancelCursorLiveSdkRun(run);
|
|
478
490
|
} catch {
|
|
479
491
|
// cancellation failure should not block session-agent abandonment
|
|
480
492
|
}
|
|
@@ -39,7 +39,9 @@ function hasNonBuiltinTool(pi: Pick<ExtensionAPI, "getAllTools">, toolName: Nati
|
|
|
39
39
|
return existingTool !== undefined && existingTool.sourceInfo.source !== "builtin";
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
type NativeRegistrationContext =
|
|
42
|
+
type NativeRegistrationContext = Pick<ExtensionContext, "mode" | "model"> & {
|
|
43
|
+
ui: Pick<ExtensionContext["ui"], "notify">;
|
|
44
|
+
};
|
|
43
45
|
|
|
44
46
|
function registerNativeCursorToolsFromSet(
|
|
45
47
|
pi: CursorNativeToolRegistryApi,
|
|
@@ -60,7 +62,7 @@ function registerNativeCursorToolsFromSet(
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
function notifySkippedNativeCursorToolsIfNeeded(ctx: NativeRegistrationContext, skippedToolNames: readonly NativeCursorToolName[]): void {
|
|
63
|
-
if (skippedToolNames.length === 0 || readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) !== true ||
|
|
65
|
+
if (skippedToolNames.length === 0 || readBooleanEnv(NATIVE_CURSOR_TOOL_DISPLAY_ENV) !== true || ctx.mode !== "tui") return;
|
|
64
66
|
ctx.ui.notify(
|
|
65
67
|
`Cursor native tool replay skipped for ${skippedToolNames.join(", ")} because another extension already provides ${skippedToolNames.length === 1 ? "that tool" : "those tools"}. Cursor will use scrubbed activity transcripts for skipped tools.`,
|
|
66
68
|
"warning",
|
|
@@ -101,7 +103,7 @@ function ensureNativeCursorToolsRegisteredForModel(pi: CursorNativeToolRegistryA
|
|
|
101
103
|
skippedNativeToolNames.clear();
|
|
102
104
|
return;
|
|
103
105
|
}
|
|
104
|
-
if (!isCursorModel(ctx.model) || hasAttemptedNativeCursorToolRegistration()) return;
|
|
106
|
+
if (ctx.mode !== "tui" || !isCursorModel(ctx.model) || hasAttemptedNativeCursorToolRegistration()) return;
|
|
105
107
|
|
|
106
108
|
const nonCoreToolNames = NATIVE_CURSOR_TOOL_NAMES.filter((toolName) => !isCursorCorePiReplayToolName(toolName));
|
|
107
109
|
const skippedToolNames = [
|
|
@@ -60,13 +60,15 @@ function getCursorConnectSource(error: unknown, record: Record<string, unknown>
|
|
|
60
60
|
const type = getErrorStringField(asRecord(detail), "type");
|
|
61
61
|
return typeof type === "string" && type.startsWith("aiserver.");
|
|
62
62
|
});
|
|
63
|
-
|
|
63
|
+
if (hasCursorBackendDetails) return "cursor-backend-details";
|
|
64
|
+
return stack.includes("@connectrpc/connect-node") ? "connect-node-stack" : "generic-connect";
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export type CursorConnectErrorSource =
|
|
67
68
|
| "cursor-sdk-stack"
|
|
68
69
|
| "cursor-extension-connect-stack"
|
|
69
70
|
| "cursor-backend-details"
|
|
71
|
+
| "connect-node-stack"
|
|
70
72
|
| "generic-connect";
|
|
71
73
|
|
|
72
74
|
export type CursorConnectErrorClassification =
|
|
@@ -204,23 +204,12 @@ export function registerCursorQuestionTool(pi: CursorQuestionToolExtensionApi):
|
|
|
204
204
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
205
205
|
const questions = normalizeQuestions(params as CursorAskQuestionParams);
|
|
206
206
|
if (questions.length === 0) {
|
|
207
|
-
|
|
208
|
-
content: [{ type: "text" as const, text: "No valid question was provided." }],
|
|
209
|
-
details: buildDetails([], [], ctx.hasUI),
|
|
210
|
-
isError: true,
|
|
211
|
-
};
|
|
207
|
+
throw new Error("No valid question was provided.");
|
|
212
208
|
}
|
|
213
209
|
if (!ctx.hasUI) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
type: "text" as const,
|
|
218
|
-
text: "Cannot ask the user because pi UI is unavailable. Make a reasonable default choice and state the assumption before proceeding.",
|
|
219
|
-
},
|
|
220
|
-
],
|
|
221
|
-
details: buildDetails(questions, [], false),
|
|
222
|
-
isError: true,
|
|
223
|
-
};
|
|
210
|
+
throw new Error(
|
|
211
|
+
"Cannot ask the user because pi UI is unavailable. Make a reasonable default choice and state the assumption before proceeding.",
|
|
212
|
+
);
|
|
224
213
|
}
|
|
225
214
|
|
|
226
215
|
const answers: CursorQuestionAnswer[] = [];
|
|
@@ -13,7 +13,7 @@ type GenericProcessEmit = (event: string | symbol, ...args: unknown[]) => boolea
|
|
|
13
13
|
|
|
14
14
|
// The local Cursor SDK can surface some ConnectRPC failures as process-level
|
|
15
15
|
// uncaught exceptions/unhandled rejections even when run.wait()/run.cancel() is awaited.
|
|
16
|
-
// Keep suppression scoped to active Cursor provider turns and tightly matched
|
|
16
|
+
// Keep suppression scoped to active Cursor provider turns and tightly matched ConnectRPC shapes.
|
|
17
17
|
const activeProviderTurns = new Set<CursorSdkProcessErrorGuardToken>();
|
|
18
18
|
let originalProcessEmit: GenericProcessEmit | undefined;
|
|
19
19
|
let captureCallbackInstalled = false;
|
|
@@ -35,7 +35,9 @@ function shouldSuppressProcessError(event: string | symbol, args: readonly unkno
|
|
|
35
35
|
const classification = classifyCursorConnectError(error);
|
|
36
36
|
if (!classification) return false;
|
|
37
37
|
if (classification.kind === "abort") return hasActiveAbortSuppression();
|
|
38
|
-
|
|
38
|
+
if (activeProviderTurns.size === 0) return false;
|
|
39
|
+
if (classification.kind === "network") return isCursorProvenance(classification.source) || classification.source === "connect-node-stack";
|
|
40
|
+
return isCursorProvenance(classification.source);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
function installProcessEmitPatch(): void {
|
package/src/cursor-skill-tool.ts
CHANGED
|
@@ -197,19 +197,13 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
|
|
|
197
197
|
async execute(_toolCallId, params) {
|
|
198
198
|
const requestedName = (params as CursorActivateSkillParams).name?.trim();
|
|
199
199
|
if (!requestedName) {
|
|
200
|
-
|
|
201
|
-
content: [{ type: "text" as const, text: "No skill name was provided." }],
|
|
202
|
-
details: buildActivationDetails(undefined),
|
|
203
|
-
isError: true,
|
|
204
|
-
};
|
|
200
|
+
throw new Error("No skill name was provided.");
|
|
205
201
|
}
|
|
206
202
|
const skill = currentSkillsByName.get(requestedName);
|
|
207
203
|
if (!skill) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
isError: true,
|
|
212
|
-
};
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Skill not available: ${requestedName}. Available skills: ${getAvailableSkillNames().join(", ") || "none"}.`,
|
|
206
|
+
);
|
|
213
207
|
}
|
|
214
208
|
|
|
215
209
|
try {
|
|
@@ -222,16 +216,9 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
|
|
|
222
216
|
details: buildActivationDetails(skill, resources),
|
|
223
217
|
};
|
|
224
218
|
} catch (error) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
type: "text" as const,
|
|
229
|
-
text: `Failed to load skill ${requestedName} from ${skill.filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
230
|
-
},
|
|
231
|
-
],
|
|
232
|
-
details: buildActivationDetails(skill),
|
|
233
|
-
isError: true,
|
|
234
|
-
};
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Failed to load skill ${requestedName} from ${skill.filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
221
|
+
);
|
|
235
222
|
}
|
|
236
223
|
},
|
|
237
224
|
});
|
package/src/cursor-state.ts
CHANGED
|
@@ -255,7 +255,7 @@ function persistCursorModePreference(pi: Pick<ExtensionAPI, "appendEntry">, mode
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
function restoreCliCursorMode(raw: boolean | string | undefined,
|
|
258
|
+
function restoreCliCursorMode(raw: boolean | string | undefined, mode: ExtensionContext["mode"], notify: ExtensionContext["ui"]["notify"]): void {
|
|
259
259
|
cliCursorModeState = { kind: "unset" };
|
|
260
260
|
if (raw === undefined || raw === "" || raw === false) return;
|
|
261
261
|
const parsed = parseCursorAgentMode(raw);
|
|
@@ -266,7 +266,7 @@ function restoreCliCursorMode(raw: boolean | string | undefined, hasUI: boolean,
|
|
|
266
266
|
const rawText = String(raw);
|
|
267
267
|
const message = formatInvalidCursorMode(rawText);
|
|
268
268
|
cliCursorModeState = { kind: "invalid", raw: rawText, message };
|
|
269
|
-
if (
|
|
269
|
+
if (mode === "tui") {
|
|
270
270
|
notify(message, "error");
|
|
271
271
|
return;
|
|
272
272
|
}
|
|
@@ -435,7 +435,7 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
|
|
|
435
435
|
cliForceNoFast = pi.getFlag("cursor-no-fast") === true;
|
|
436
436
|
restoreSessionFastPreferences(ctx);
|
|
437
437
|
restoreSessionCursorMode(ctx);
|
|
438
|
-
restoreCliCursorMode(pi.getFlag("cursor-mode"), ctx.
|
|
438
|
+
restoreCliCursorMode(pi.getFlag("cursor-mode"), ctx.mode, ctx.ui.notify.bind(ctx.ui));
|
|
439
439
|
updateCursorStatus(ctx);
|
|
440
440
|
});
|
|
441
441
|
|