pi-cursor-sdk 0.1.55 → 0.1.57

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 (88) hide show
  1. package/CHANGELOG.md +60 -2
  2. package/README.md +100 -28
  3. package/docs/cursor-live-smoke-checklist.md +7 -7
  4. package/docs/cursor-model-ux-spec.md +48 -41
  5. package/docs/cursor-native-tool-replay.md +4 -4
  6. package/docs/cursor-native-tool-visual-audit.md +1 -1
  7. package/docs/cursor-testing-lessons.md +20 -7
  8. package/docs/cursor-tool-surfaces.md +13 -2
  9. package/docs/platform-smoke-implementation.md +220 -0
  10. package/docs/platform-smoke.md +183 -247
  11. package/package.json +39 -7
  12. package/platform-smoke.config.mjs +5 -1
  13. package/scripts/cloud-runtime-smoke.d.mts +3 -0
  14. package/scripts/cloud-runtime-smoke.mjs +502 -0
  15. package/scripts/debug-provider-events.mjs +7 -2
  16. package/scripts/isolated-cursor-smoke.sh +4 -6
  17. package/scripts/lib/cursor-child-process.d.mts +1 -0
  18. package/scripts/lib/cursor-child-process.mjs +137 -7
  19. package/scripts/lib/local-resume-smoke-harness.mjs +543 -0
  20. package/scripts/local-resume-cleanup-smoke.mjs +108 -0
  21. package/scripts/local-resume-smoke.d.mts +1 -0
  22. package/scripts/local-resume-smoke.mjs +642 -0
  23. package/scripts/platform-smoke/artifact-anchored-extract.d.mts +10 -0
  24. package/scripts/platform-smoke/artifact-anchored-extract.mjs +111 -0
  25. package/scripts/platform-smoke/artifact-bundle-chunk.mjs +57 -0
  26. package/scripts/platform-smoke/artifact-bundle-contract.mjs +34 -0
  27. package/scripts/platform-smoke/artifact-fs-safety.mjs +311 -0
  28. package/scripts/platform-smoke/artifact-openat-extract.c +335 -0
  29. package/scripts/platform-smoke/artifact-secrets.mjs +155 -0
  30. package/scripts/platform-smoke/artifacts.mjs +293 -65
  31. package/scripts/platform-smoke/card-detect.mjs +16 -4
  32. package/scripts/platform-smoke/crabbox-runner.mjs +45 -3
  33. package/scripts/platform-smoke/doctor.mjs +20 -10
  34. package/scripts/platform-smoke/live-suite-runner.mjs +18 -57
  35. package/scripts/platform-smoke/local-resume-runner.mjs +252 -0
  36. package/scripts/platform-smoke/local-resume-suites.d.mts +15 -0
  37. package/scripts/platform-smoke/local-resume-suites.mjs +104 -0
  38. package/scripts/platform-smoke/scenarios.mjs +16 -2
  39. package/scripts/platform-smoke/target-runtime.mjs +206 -0
  40. package/scripts/platform-smoke/targets.mjs +33 -141
  41. package/scripts/platform-smoke/visual-evidence.mjs +6 -7
  42. package/scripts/platform-smoke/wrapped-line-match.mjs +9 -0
  43. package/scripts/platform-smoke.mjs +40 -27
  44. package/scripts/refresh-cursor-model-snapshots.mjs +18 -6
  45. package/scripts/steering-rpc-smoke.mjs +12 -2
  46. package/scripts/tmux-live-smoke.sh +3 -5
  47. package/shared/cursor-cloud-lifecycle-constants.d.mts +3 -0
  48. package/shared/cursor-cloud-lifecycle-constants.mjs +7 -0
  49. package/shared/cursor-sensitive-text.mjs +7 -1
  50. package/src/context.ts +5 -2
  51. package/src/cursor-agents-context-registration.ts +7 -0
  52. package/src/cursor-agents-context.ts +3 -1
  53. package/src/cursor-api-key.ts +15 -1
  54. package/src/cursor-bridge-contract.ts +3 -0
  55. package/src/cursor-cloud-lifecycle.ts +733 -0
  56. package/src/cursor-cloud-options.ts +206 -0
  57. package/src/cursor-cloud-reporting.ts +246 -0
  58. package/src/cursor-config.ts +659 -0
  59. package/src/cursor-display-only-trace.ts +14 -0
  60. package/src/cursor-display-text.ts +8 -2
  61. package/src/cursor-durable-fs.ts +49 -0
  62. package/src/cursor-fallback-models.generated.ts +2045 -485
  63. package/src/cursor-live-run-accounting.ts +7 -1
  64. package/src/cursor-live-run-coordinator.ts +1 -0
  65. package/src/cursor-pi-tool-bridge-run.ts +14 -4
  66. package/src/cursor-provider-errors.ts +31 -5
  67. package/src/cursor-provider-live-run-drain.ts +5 -0
  68. package/src/cursor-provider-run-finalizer.ts +37 -28
  69. package/src/cursor-provider-run-outcome.ts +7 -2
  70. package/src/cursor-provider-turn-finalize.ts +77 -7
  71. package/src/cursor-provider-turn-prepare.ts +228 -12
  72. package/src/cursor-provider-turn-runner.ts +42 -13
  73. package/src/cursor-provider-turn-send.ts +59 -16
  74. package/src/cursor-provider-turn-types.ts +44 -10
  75. package/src/cursor-runtime-state.ts +478 -0
  76. package/src/cursor-sdk-event-debug.ts +46 -6
  77. package/src/cursor-sdk-process-error-guard.ts +101 -30
  78. package/src/cursor-session-agent-cleanup.ts +328 -0
  79. package/src/cursor-session-agent-resume.ts +439 -0
  80. package/src/cursor-session-agent.ts +109 -13
  81. package/src/cursor-session-scope.ts +35 -2
  82. package/src/cursor-session-send-policy.ts +1 -1
  83. package/src/cursor-skill-tool.ts +30 -11
  84. package/src/cursor-state.ts +112 -69
  85. package/src/cursor-tool-manifest.ts +1 -1
  86. package/src/cursor-usage-accounting.ts +14 -4
  87. package/src/index.ts +11 -2
  88. package/src/model-discovery.ts +10 -56
