@emmaneugene/pi-cursor-sdk 0.3.7 → 0.4.1

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +99 -126
  3. package/dist/cursor-agents-context-registration.js +1 -3
  4. package/dist/cursor-agents-context.js +2 -2
  5. package/dist/cursor-config.js +2 -290
  6. package/dist/cursor-extension-factory-guard.js +31 -0
  7. package/dist/cursor-pi-tool-bridge-server.js +3 -0
  8. package/dist/cursor-pi-tool-bridge.js +5 -0
  9. package/dist/cursor-provider-errors.js +3 -58
  10. package/dist/cursor-provider-live-run-drain.js +0 -3
  11. package/dist/cursor-provider-run-finalizer.js +4 -7
  12. package/dist/cursor-provider-run-outcome.js +1 -1
  13. package/dist/cursor-provider-turn-finalize.js +3 -55
  14. package/dist/cursor-provider-turn-prepare.js +8 -144
  15. package/dist/cursor-provider-turn-runner.js +9 -23
  16. package/dist/cursor-provider-turn-send.js +8 -35
  17. package/dist/cursor-runtime-state.js +6 -268
  18. package/dist/cursor-sdk-billed-usage.js +3 -18
  19. package/dist/cursor-sdk-platform-package.js +88 -0
  20. package/dist/cursor-session-agent.js +18 -14
  21. package/dist/cursor-skill-tool.js +12 -22
  22. package/dist/cursor-state.js +5 -8
  23. package/dist/cursor-usage-accounting.js +1 -1
  24. package/dist/index.js +65 -49
  25. package/docs/cursor-dogfood-checklist.md +4 -4
  26. package/docs/cursor-live-smoke-checklist.md +24 -24
  27. package/docs/cursor-model-ux-spec.md +27 -30
  28. package/docs/cursor-native-tool-replay.md +4 -4
  29. package/docs/cursor-native-tool-visual-audit.md +10 -10
  30. package/docs/cursor-testing-lessons.md +38 -16
  31. package/docs/cursor-tool-surfaces.md +2 -4
  32. package/docs/platform-smoke-implementation.md +5 -5
  33. package/docs/platform-smoke.md +28 -73
  34. package/node_modules/cross-spawn/node_modules/which/CHANGELOG.md +166 -0
  35. package/package.json +3 -17
  36. package/platform-smoke.config.mjs +2 -1
  37. package/scripts/lib/cursor-visual-render.mjs +12 -2
  38. package/scripts/lib/local-resume-smoke-harness.mjs +0 -18
  39. package/scripts/platform-smoke/card-detect.mjs +1 -1
  40. package/scripts/platform-smoke/local-resume-runner.mjs +1 -1
  41. package/scripts/platform-smoke/scenarios.mjs +1 -1
  42. package/scripts/platform-smoke/targets.mjs +14 -5
  43. package/scripts/visual-tui-smoke-self-test.mjs +9 -33
  44. package/scripts/visual-tui-smoke.mjs +54 -11
  45. package/src/cursor-agents-context-registration.ts +0 -5
  46. package/src/cursor-agents-context.ts +1 -3
  47. package/src/cursor-config.ts +8 -379
  48. package/src/cursor-extension-factory-guard.ts +57 -0
  49. package/src/cursor-pi-tool-bridge-server.ts +4 -0
  50. package/src/cursor-pi-tool-bridge.ts +5 -0
  51. package/src/cursor-provider-errors.ts +2 -62
  52. package/src/cursor-provider-live-run-drain.ts +0 -3
  53. package/src/cursor-provider-run-finalizer.ts +4 -12
  54. package/src/cursor-provider-run-outcome.ts +0 -3
  55. package/src/cursor-provider-turn-finalize.ts +3 -61
  56. package/src/cursor-provider-turn-prepare.ts +8 -165
  57. package/src/cursor-provider-turn-runner.ts +15 -31
  58. package/src/cursor-provider-turn-send.ts +7 -38
  59. package/src/cursor-provider-turn-types.ts +9 -30
  60. package/src/cursor-runtime-state.ts +6 -345
  61. package/src/cursor-sdk-billed-usage.ts +3 -24
  62. package/src/cursor-sdk-platform-package.ts +106 -0
  63. package/src/cursor-session-agent.ts +16 -12
  64. package/src/cursor-skill-tool.ts +10 -26
  65. package/src/cursor-state.ts +5 -10
  66. package/src/cursor-usage-accounting.ts +1 -3
  67. package/src/index.ts +70 -49
  68. package/dist/cursor-cloud-lifecycle.js +0 -650
  69. package/dist/cursor-cloud-local-state.js +0 -460
  70. package/dist/cursor-cloud-options.js +0 -145
  71. package/dist/cursor-cloud-reporting.js +0 -222
  72. package/dist/cursor-ripgrep-path.js +0 -27
  73. package/scripts/cloud-runtime-smoke.d.mts +0 -42
  74. package/scripts/cloud-runtime-smoke.mjs +0 -623
  75. package/scripts/lib/cloud-smoke-artifacts.d.mts +0 -16
  76. package/scripts/lib/cloud-smoke-artifacts.mjs +0 -94
  77. package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +0 -209
  78. package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +0 -585
  79. package/scripts/lib/cloud-smoke-github.d.mts +0 -78
  80. package/scripts/lib/cloud-smoke-github.mjs +0 -331
  81. package/scripts/lib/cloud-smoke-pi-runner.d.mts +0 -42
  82. package/scripts/lib/cloud-smoke-pi-runner.mjs +0 -214
  83. package/scripts/lib/cloud-smoke-shutdown.d.mts +0 -67
  84. package/scripts/lib/cloud-smoke-shutdown.mjs +0 -133
  85. package/shared/cursor-cloud-lifecycle-constants.d.mts +0 -3
  86. package/shared/cursor-cloud-lifecycle-constants.mjs +0 -7
  87. package/src/cursor-cloud-lifecycle.ts +0 -733
  88. package/src/cursor-cloud-local-state.ts +0 -536
  89. package/src/cursor-cloud-options.ts +0 -182
  90. package/src/cursor-cloud-reporting.ts +0 -267
  91. package/src/cursor-ripgrep-path.ts +0 -32
