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.
@@ -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, readFileSync } from "node:fs";
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
- PLATFORM_SMOKE_WINDOWS_VM Parallels source VM name
51
- PLATFORM_SMOKE_WINDOWS_SNAPSHOT Snapshot name
52
- PLATFORM_SMOKE_WINDOWS_USER Windows SSH user
53
- PLATFORM_SMOKE_UBUNTU_IMAGE Ubuntu container image
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 assertHostReleaseVersionGuard() {
94
- const packageJson = JSON.parse(readFileSync(resolve(repoRoot, "package.json"), "utf8"));
95
- const result = spawnSync("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"], {
96
- cwd: repoRoot,
97
- encoding: "utf8",
98
- });
99
- if (result.status !== 0) throw new Error(`failed to inspect release tags: ${result.stderr || result.error?.message || "unknown git error"}`);
100
- const latestTag = result.stdout.split(/\r?\n/).find((tag) => tag.length > 0);
101
- if (!latestTag) throw new Error("no local release tags found; cannot enforce package version reuse guard");
102
- const latestVersion = latestTag.replace(/^v/, "");
103
- if (packageJson.version === latestVersion) throw new Error(`package version ${packageJson.version} reuses latest release tag ${latestTag}`);
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.sdkRun?.cancel();
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 = { hasUI: boolean; ui: Pick<ExtensionContext["ui"], "notify">; model?: ExtensionContext["model"] };
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 || !ctx.hasUI) return;
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
- 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 =
@@ -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
- return {
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
- return {
215
- content: [
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 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 {
@@ -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
- return {
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
- return {
209
- content: [{ type: "text" as const, text: `Skill not available: ${requestedName}. Available skills: ${getAvailableSkillNames().join(", ") || "none"}.` }],
210
- details: buildActivationDetails(undefined),
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
- return {
226
- content: [
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
  });
@@ -255,7 +255,7 @@ function persistCursorModePreference(pi: Pick<ExtensionAPI, "appendEntry">, mode
255
255
  }
256
256
  }
257
257
 
258
- function restoreCliCursorMode(raw: boolean | string | undefined, hasUI: boolean, notify: ExtensionContext["ui"]["notify"]): void {
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 (hasUI) {
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.hasUI, ctx.ui.notify.bind(ctx.ui));
438
+ restoreCliCursorMode(pi.getFlag("cursor-mode"), ctx.mode, ctx.ui.notify.bind(ctx.ui));
439
439
  updateCursorStatus(ctx);
440
440
  });
441
441