@@ -1,11 +1,14 @@
1
- import type { ExtensionHandler, SessionStartEvent } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionHandler, SessionInfoChangedEvent, SessionStartEvent } from "@earendil-works/pi-coding-agent";
2
+ import { truncateCursorDisplayLine } from "./cursor-display-text.js";
2
3
 
3
4
  interface CursorSessionScopeExtensionApi {
4
5
  on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
6
+ on(event: "session_info_changed", handler: ExtensionHandler<SessionInfoChangedEvent>): void;
5
7
  }
6
8
 
7
9
  const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__";
8
10
  const EPHEMERAL_SESSION_SCOPE_PREFIX = "__ephemeral__:";
11
+ export const MAX_CURSOR_SESSION_NAME_LENGTH = 100;
9
12
 
10
13
  type CursorSessionScopeChangeHandler = (previousScopeKey: string) => Promise<void> | void;
11
14
 
@@ -13,6 +16,8 @@ const state = {
13
16
  sessionCwd: process.cwd(),
14
17
  sessionFile: undefined as string | undefined,
15
18
  sessionId: undefined as string | undefined,
19
+ sessionName: undefined as string | undefined,
20
+ projectTrusted: false,
16
21
  sessionGeneration: 0,
17
22
  };
18
23
 
@@ -50,10 +55,31 @@ export function getCursorSessionCwd(): string {
50
55
  return state.sessionCwd;
51
56
  }
52
57
 
53
- function setCursorSessionScope(cwd: string, sessionFile: string | undefined, sessionId?: string): void {
58
+ export function getCursorSessionProjectTrusted(): boolean {
59
+ return state.projectTrusted;
60
+ }
61
+
62
+ export function getCursorSessionName(): string | undefined {
63
+ return state.sessionName;
64
+ }
65
+
66
+ function normalizeCursorSessionName(name: string | undefined): string | undefined {
67
+ if (name === undefined) return undefined;
68
+ return truncateCursorDisplayLine(name, MAX_CURSOR_SESSION_NAME_LENGTH) || undefined;
69
+ }
70
+
71
+ function setCursorSessionScope(
72
+ cwd: string,
73
+ sessionFile: string | undefined,
74
+ sessionId?: string,
75
+ projectTrusted = false,
76
+ sessionName?: string,
77
+ ): void {
54
78
  state.sessionCwd = cwd;
55
79
  state.sessionFile = sessionFile;
56
80
  state.sessionId = sessionId;
81
+ state.sessionName = normalizeCursorSessionName(sessionName);
82
+ state.projectTrusted = projectTrusted;
57
83
  state.sessionGeneration = nextSessionGeneration;
58
84
  nextSessionGeneration += 1;
59
85
  scopeGenerations.set(getCursorSessionScopeKey(), state.sessionGeneration);
@@ -63,6 +89,8 @@ function resetCursorSessionScope(): void {
63
89
  state.sessionCwd = process.cwd();
64
90
  state.sessionFile = undefined;
65
91
  state.sessionId = undefined;
92
+ state.sessionName = undefined;
93
+ state.projectTrusted = false;
66
94
  state.sessionGeneration = 0;
67
95
  nextSessionGeneration = 1;
68
96
  scopeGenerations.clear();
@@ -80,11 +108,16 @@ export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi):
80
108
  ctx.cwd,
81
109
  ctx.sessionManager?.getSessionFile?.() ?? undefined,
82
110
  ctx.sessionManager?.getSessionId?.() ?? undefined,
111
+ ctx.isProjectTrusted?.() === true,
112
+ ctx.sessionManager?.getSessionName?.() ?? undefined,
83
113
  );
84
114
  if (previousScopeKey !== getCursorSessionScopeKey()) {
85
115
  await scopeChangeHandler?.(previousScopeKey);
86
116
  }
87
117
  });