@@ -9,11 +9,9 @@ import type {
9
9
  } from "@earendil-works/pi-coding-agent";
10
10
  import { Type } from "typebox";
11
11
  import { arePiToolsDisabled } from "./cursor-active-tools.js";
12
- import type { CursorRuntime } from "./cursor-config.js";
13
12
  import { isCursorModel } from "./cursor-model.js";
14
13
  import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
15
14
  import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
16
- import { resolveEffectiveCursorConfigForContext } from "./cursor-runtime-state.js";
17
15
 
18
16
  export const CURSOR_ACTIVATE_SKILL_TOOL_NAME = "cursor_activate_skill";
19
17
  export const CURSOR_ACTIVATE_SKILL_MCP_NAME = "pi__cursor_activate_skill";
@@ -59,24 +57,16 @@ function getAvailableSkillNames(): string[] {
59
57
  return [...currentSkillsByName.keys()].sort();
60
58
  }
61
59
 
62
- function resolveEffectiveRuntimeForSkillLifecycle(
63
- cursorModel: boolean,
64
- ctx: Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "isProjectTrusted">>,
65
- ): CursorRuntime {
66
- return cursorModel ? resolveEffectiveCursorConfigForContext(ctx).runtime.value : "local";
67
- }
68
-
69
- function shouldExposeSkillTool(model: ExtensionContext["model"], runtime: CursorRuntime): boolean {
70
- return runtime === "local" && isCursorModel(model) && resolveCursorPiToolBridgeEnabled() && currentSkillsByName.size > 0;
60
+ function shouldExposeSkillTool(model: ExtensionContext["model"]): boolean {
61
+ return isCursorModel(model) && resolveCursorPiToolBridgeEnabled() && currentSkillsByName.size > 0;
71
62
  }
72
63
 
73
64
  function syncCursorSkillToolForModel(
74
65
  pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">,
75
66
  model: ExtensionContext["model"],
76
- runtime: CursorRuntime,
77
67
  ): void {
78
68
  const activeToolNames = new Set(pi.getActiveTools());
79
- const shouldBeActive = !arePiToolsDisabled(pi) && shouldExposeSkillTool(model, runtime);
69
+ const shouldBeActive = !arePiToolsDisabled(pi) && shouldExposeSkillTool(model);
80
70
  const alreadyActive = activeToolNames.has(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
81
71
  if (shouldBeActive === alreadyActive) return;
82
72
  if (shouldBeActive) {
@@ -114,10 +104,8 @@ export function resolveCursorSkillSystemPrompt(
114
104
  systemPrompt: string,
115
105
  model: ExtensionContext["model"],
116
106
  systemPromptOptions?: BuildSystemPromptOptions,
117
- runtime: CursorRuntime = "local",
118
107
  ): string {
119
108
  if (!isCursorModel(model)) return systemPrompt;
120
- if (runtime === "cloud") return systemPrompt.replace(AVAILABLE_SKILLS_SECTION_PATTERN, "");
121
109
  const skills = getVisibleSkills(systemPromptOptions?.skills);
122
110
  if (skills.length === 0) return systemPrompt;
123
111
  const replacement = formatCursorSkillsForPrompt(skills);
@@ -231,9 +219,9 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
231
219
  },
232
220
  });
233
221
 
234
- const clearSkillsAndSync = (model: ExtensionContext["model"], runtime: CursorRuntime = "local"): void => {
222
+ const clearSkillsAndSync = (model: ExtensionContext["model"]): void => {
235
223
  setCurrentSkills([]);
236
- syncCursorSkillToolForModel(pi, model, runtime);
224
+ syncCursorSkillToolForModel(pi, model);
237
225
  };
238
226
 
239
227
  registerCursorModelLifecycle(pi, {
@@ -244,21 +232,17 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
244
232
  clearSkillsAndSync(event.model);
245
233
  },
246
234
  turnStart: (_event, ctx) => {
247
- const cursorModel = isCursorModel(ctx.model);
248
- const runtime = resolveEffectiveRuntimeForSkillLifecycle(cursorModel, ctx);
249
- if (!cursorModel || runtime === "cloud") setCurrentSkills([]);
250
- syncCursorSkillToolForModel(pi, ctx.model, runtime);
235
+ if (!isCursorModel(ctx.model)) setCurrentSkills([]);
236
+ syncCursorSkillToolForModel(pi, ctx.model);
251
237
  },
252
238
  beforeAgentStart: (event, ctx) => {
253
- const cursorModel = isCursorModel(ctx.model);
254
- const runtime = resolveEffectiveRuntimeForSkillLifecycle(cursorModel, ctx);
255
- if (cursorModel && runtime === "local") {
239
+ if (isCursorModel(ctx.model)) {
256
240
  setCurrentSkills(event.systemPromptOptions?.skills);
257
241
  } else {
258
242
  setCurrentSkills([]);
259
243
  }
260
- syncCursorSkillToolForModel(pi, ctx.model, runtime);
261
- const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions, runtime);
244
+ syncCursorSkillToolForModel(pi, ctx.model);
245
+ const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions);
262
246
  if (resolved === event.systemPrompt) return undefined;
263
247
  return { systemPrompt: resolved };
264
248
  },
@@ -42,16 +42,14 @@ import {
42
42
  } from "./cursor-config.js";
43
43
  import {
44
44
  consumeCursorLocalForceOverride,
45
- CURSOR_RUNTIME_ENTRY_TYPE,
46
45
  formatCursorStatus,
47
46
  getCursorCliConfig,
48
47
  getCursorSessionConfig,
49
- registerCursorCloudRuntimeControls,
48
+ registerCursorLocalRuntimeFlags,
50
49
  resetCursorRuntimeStateForTests,
51
50
  resolveCursorStatusRuntime,
52
51
  resolveEffectiveCursorConfigForContext,
53
52
  restoreCursorCliState,
54
- restoreSessionCursorRuntimeState,
55
53
  type CursorRuntimeStateExtensionApi,
56
54
  } from "./cursor-runtime-state.js";
57
55
 
@@ -178,7 +176,6 @@ function restoreSessionCursorMode(branch: readonly SessionEntry[]): void {
178
176
 
179
177
  function restoreSessionCursorPreferences(ctx: { sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch"> }): void {
180
178
  const branch = ctx.sessionManager.getBranch();
181
- restoreSessionCursorRuntimeState(branch);
182
179
  restoreSessionFastPreferences(branch);
183
180
  restoreSessionCursorMode(branch);
184
181
  restoreSessionCursorHttp1(branch);
@@ -266,14 +263,13 @@ function updateCursorStatus(ctx: CursorStatusContext & Pick<ExtensionContext, "m
266
263
  const modeResolution = resolveCursorAgentMode();
267
264
  const mode = modeResolution.kind === "invalid" ? "invalid" : modeResolution.mode;
268
265
  if (resolution.kind === "invalid") {
269
- ctx.ui.setStatus("cursor", formatCursorStatus("invalid", undefined, mode));
266
+ ctx.ui.setStatus("cursor", formatCursorStatus(undefined, "invalid"));
270
267
  return;
271
268
  }
272
- const runtime = resolution.runtime.value;
273
- const fast = runtime === "cloud" ? undefined : metadata?.supportsFast ? getEffectiveFast(model.id) : undefined;
269
+ const fast = metadata?.supportsFast ? getEffectiveFast(model.id) : undefined;
274
270
  ctx.ui.setStatus(
275
271
  "cursor",
276
- formatCursorStatus(runtime, fast, mode, resolution.useHttp1ForAgent.value),
272
+ formatCursorStatus(fast, mode, resolution.useHttp1ForAgent.value),
277
273
  );
278
274
  }
279
275
 
@@ -432,7 +428,7 @@ export function getEffectiveFastForModelId(modelId: string): boolean | undefined
432
428
  }
433
429
 
434
430
  export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtensionApi): void {
435
- registerCursorCloudRuntimeControls(pi, { refreshStatus: updateCursorStatus });
431
+ registerCursorLocalRuntimeFlags(pi);
436
432
 
437
433
  pi.registerFlag("cursor-fast", {
438
434
  description: "Force Cursor fast mode for this run when the selected Cursor model supports it",
@@ -673,7 +669,6 @@ export const __testUtils = {
673
669
  FAST_ENTRY_TYPE,
674
670
  MODE_ENTRY_TYPE,
675
671
  CURSOR_HTTP1_ENTRY_TYPE,
676
- RUNTIME_ENTRY_TYPE: CURSOR_RUNTIME_ENTRY_TYPE,
677
672
  DEFAULT_CURSOR_AGENT_MODE,
678
673
  getConfigPath,
679
674
  loadGlobalFastPreferences,
@@ -7,7 +7,6 @@ import {
7
7
  type CursorPromptOptions,
8
8
  } from "./context.js";
9
9
  import { asRecord, getNumber } from "./cursor-record-utils.js";
10
- import type { CursorRuntime } from "./cursor-config.js";
11
10
 
12
11
  export interface CursorUsagePromptOptions extends CursorPromptOptions {
13
12
  maxInputTokens: number;
@@ -110,7 +109,6 @@ export function isCursorSdkUsageSafeForPiMessage(turnUsage: CursorSdkTurnUsage,
110
109
  }
111
110
 
112
111
  export interface CursorSdkUsageApplyOptions {
113
- runtime: CursorRuntime;
114
112
  turn?: CursorSdkTurnUsage;
115
113
  billed?: CursorSdkTurnUsage;
116
114
  }
@@ -209,7 +207,7 @@ export function applyCursorUsage(
209
207
  sdkUsage?: CursorSdkUsageApplyOptions,
210
208
  ): void {
211
209
  const billed = sdkUsage?.billed;
212
- const localTurn = sdkUsage?.runtime === "local" ? sdkUsage.turn : undefined;
210
+ const localTurn = sdkUsage?.turn;
213
211
  if (billed && isCursorSdkUsagePartitionSafe(billed, model)) {
214
212
  applyCursorSdkUsage(partial, billed);
215
213
  applyResolvedCursorOccupancy(partial, model, context, localTurn);
package/src/index.ts CHANGED
@@ -16,6 +16,11 @@ import { registerCursorAgentsContextDedup } from "./cursor-agents-context-regist
16
16
  import { registerCursorOverflowNormalization } from "./cursor-provider-overflow.js";
17
17
  import { registerCursorSdkSessionProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
18
18
  import { prepareCursorSessionForCompaction } from "./cursor-session-compaction-prep.js";
19
+ import {
20
+ claimCursorExtensionFactory,
21
+ registerCursorExtensionFactoryRelease,
22
+ releaseCursorExtensionFactory,
23
+ } from "./cursor-extension-factory-guard.js";
19
24
 
20
25
  type CursorExtensionApi =
21
26
  & Pick<ExtensionAPI, "registerProvider" | "registerCommand" | "on">
@@ -31,7 +36,8 @@ type CursorExtensionApi =
31
36
  & Parameters<typeof registerCursorFallbackIssueWarning>[0]
32
37
  & Parameters<typeof registerCursorAgentsContextDedup>[0]
33
38
  & Parameters<typeof registerCursorOverflowNormalization>[0]
34
- & Parameters<typeof registerCursorSdkSessionProcessErrorGuard>[0];
39
+ & Parameters<typeof registerCursorSdkSessionProcessErrorGuard>[0]
40
+ & Parameters<typeof registerCursorExtensionFactoryRelease>[0];
35
41
 
36
42
  function createCursorProviderConfig(models: ProviderModelConfig[]): ProviderConfig {
37
43
  return {
@@ -49,55 +55,70 @@ function registerCursorProvider(pi: Pick<ExtensionAPI, "registerProvider">, mode
49
55
  }
50
56
 
51
57
  export default async function (pi: CursorExtensionApi) {
52
- // Session cwd must register before other session_start listeners that depend on it.
53
- registerCursorSessionScope(pi);
54
- registerCursorSessionAgentLineage(pi);
55
- registerCursorSessionAgentLifecycle(pi);
56
- registerCursorSessionAgentResume(pi);
57
- pi.on("session_before_compact", async () => {
58
- await prepareCursorSessionForCompaction();
59
- });
60
- registerCursorRuntimeControls(pi);
61
- registerCursorNativeToolDisplay(pi);
62
- registerCursorQuestionTool(pi);
63
- registerCursorSkillTool(pi);
64
- registerCursorPiToolBridge(pi);
65
- registerCursorAgentsContextDedup(pi);
66
- registerCursorOverflowNormalization(pi);
67
- let fallbackIssue: CursorModelFallbackIssue | undefined;
68
- const models = await discoverModels({
69
- onFallback: (issue) => {
70
- fallbackIssue = issue;
71
- },
72
- });
58
+ const factoryClaim = claimCursorExtensionFactory();
59
+ if (factoryClaim.kind === "nested") return;
73
60
 
74
- if (fallbackIssue) {
75
- registerCursorFallbackIssueWarning(pi, fallbackIssue);
76
- }
61
+ try {
62
+ // Discover first. A discovery failure must not leave process-global
63
+ // registrars from a discarded extension load.
64
+ let fallbackIssue: CursorModelFallbackIssue | undefined;
65
+ const models = await discoverModels({
66
+ onFallback: (issue) => {
67
+ fallbackIssue = issue;
68
+ },
69
+ });
70
+
71
+ // Session cwd must register before other session_start listeners that depend on it.
72
+ registerCursorSessionScope(pi);
73
+ registerCursorSessionAgentLineage(pi);
74
+ registerCursorSessionAgentLifecycle(pi);
75
+ registerCursorSessionAgentResume(pi);
76
+ pi.on("session_before_compact", async () => {
77
+ await prepareCursorSessionForCompaction();
78
+ });
79
+ registerCursorRuntimeControls(pi);
80
+ registerCursorNativeToolDisplay(pi);
81
+ registerCursorQuestionTool(pi);
82
+ registerCursorSkillTool(pi);
83
+ registerCursorPiToolBridge(pi);
84
+ registerCursorAgentsContextDedup(pi);
85
+ registerCursorOverflowNormalization(pi);
77
86
 
78
- pi.registerCommand("cursor-refresh-models", {
79
- description: "Refresh the live Cursor model catalog without restarting pi",
80
- handler: async (_args, ctx) => {
81
- let refreshFallbackIssue: CursorModelFallbackIssue | undefined;
82
- const apiKey = resolveCursorApiKey(await ctx.modelRegistry.getApiKeyForProvider("cursor"));
83
- const refreshedModels = await discoverModels({
84
- apiKey,
85
- forceRefresh: true,
86
- onFallback: (issue) => {
87
- refreshFallbackIssue = issue;
88
- },
89
- });
90
- registerCursorProvider(pi, refreshedModels);
91
- if (!ctx.hasUI) return;
92
- if (refreshFallbackIssue) {
93
- ctx.ui.notify(`Cursor model catalog refresh did not use a live catalog: ${refreshFallbackIssue.message}`, "warning");
94
- } else {
95
- ctx.ui.notify(`Cursor model catalog refreshed with ${refreshedModels.length} model${refreshedModels.length === 1 ? "" : "s"}.`, "info");
96
- }
97
- },
98
- });
87
+ if (fallbackIssue) {
88
+ registerCursorFallbackIssueWarning(pi, fallbackIssue);
89
+ }
99
90
 
100
- registerCursorProvider(pi, models);
101
- // Register last so session_shutdown cleanup remains protected until other Cursor handlers finish.
102
- registerCursorSdkSessionProcessErrorGuard(pi);
91
+ pi.registerCommand("cursor-refresh-models", {
92
+ description: "Refresh the live Cursor model catalog without restarting pi",
93
+ handler: async (_args, ctx) => {
94
+ let refreshFallbackIssue: CursorModelFallbackIssue | undefined;
95
+ const apiKey = resolveCursorApiKey(await ctx.modelRegistry.getApiKeyForProvider("cursor"));
96
+ const refreshedModels = await discoverModels({
97
+ apiKey,
98
+ forceRefresh: true,
99
+ onFallback: (issue) => {
100
+ refreshFallbackIssue = issue;
101
+ },
102
+ });
103
+ registerCursorProvider(pi, refreshedModels);
104
+ if (!ctx.hasUI) return;
105
+ if (refreshFallbackIssue) {
106
+ ctx.ui.notify(`Cursor model catalog refresh did not use a live catalog: ${refreshFallbackIssue.message}`, "warning");
107
+ } else {
108
+ ctx.ui.notify(`Cursor model catalog refreshed with ${refreshedModels.length} model${refreshedModels.length === 1 ? "" : "s"}.`, "info");
109
+ }
110
+ },
111
+ });
112
+
113
+ registerCursorProvider(pi, models);
114
+ // Keep the process error guard near the end so earlier Cursor cleanup
115
+ // remains protected during session shutdown.
116
+ registerCursorSdkSessionProcessErrorGuard(pi);
117
+ // Register last so ownership remains protected until all other Cursor
118
+ // session_shutdown handlers finish.
119
+ registerCursorExtensionFactoryRelease(pi, factoryClaim);
120
+ } catch (error) {
121
+ releaseCursorExtensionFactory(factoryClaim.token);
122
+ throw error;
123
+ }
103
124
  }