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.
@@ -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")) as ContextWindowCacheFile;
28
- for (const [modelId, contextWindow] of Object.entries(parsed.contextWindows ?? {})) {
29
- if (isPositiveInteger(contextWindow)) overrides.set(modelId, contextWindow);
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 === null || typeof checkpoint !== "object") return undefined;
56
- const tokenDetails = (checkpoint as Record<PropertyKey, unknown>).tokenDetails;
57
- if (tokenDetails === null || typeof tokenDetails !== "object") return undefined;
58
- const maxTokens = (tokenDetails as Record<PropertyKey, unknown>).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.sdkRun?.cancel();
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
- return hasCursorBackendDetails ? "cursor-backend-details" : "generic-connect";
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 SDK shapes.
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
- return activeProviderTurns.size > 0 && isCursorProvenance(classification.source);
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 {
@@ -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 || typeof value !== "object") return false;
74
- const data = value as Record<string, unknown>;
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
- if (!value || typeof value !== "object") return false;
84
- const data = value as Record<string, unknown>;
85
- return isCursorAgentMode(data.mode);
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")) as CursorGlobalConfig;
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
  }
@@ -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
- const parsed = JSON.parse(readFileSync(path, "utf-8")) as ModelListCacheFile;
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
  }