pi-cursor-sdk 0.1.31 → 0.1.33
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 +22 -0
- package/README.md +21 -1
- package/docs/cursor-testing-lessons.md +1 -1
- package/docs/platform-smoke.md +42 -19
- package/package.json +2 -3
- package/platform-smoke.config.mjs +7 -2
- package/scripts/platform-smoke/crabbox-runner.mjs +5 -4
- package/scripts/platform-smoke/doctor.mjs +54 -26
- 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/context-window-cache.ts +23 -7
- package/src/cursor-live-run-coordinator.ts +13 -1
- package/src/cursor-provider-errors.ts +3 -1
- package/src/cursor-sdk-process-error-guard.ts +4 -2
- package/src/cursor-state.ts +21 -12
- package/src/model-list-cache.ts +74 -10
- package/docs/crabbox-platform-testing-lessons.md +0 -508
|
@@ -18,15 +18,31 @@ function isPositiveInteger(value: unknown): value is number {
|
|
|
18
18
|
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseContextWindowCacheFile(value: unknown): ContextWindowCacheFile | undefined {
|
|
26
|
+
if (!isRecord(value)) return undefined;
|
|
27
|
+
const { contextWindows } = value;
|
|
28
|
+
if (contextWindows === undefined) return {};
|
|
29
|
+
if (!isRecord(contextWindows)) return undefined;
|
|
30
|
+
return {
|
|
31
|
+
contextWindows: Object.fromEntries(
|
|
32
|
+
Object.entries(contextWindows).filter((entry): entry is [string, number] => isPositiveInteger(entry[1])),
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
function loadUserContextWindowOverrides(): Map<string, number> {
|
|
22
38
|
userContextWindowOverrideLoadCount += 1;
|
|
23
39
|
const path = getCachePath();
|
|
24
40
|
const overrides = new Map<string, number>();
|
|
25
41
|
if (!existsSync(path)) return overrides;
|
|
26
42
|
try {
|
|
27
|
-
const parsed = JSON.parse(readFileSync(path, "utf-8"))
|
|
28
|
-
for (const [modelId, contextWindow] of Object.entries(parsed
|
|
29
|
-
|
|
43
|
+
const parsed = parseContextWindowCacheFile(JSON.parse(readFileSync(path, "utf-8")));
|
|
44
|
+
for (const [modelId, contextWindow] of Object.entries(parsed?.contextWindows ?? {})) {
|
|
45
|
+
overrides.set(modelId, contextWindow);
|
|
30
46
|
}
|
|
31
47
|
} catch {
|
|
32
48
|
return overrides;
|
|
@@ -52,10 +68,10 @@ export function getCachedContextWindow(modelId: string): number | undefined {
|
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
export function getCheckpointContextWindow(checkpoint: unknown): number | undefined {
|
|
55
|
-
if (checkpoint
|
|
56
|
-
const tokenDetails =
|
|
57
|
-
if (tokenDetails
|
|
58
|
-
const maxTokens =
|
|
71
|
+
if (!isRecord(checkpoint)) return undefined;
|
|
72
|
+
const { tokenDetails } = checkpoint;
|
|
73
|
+
if (!isRecord(tokenDetails)) return undefined;
|
|
74
|
+
const { maxTokens } = tokenDetails;
|
|
59
75
|
if (!isPositiveInteger(maxTokens)) return undefined;
|
|
60
76
|
return maxTokens;
|
|
61
77
|
}
|
|
@@ -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
|
}
|
|
@@ -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 =
|
|
@@ -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-state.ts
CHANGED
|
@@ -69,10 +69,13 @@ export function parseCursorAgentMode(raw: unknown): AgentModeOption | undefined
|
|
|
69
69
|
return isCursorAgentMode(mode) ? mode : undefined;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
73
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
72
76
|
function isCursorFastEntryData(value: unknown): value is CursorFastEntryData {
|
|
73
|
-
if (!value
|
|
74
|
-
|
|
75
|
-
return (typeof data.modelId === "string" || typeof data.baseModelId === "string") && typeof data.fast === "boolean";
|
|
77
|
+
if (!isRecord(value)) return false;
|
|
78
|
+
return (typeof value.modelId === "string" || typeof value.baseModelId === "string") && typeof value.fast === "boolean";
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
function getCursorFastEntryModelId(data: CursorFastEntryData): string {
|
|
@@ -80,9 +83,19 @@ function getCursorFastEntryModelId(data: CursorFastEntryData): string {
|
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
function isCursorModeEntryData(value: unknown): value is CursorModeEntryData {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
return isRecord(value) && isCursorAgentMode(value.mode);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseCursorGlobalConfig(value: unknown): CursorGlobalConfig | undefined {
|
|
90
|
+
if (!isRecord(value)) return undefined;
|
|
91
|
+
const { fastDefaults } = value;
|
|
92
|
+
if (fastDefaults === undefined) return {};
|
|
93
|
+
if (!isRecord(fastDefaults)) return undefined;
|
|
94
|
+
return {
|
|
95
|
+
fastDefaults: Object.fromEntries(
|
|
96
|
+
Object.entries(fastDefaults).filter((entry): entry is [string, boolean] => typeof entry[1] === "boolean"),
|
|
97
|
+
),
|
|
98
|
+
};
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
function getConfigPath(): string {
|
|
@@ -93,12 +106,8 @@ function loadGlobalFastPreferences(): Map<string, boolean> {
|
|
|
93
106
|
const path = getConfigPath();
|
|
94
107
|
if (!existsSync(path)) return new Map();
|
|
95
108
|
try {
|
|
96
|
-
const parsed = JSON.parse(readFileSync(path, "utf-8"))
|
|
97
|
-
return new Map(
|
|
98
|
-
Object.entries(parsed.fastDefaults ?? {}).filter(
|
|
99
|
-
(entry): entry is [string, boolean] => typeof entry[1] === "boolean",
|
|
100
|
-
),
|
|
101
|
-
);
|
|
109
|
+
const parsed = parseCursorGlobalConfig(JSON.parse(readFileSync(path, "utf-8")));
|
|
110
|
+
return new Map(Object.entries(parsed?.fastDefaults ?? {}));
|
|
102
111
|
} catch {
|
|
103
112
|
return new Map();
|
|
104
113
|
}
|
package/src/model-list-cache.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { parseEnvBoolean } from "./cursor-env-boolean.js";
|
|
|
8
8
|
const MODEL_LIST_CACHE_FILE = "cursor-sdk-model-list.json";
|
|
9
9
|
const MODEL_LIST_CACHE_VERSION = 1;
|
|
10
10
|
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
const MAX_CACHE_CLOCK_SKEW_MS = 5 * 60 * 1000;
|
|
11
12
|
const DISABLE_ENV_VAR = "PI_CURSOR_SDK_DISABLE_MODEL_CACHE";
|
|
12
13
|
const TTL_ENV_VAR = "PI_CURSOR_SDK_MODEL_CACHE_TTL_MS";
|
|
13
14
|
|
|
@@ -45,20 +46,83 @@ export function fingerprintApiKey(apiKey: string): string {
|
|
|
45
46
|
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isStringArray(value: unknown): value is string[] {
|
|
54
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isModelParameterValue(value: unknown): value is NonNullable<ModelListItem["variants"]>[number]["params"][number] {
|
|
58
|
+
return isRecord(value) && typeof value.id === "string" && typeof value.value === "string";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isModelParameterDefinitionValue(value: unknown): value is NonNullable<ModelListItem["parameters"]>[number]["values"][number] {
|
|
62
|
+
return isRecord(value) && typeof value.value === "string" && (value.displayName === undefined || typeof value.displayName === "string");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isModelParameterDefinition(value: unknown): value is NonNullable<ModelListItem["parameters"]>[number] {
|
|
66
|
+
if (!isRecord(value)) return false;
|
|
67
|
+
return (
|
|
68
|
+
typeof value.id === "string" &&
|
|
69
|
+
(value.displayName === undefined || typeof value.displayName === "string") &&
|
|
70
|
+
Array.isArray(value.values) &&
|
|
71
|
+
value.values.every(isModelParameterDefinitionValue)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isModelVariant(value: unknown): value is NonNullable<ModelListItem["variants"]>[number] {
|
|
76
|
+
if (!isRecord(value)) return false;
|
|
77
|
+
return (
|
|
78
|
+
Array.isArray(value.params) &&
|
|
79
|
+
value.params.every(isModelParameterValue) &&
|
|
80
|
+
typeof value.displayName === "string" &&
|
|
81
|
+
(value.description === undefined || typeof value.description === "string") &&
|
|
82
|
+
(value.isDefault === undefined || typeof value.isDefault === "boolean")
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isModelListItem(value: unknown): value is ModelListItem {
|
|
87
|
+
if (!isRecord(value)) return false;
|
|
88
|
+
return (
|
|
89
|
+
typeof value.id === "string" &&
|
|
90
|
+
typeof value.displayName === "string" &&
|
|
91
|
+
(value.description === undefined || typeof value.description === "string") &&
|
|
92
|
+
(value.aliases === undefined || isStringArray(value.aliases)) &&
|
|
93
|
+
(value.parameters === undefined || (Array.isArray(value.parameters) && value.parameters.every(isModelParameterDefinition))) &&
|
|
94
|
+
(value.variants === undefined || (Array.isArray(value.variants) && value.variants.every(isModelVariant)))
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isValidFetchedAt(value: unknown): value is number {
|
|
99
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= Date.now() + MAX_CACHE_CLOCK_SKEW_MS;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseModelListCacheFile(value: unknown): ModelListCacheFile | undefined {
|
|
103
|
+
if (!isRecord(value)) return undefined;
|
|
104
|
+
if (
|
|
105
|
+
value.version !== MODEL_LIST_CACHE_VERSION ||
|
|
106
|
+
!isValidFetchedAt(value.fetchedAt) ||
|
|
107
|
+
typeof value.keyFingerprint !== "string" ||
|
|
108
|
+
!Array.isArray(value.models) ||
|
|
109
|
+
!value.models.every(isModelListItem)
|
|
110
|
+
) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
version: value.version,
|
|
115
|
+
fetchedAt: value.fetchedAt,
|
|
116
|
+
keyFingerprint: value.keyFingerprint,
|
|
117
|
+
models: value.models,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
48
121
|
function readCacheFile(): ModelListCacheFile | undefined {
|
|
49
122
|
const path = getCachePath();
|
|
50
123
|
if (!existsSync(path)) return undefined;
|
|
51
124
|
try {
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
parsed.version !== MODEL_LIST_CACHE_VERSION ||
|
|
55
|
-
typeof parsed.fetchedAt !== "number" ||
|
|
56
|
-
typeof parsed.keyFingerprint !== "string" ||
|
|
57
|
-
!Array.isArray(parsed.models)
|
|
58
|
-
) {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
return parsed;
|
|
125
|
+
return parseModelListCacheFile(JSON.parse(readFileSync(path, "utf-8")));
|
|
62
126
|
} catch {
|
|
63
127
|
return undefined;
|
|
64
128
|
}
|