118
+ pi.on("session_info_changed", (event) => {
119
+ state.sessionName = normalizeCursorSessionName(event.name);
120
+ });
88
121
  }
89
122
 
90
123
  export const __testUtils = {
@@ -13,7 +13,7 @@ export const MAX_COMPLETED_INCREMENTAL_SENDS_BEFORE_REBOOTSTRAP = 20;
13
13
 
14
14
  export type CursorSessionSendMode = "bootstrap" | "incremental";
15
15
 
16
- export type CursorSessionSendReason = "initial" | "context_divergence" | "incremental_threshold" | "incremental";
16
+ export type CursorSessionSendReason = "initial" | "context_divergence" | "incremental_threshold" | "process_resume" | "incremental";
17
17
 
18
18
  export interface CursorSessionSendPlan {
19
19
  mode: CursorSessionSendMode;
@@ -9,9 +9,11 @@ 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";
12
13
  import { isCursorModel } from "./cursor-model.js";
13
14
  import { registerCursorModelLifecycle, type CursorModelLifecycleExtensionApi } from "./cursor-model-lifecycle.js";
14
15
  import { resolveCursorPiToolBridgeEnabled } from "./cursor-pi-tool-bridge-env.js";
16
+ import { resolveEffectiveCursorConfigForContext } from "./cursor-runtime-state.js";
15
17
 
16
18
  export const CURSOR_ACTIVATE_SKILL_TOOL_NAME = "cursor_activate_skill";
17
19
  export const CURSOR_ACTIVATE_SKILL_MCP_NAME = "pi__cursor_activate_skill";
@@ -57,13 +59,24 @@ function getAvailableSkillNames(): string[] {
57
59
  return [...currentSkillsByName.keys()].sort();
58
60
  }
59
61
 
60
- function shouldExposeSkillTool(model: ExtensionContext["model"]): boolean {
61
- return isCursorModel(model) && resolveCursorPiToolBridgeEnabled() && currentSkillsByName.size > 0;
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";
62
67
  }
63
68
 
64
- function syncCursorSkillToolForModel(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">, model: ExtensionContext["model"]): void {
69
+ function shouldExposeSkillTool(model: ExtensionContext["model"], runtime: CursorRuntime): boolean {
70
+ return runtime === "local" && isCursorModel(model) && resolveCursorPiToolBridgeEnabled() && currentSkillsByName.size > 0;
71
+ }
72
+
73
+ function syncCursorSkillToolForModel(
74
+ pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools">,
75
+ model: ExtensionContext["model"],
76
+ runtime: CursorRuntime,
77
+ ): void {
65
78
  const activeToolNames = new Set(pi.getActiveTools());
66
- const shouldBeActive = !arePiToolsDisabled(pi) && shouldExposeSkillTool(model);
79
+ const shouldBeActive = !arePiToolsDisabled(pi) && shouldExposeSkillTool(model, runtime);
67
80
  const alreadyActive = activeToolNames.has(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
68
81
  if (shouldBeActive === alreadyActive) return;
69
82
  if (shouldBeActive) {
@@ -101,8 +114,10 @@ export function resolveCursorSkillSystemPrompt(
101
114
  systemPrompt: string,
102
115
  model: ExtensionContext["model"],
103
116
  systemPromptOptions?: BuildSystemPromptOptions,
117
+ runtime: CursorRuntime = "local",
104
118
  ): string {
105
119
  if (!isCursorModel(model)) return systemPrompt;
120
+ if (runtime === "cloud") return systemPrompt.replace(AVAILABLE_SKILLS_SECTION_PATTERN, "");
106
121
  const skills = getVisibleSkills(systemPromptOptions?.skills);
107
122
  if (skills.length === 0) return systemPrompt;
108
123
  const replacement = formatCursorSkillsForPrompt(skills);
@@ -216,9 +231,9 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
216
231
  },
217
232
  });
218
233
 
219
- const clearSkillsAndSync = (model: ExtensionContext["model"]): void => {
234
+ const clearSkillsAndSync = (model: ExtensionContext["model"], runtime: CursorRuntime = "local"): void => {
220
235
  setCurrentSkills([]);
221
- syncCursorSkillToolForModel(pi, model);
236
+ syncCursorSkillToolForModel(pi, model, runtime);
222
237
  };
223
238
 
224
239
  registerCursorModelLifecycle(pi, {
@@ -229,17 +244,21 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
229
244
  clearSkillsAndSync(event.model);
230
245
  },
231
246
  turnStart: (_event, ctx) => {
232
- if (!isCursorModel(ctx.model)) setCurrentSkills([]);
233
- syncCursorSkillToolForModel(pi, ctx.model);
247
+ const cursorModel = isCursorModel(ctx.model);
248
+ const runtime = resolveEffectiveRuntimeForSkillLifecycle(cursorModel, ctx);
249
+ if (!cursorModel || runtime === "cloud") setCurrentSkills([]);
250
+ syncCursorSkillToolForModel(pi, ctx.model, runtime);
234
251
  },
235
252
  beforeAgentStart: (event, ctx) => {
236
- if (isCursorModel(ctx.model)) {
253
+ const cursorModel = isCursorModel(ctx.model);
254
+ const runtime = resolveEffectiveRuntimeForSkillLifecycle(cursorModel, ctx);
255
+ if (cursorModel && runtime === "local") {
237
256
  setCurrentSkills(event.systemPromptOptions?.skills);
238
257
  } else {
239
258
  setCurrentSkills([]);
240
259
  }
241
- syncCursorSkillToolForModel(pi, ctx.model);
242
- const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions);
260
+ syncCursorSkillToolForModel(pi, ctx.model, runtime);
261
+ const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions, runtime);
243
262
  if (resolved === event.systemPrompt) return undefined;
244
263
  return { systemPrompt: resolved };
245
264
  },
@@ -1,13 +1,11 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
1
  import type { AgentModeOption } from "@cursor/sdk";
4
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
+ import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
6
3
  import {
7
4
  buildCursorToolManifestText,
8
5
  CURSOR_TOOL_MANIFEST_ENV,
9
6
  resolveCursorToolManifestEnabled,
10
7
  } from "./cursor-tool-manifest.js";
8
+ import { runCursorSessionAgentCleanupCommand } from "./cursor-session-agent-cleanup.js";
11
9
  import {
12
10
  buildCursorPiToolBridgeSnapshot,
13
11
  CURSOR_PI_TOOL_BRIDGE_ENV,
@@ -22,11 +20,39 @@ import { isCursorModel } from "./cursor-model.js";
22
20
  import { registerCursorModelLifecycle } from "./cursor-model-lifecycle.js";
23
21
  import { asRecord } from "./cursor-record-utils.js";
24
22
  import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
23
+ import { refreshSessionCursorAgentConfig } from "./cursor-session-agent.js";
25
24
  import { getCursorModelMetadata } from "./model-discovery.js";
25
+ import {
26
+ cursorFastDefaultsFromConfig,
27
+ getCursorSdkUserConfigPath,
28
+ loadCursorSdkUserConfig,
29
+ resolveCursorFastDefault,
30
+ saveCursorSdkUserConfig,
31
+ withCursorFastDefaults,
32
+ type CursorSdkConfig,
33
+ } from "./cursor-config.js";
34
+ import {
35
+ consumeCursorLocalForceOverride,
36
+ CURSOR_RUNTIME_ENTRY_TYPE,
37
+ formatCursorStatus,
38
+ getCursorCliConfig,
39
+ getCursorSessionConfig,
40
+ registerCursorCloudRuntimeControls,
41
+ resetCursorRuntimeStateForTests,
42
+ resolveCursorStatusRuntime,
43
+ restoreCursorCliState,
44
+ restoreSessionCursorRuntimeState,
45
+ type CursorRuntimeStateExtensionApi,
46
+ } from "./cursor-runtime-state.js";
47
+
48
+ export {
49
+ consumeCursorLocalForceOverride,
50
+ getCursorCliConfig,
51
+ getCursorSessionConfig,
52
+ } from "./cursor-runtime-state.js";
26
53
 
27
54
  const FAST_ENTRY_TYPE = "cursor-fast-state";
28
55
  const MODE_ENTRY_TYPE = "cursor-mode-state";
29
- const GLOBAL_CONFIG_FILE = "cursor-sdk.json";
30
56
 
31
57
  export type CursorAgentMode = AgentModeOption;
32
58
 
@@ -42,14 +68,10 @@ interface CursorModeEntryData {
42
68
  mode: AgentModeOption;
43
69
  }
44
70
 
45
- interface CursorGlobalConfig {
46
- fastDefaults?: Record<string, boolean>;
47
- }
48
-
49
71
  type CursorRuntimeControlsExtensionApi = Pick<
50
72
  ExtensionAPI,
51
73
  "appendEntry" | "getFlag" | "registerFlag" | "registerCommand" | "on" | "getActiveTools" | "getAllTools"
52
- >;
74
+ > & CursorRuntimeStateExtensionApi;
53
75
 
54
76
  type CursorCliModeState =
55
77
  | { kind: "unset" }
@@ -88,47 +110,22 @@ function isCursorModeEntryData(value: unknown): value is CursorModeEntryData {
88
110
  return isCursorAgentMode(asRecord(value)?.mode);
89
111
  }
90
112
 
91
- function parseCursorGlobalConfig(value: unknown): CursorGlobalConfig | undefined {
92
- const record = asRecord(value);
93
- if (!record) return undefined;
94
- const { fastDefaults } = record;
95
- if (fastDefaults === undefined) return {};
96
- const fastDefaultsRecord = asRecord(fastDefaults);
97
- if (!fastDefaultsRecord) return undefined;
98
- return {
99
- fastDefaults: Object.fromEntries(
100
- Object.entries(fastDefaultsRecord).filter((entry): entry is [string, boolean] => typeof entry[1] === "boolean"),
101
- ),
102
- };
103
- }
104
-
105
113
  function getConfigPath(): string {
106
- return join(getAgentDir(), GLOBAL_CONFIG_FILE);
114
+ return getCursorSdkUserConfigPath();
107
115
  }
108
116
 
109
117
  function loadGlobalFastPreferences(): Map<string, boolean> {
110
- const path = getConfigPath();
111
- if (!existsSync(path)) return new Map();
112
- try {
113
- const parsed = parseCursorGlobalConfig(JSON.parse(readFileSync(path, "utf-8")));
114
- return new Map(Object.entries(parsed?.fastDefaults ?? {}));
115
- } catch {
116
- return new Map();
117
- }
118
+ return cursorFastDefaultsFromConfig(loadCursorSdkUserConfig());
118
119
  }
119
120
 
120
121
  function saveGlobalFastPreferences(): void {
121
- const path = getConfigPath();
122
- mkdirSync(dirname(path), { recursive: true });
123
- const config: CursorGlobalConfig = {
124
- fastDefaults: Object.fromEntries([...globalFastPreferences.entries()].sort(([a], [b]) => a.localeCompare(b))),
125
- };
126
- writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
122
+ const currentConfig: CursorSdkConfig = loadCursorSdkUserConfig();
123
+ saveCursorSdkUserConfig(withCursorFastDefaults(currentConfig, globalFastPreferences));
127
124
  }
128
125
 
129
- function restoreSessionFastPreferences(ctx: { sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch"> }): void {
126
+ function restoreSessionFastPreferences(branch: readonly SessionEntry[]): void {
130
127
  sessionFastPreferences.clear();
131
- for (const entry of ctx.sessionManager.getBranch()) {
128
+ for (const entry of branch) {
132
129
  if (entry.type !== "custom" || entry.customType !== FAST_ENTRY_TYPE) continue;
133
130
  if (isCursorFastEntryData(entry.data)) {
134
131
  const modelId = getCursorFastEntryModelId(entry.data);
@@ -137,9 +134,9 @@ function restoreSessionFastPreferences(ctx: { sessionManager: Pick<ExtensionCont
137
134
  }
138
135
  }
139
136
 
140
- function restoreSessionCursorMode(ctx: { sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch"> }): void {
137
+ function restoreSessionCursorMode(branch: readonly SessionEntry[]): void {
141
138
  sessionCursorAgentMode = undefined;
142
- for (const entry of ctx.sessionManager.getBranch()) {
139
+ for (const entry of branch) {
143
140
  if (entry.type !== "custom" || entry.customType !== MODE_ENTRY_TYPE) continue;
144
141
  if (isCursorModeEntryData(entry.data)) {
145
142
  sessionCursorAgentMode = entry.data.mode;
@@ -147,6 +144,13 @@ function restoreSessionCursorMode(ctx: { sessionManager: Pick<ExtensionContext["
147
144
  }
148
145
  }
149
146
 
147
+ function restoreSessionCursorPreferences(ctx: { sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch"> }): void {
148
+ const branch = ctx.sessionManager.getBranch();
149
+ restoreSessionCursorRuntimeState(branch);
150
+ restoreSessionFastPreferences(branch);
151
+ restoreSessionCursorMode(branch);
152
+ }
153
+
150
154
  function getFastPreferenceModelId(metadata: NonNullable<ReturnType<typeof getCursorModelMetadata>>): string {
151
155
  return metadata.selectionModelId || metadata.baseModelId;
152
156
  }
@@ -155,23 +159,25 @@ function getVirtualFastBaseModelId(modelId: string): string {
155
159
  return modelId.replace(/:(?:fast|slow)$/, "");
156
160
  }
157
161
 
158
- function getStoredFastPreference(metadata: NonNullable<ReturnType<typeof getCursorModelMetadata>>): boolean | undefined {
162
+ function getMapFastPreference(
163
+ map: Map<string, boolean>,
164
+ metadata: NonNullable<ReturnType<typeof getCursorModelMetadata>>,
165
+ ): boolean | undefined {
159
166
  const preferenceModelId = getFastPreferenceModelId(metadata);
160
- return (
161
- sessionFastPreferences.get(preferenceModelId) ??
162
- (preferenceModelId !== metadata.baseModelId ? sessionFastPreferences.get(metadata.baseModelId) : undefined) ??
163
- globalFastPreferences.get(preferenceModelId) ??
164
- (preferenceModelId !== metadata.baseModelId ? globalFastPreferences.get(metadata.baseModelId) : undefined)
165
- );
167
+ return map.get(preferenceModelId) ?? (preferenceModelId !== metadata.baseModelId ? map.get(metadata.baseModelId) : undefined);
166
168
  }
167
169
 
168
170
  function getEffectiveFast(modelId: string): boolean | undefined {
169
171
  const metadata = getCursorModelMetadata(modelId);
170
172
  if (!metadata?.supportsFast) return undefined;
171
- if (cliForceNoFast) return false;
172
- if (cliForceFast) return true;
173
- if (metadata.fastOverride !== undefined) return metadata.fastOverride;
174
- return getStoredFastPreference(metadata) ?? metadata.defaultFast;
173
+ return resolveCursorFastDefault({
174
+ cliForceNoFast,
175
+ cliForceFast,
176
+ aliasOverride: metadata.fastOverride,
177
+ sessionValue: getMapFastPreference(sessionFastPreferences, metadata),
178
+ userValue: getMapFastPreference(globalFastPreferences, metadata),
179
+ modelDefault: metadata.defaultFast,
180
+ }).value;
175
181
  }
176
182
 
177
183
  function formatInvalidCursorMode(raw: string): string {
@@ -203,25 +209,24 @@ export function getCursorProviderAgentModeOrThrow(): AgentModeOption {
203
209
  return resolution.mode;
204
210
  }
205
211
 
206
- function formatCursorStatus(fast: boolean | undefined): string {
207
- const parts = [fast === true ? "cursor-fast:on" : fast === false ? "cursor-fast:off" : "cursor-fast:n/a"];
208
- const modeResolution = resolveCursorAgentMode();
209
- if (modeResolution.kind === "invalid") {
210
- parts.push("mode invalid");
211
- } else if (modeResolution.mode === "plan") {
212
- parts.push("plan");
213
- }
214
- return parts.join(" · ");
215
- }
212
+ type CursorStatusContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "isProjectTrusted">>;
216
213
 
217
- function updateCursorStatus(ctx: Pick<ExtensionContext, "model" | "ui">, model = ctx.model): void {
214
+ function updateCursorStatus(ctx: CursorStatusContext & Pick<ExtensionContext, "model" | "ui">, model = ctx.model): void {
218
215
  if (!model || !isCursorModel(model)) {
219
216
  ctx.ui.setStatus("cursor", undefined);
220
217
  return;
221
218
  }
222
219
  const metadata = getCursorModelMetadata(model.id);
223
- const fast = metadata?.supportsFast ? getEffectiveFast(model.id) : undefined;
224
- ctx.ui.setStatus("cursor", formatCursorStatus(fast));
220
+ const resolution = resolveCursorStatusRuntime(ctx);
221
+ const modeResolution = resolveCursorAgentMode();
222
+ const mode = modeResolution.kind === "invalid" ? "invalid" : modeResolution.mode;
223
+ if (resolution.kind === "invalid") {
224
+ ctx.ui.setStatus("cursor", formatCursorStatus("invalid", undefined, mode));
225
+ return;
226
+ }
227
+ const runtime = resolution.runtime.value;
228
+ const fast = runtime === "cloud" ? undefined : metadata?.supportsFast ? getEffectiveFast(model.id) : undefined;
229
+ ctx.ui.setStatus("cursor", formatCursorStatus(runtime, fast, mode));
225
230
  }
226
231
 
227
232
  function getCurrentCursorMetadata(ctx: Pick<ExtensionContext, "model">) {
@@ -345,6 +350,8 @@ export function getEffectiveFastForModelId(modelId: string): boolean | undefined
345
350
  }
346
351
 
347
352
  export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtensionApi): void {
353
+ registerCursorCloudRuntimeControls(pi, { refreshStatus: updateCursorStatus });
354
+
348
355
  pi.registerFlag("cursor-fast", {
349
356
  description: "Force Cursor fast mode for this run when the selected Cursor model supports it",
350
357
  type: "boolean",
@@ -411,6 +418,35 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
411
418
  },
412
419
  });
413
420
 
421
+ pi.registerCommand("cursor-local-resume-cleanup", {
422
+ description: "Dry-run or delete recorded superseded local Cursor SDK agents",
423
+ handler: async (args, ctx) => {
424
+ await runCursorSessionAgentCleanupCommand(pi, args, ctx);
425
+ },
426
+ });
427
+
428
+ pi.registerCommand("cursor-refresh-config", {
429
+ description: "Refresh filesystem Cursor config in the current pooled SDK agent",
430
+ handler: async (_args, ctx) => {
431
+ if (!isCursorModel(ctx.model)) {
432
+ ctx.ui.notify("Cursor config refresh is available only for Cursor models.", "info");
433
+ return;
434
+ }
435
+ try {
436
+ const result = await refreshSessionCursorAgentConfig();
437
+ const messages: Record<typeof result, string> = {
438
+ reloaded: "Cursor SDK agent config refreshed.",
439
+ "no-agent": "No Cursor SDK agent exists yet; config will load on the next Cursor run.",
440
+ busy: "Cursor SDK agent is still finalizing a run; retry /cursor-refresh-config after it finishes.",
441
+ unsupported: "Current Cursor SDK agent does not support config reload.",
442
+ };
443
+ ctx.ui.notify(messages[result], result === "reloaded" ? "info" : "warning");
444
+ } catch (error) {
445
+ ctx.ui.notify(`Failed to refresh Cursor config: ${error instanceof Error ? error.message : String(error)}`, "error");
446
+ }
447
+ },
448
+ });
449
+
414
450
  pi.registerCommand("cursor-mode", {
415
451
  description: "Set Cursor SDK conversation mode: agent or plan",
416
452
  handler: async (args, ctx) => {
@@ -452,13 +488,18 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
452
488
  },
453
489
  });
454
490
 
491
+ pi.on("session_tree", (_event, ctx) => {
492
+ restoreSessionCursorPreferences(ctx);
493
+ updateCursorStatus(ctx);
494
+ });
495
+
455
496
  registerCursorModelLifecycle(pi, {
456
497
  sessionStart: (_event, ctx) => {
457
498
  globalFastPreferences = loadGlobalFastPreferences();
458
499
  cliForceFast = pi.getFlag("cursor-fast") === true;
459
500
  cliForceNoFast = pi.getFlag("cursor-no-fast") === true;
460
- restoreSessionFastPreferences(ctx);
461
- restoreSessionCursorMode(ctx);
501
+ restoreCursorCliState(pi);
502
+ restoreSessionCursorPreferences(ctx);
462
503
  restoreCliCursorMode(pi.getFlag("cursor-mode"));
463
504
  },
464
505
  sync: (ctx) => {
@@ -471,12 +512,14 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
471
512
  function resetCursorModeStateForTests(): void {
472
513
  sessionCursorAgentMode = undefined;
473
514
  cliCursorModeState = { kind: "unset" };
515
+ resetCursorRuntimeStateForTests();
474
516
  invalidCursorModeNotifiedSessionScopeKeys.clear();
475
517
  }
476
518
 
477
519
  export const __testUtils = {
478
520
  FAST_ENTRY_TYPE,
479
521
  MODE_ENTRY_TYPE,
522
+ RUNTIME_ENTRY_TYPE: CURSOR_RUNTIME_ENTRY_TYPE,
480
523
  DEFAULT_CURSOR_AGENT_MODE,
481
524
  getConfigPath,
482
525
  loadGlobalFastPreferences,
@@ -4,7 +4,7 @@ import type { CursorPiToolBridgeSnapshot } from "./cursor-pi-tool-bridge-types.j
4
4
  export const CURSOR_TOOL_MANIFEST_ENV = "PI_CURSOR_TOOL_MANIFEST";
5
5
 
6
6
  /**
7
- * Representative @cursor/sdk@1.0.22 local-agent ToolType values; actual exposure can vary by run.
7
+ * Representative @cursor/sdk@1.0.23 local-agent ToolType values; actual exposure can vary by run.
8
8
  * See docs/cursor-native-tool-replay.md#sdk-tooltype-replay-matrix.
9
9
  */
10
10
  export const CURSOR_HOST_TOOL_MANIFEST_SUMMARY =
@@ -36,7 +36,7 @@ export function getCursorPromptOptions(model: Model<Api>): CursorUsagePromptOpti
36
36
 
37
37
  function getNonNegativeTokenCount(record: Record<string, unknown> | undefined, key: string): number | undefined {
38
38
  const value = getNumber(record, key);
39
- return value === undefined ? undefined : Math.max(0, Math.floor(value));
39
+ return value === undefined || value < 0 ? undefined : Math.floor(value);
40
40
  }
41
41
 
42
42
  export function readCursorSdkTurnUsage(value: unknown): CursorSdkTurnUsage | undefined {
@@ -84,12 +84,21 @@ export function estimateCursorContextTotalTokens(partial: AssistantMessage, mode
84
84
  return estimateCursorContextTokens(withAssistantMessage(context, partial), getCursorPromptOptions(model));
85
85
  }
86
86
 
87
+ export function isCursorSdkUsageSafeForPiMessage(turnUsage: CursorSdkTurnUsage, model: Model<Api>): boolean {
88
+ const counts = [turnUsage.inputTokens, turnUsage.outputTokens, turnUsage.cacheReadTokens, turnUsage.cacheWriteTokens];
89
+ return (
90
+ counts.every((count) => Number.isFinite(count) && count >= 0) &&
91
+ turnUsage.outputTokens <= model.maxTokens &&
92
+ turnUsage.inputTokens + turnUsage.outputTokens + turnUsage.cacheReadTokens + turnUsage.cacheWriteTokens <= model.contextWindow
93
+ );
94
+ }
95
+
87
96
  export function applyCursorSdkUsage(partial: AssistantMessage, turnUsage: CursorSdkTurnUsage): void {
88
97
  partial.usage.input = turnUsage.inputTokens;
89
98
  partial.usage.output = turnUsage.outputTokens;
90
99
  partial.usage.cacheRead = turnUsage.cacheReadTokens;
91
100
  partial.usage.cacheWrite = turnUsage.cacheWriteTokens;
92
- partial.usage.totalTokens = turnUsage.inputTokens + turnUsage.outputTokens;
101
+ partial.usage.totalTokens = turnUsage.inputTokens + turnUsage.outputTokens + turnUsage.cacheReadTokens + turnUsage.cacheWriteTokens;
93
102
  }
94
103
 
95
104
  export function applyCursorApproximateUsage(partial: AssistantMessage, model: Model<Api>, context: Context, sessionInputTokens: number): void {
@@ -111,8 +120,9 @@ export function applyCursorUsage(
111
120
  sessionInputTokens: number,
112
121
  sdkUsage?: { turn?: CursorSdkTurnUsage },
113
122
  ): void {
114
- if (sdkUsage?.turn) {
115
- applyCursorSdkUsage(partial, sdkUsage.turn);
123
+ const usage = sdkUsage?.turn;
124
+ if (usage && isCursorSdkUsageSafeForPiMessage(usage, model)) {
125
+ applyCursorSdkUsage(partial, usage);
116
126
  return;
117
127
  }
118
128
  applyCursorApproximateUsage(partial, model, context, sessionInputTokens);
package/src/index.ts CHANGED
@@ -7,16 +7,19 @@ import { registerCursorQuestionTool } from "./cursor-question-tool.js";
7
7
  import { registerCursorSkillTool } from "./cursor-skill-tool.js";
8
8
  import { registerCursorSessionScope } from "./cursor-session-scope.js";
9
9
  import { registerCursorSessionAgentLifecycle } from "./cursor-session-agent-lifecycle.js";
10
+ import { registerCursorSessionAgentResume } from "./cursor-session-agent-resume.js";
10
11
  import { streamCursorLazy } from "./cursor-provider-lazy.js";
11
- import { CURSOR_API_KEY_CONFIG_VALUE } from "./cursor-api-key.js";
12
+ import { CURSOR_API_KEY_CONFIG_VALUE, resolveCursorApiKey } from "./cursor-api-key.js";
12
13
  import { registerCursorFallbackIssueWarning } from "./cursor-fallback-warning.js";
13
14
  import { registerCursorAgentsContextDedup } from "./cursor-agents-context-registration.js";
14
15
  import { registerCursorOverflowNormalization } from "./cursor-provider-overflow.js";
16
+ import { registerCursorSdkSessionProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
15
17
 
16
18
  type CursorExtensionApi =
17
19
  & Pick<ExtensionAPI, "registerProvider" | "registerCommand" | "on">
18
20
  & Parameters<typeof registerCursorSessionScope>[0]
19
21
  & Parameters<typeof registerCursorSessionAgentLifecycle>[0]
22
+ & Parameters<typeof registerCursorSessionAgentResume>[0]
20
23
  & Parameters<typeof registerCursorRuntimeControls>[0]
21
24
  & Parameters<typeof registerCursorNativeToolDisplay>[0]
22
25
  & Parameters<typeof registerCursorQuestionTool>[0]
@@ -24,7 +27,8 @@ type CursorExtensionApi =
24
27
  & Parameters<typeof registerCursorPiToolBridge>[0]
25
28
  & Parameters<typeof registerCursorFallbackIssueWarning>[0]
26
29
  & Parameters<typeof registerCursorAgentsContextDedup>[0]
27
- & Parameters<typeof registerCursorOverflowNormalization>[0];
30
+ & Parameters<typeof registerCursorOverflowNormalization>[0]
31
+ & Parameters<typeof registerCursorSdkSessionProcessErrorGuard>[0];
28
32
 
29
33
  function createCursorProviderConfig(models: ProviderModelConfig[]): ProviderConfig {
30
34
  return {
@@ -45,6 +49,7 @@ export default async function (pi: CursorExtensionApi) {
45
49
  // Session cwd must register before other session_start listeners that depend on it.
46
50
  registerCursorSessionScope(pi);
47
51
  registerCursorSessionAgentLifecycle(pi);
52
+ registerCursorSessionAgentResume(pi);
48
53
  pi.on("session_before_compact", async () => {
49
54
  const { prepareCursorSessionForCompaction } = await import("./cursor-session-compaction-prep.js");
50
55
  await prepareCursorSessionForCompaction();
@@ -71,7 +76,9 @@ export default async function (pi: CursorExtensionApi) {
71
76
  description: "Refresh the live Cursor model catalog without restarting pi",
72
77
  handler: async (_args, ctx) => {
73
78
  let refreshFallbackIssue: CursorModelFallbackIssue | undefined;
79
+ const apiKey = resolveCursorApiKey(await ctx.modelRegistry.getApiKeyForProvider("cursor"));
74
80
  const refreshedModels = await discoverModels({
81
+ apiKey,
75
82
  forceRefresh: true,
76
83
  onFallback: (issue) => {
77
84
  refreshFallbackIssue = issue;
@@ -88,4 +95,6 @@ export default async function (pi: CursorExtensionApi) {
88
95
  });
89
96
 
90
97
  registerCursorProvider(pi, models);
98
+ // Register last so session_shutdown cleanup remains protected until other Cursor handlers finish.
99
+ registerCursorSdkSessionProcessErrorGuard(pi);
91
100
  }