pi-cursor-sdk 0.4.0 → 0.5.0

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 (125) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +65 -13
  3. package/dist/context-window-cache.js +0 -6
  4. package/dist/context.js +8 -0
  5. package/dist/cursor-cloud-lifecycle.js +122 -98
  6. package/dist/cursor-durable-fs.js +97 -3
  7. package/dist/cursor-fallback-models.generated.js +288 -1
  8. package/dist/cursor-fallback-warning.js +2 -2
  9. package/dist/cursor-http1.js +8 -8
  10. package/dist/cursor-live-run-coordinator.js +9 -13
  11. package/dist/cursor-native-replay-routing.js +1 -1
  12. package/dist/cursor-native-tool-display-registration.js +46 -22
  13. package/dist/cursor-native-tool-display-state.js +57 -22
  14. package/dist/cursor-native-tool-display-tools.js +5 -6
  15. package/dist/cursor-pi-tool-bridge-constants.js +23 -2
  16. package/dist/cursor-pi-tool-bridge-run.js +7 -3
  17. package/dist/cursor-pi-tool-bridge-snapshot.js +1 -1
  18. package/dist/cursor-pi-tool-bridge.js +31 -9
  19. package/dist/cursor-provider-binding.js +58 -0
  20. package/dist/cursor-provider-errors.js +28 -7
  21. package/dist/cursor-provider-lazy.js +23 -11
  22. package/dist/cursor-provider-live-run-drain.js +15 -12
  23. package/dist/cursor-provider-run-finalizer.js +19 -8
  24. package/dist/cursor-provider-turn-coordinator.js +1 -0
  25. package/dist/cursor-provider-turn-display-router.js +4 -0
  26. package/dist/cursor-provider-turn-emit.js +1 -0
  27. package/dist/cursor-provider-turn-finalize.js +29 -20
  28. package/dist/cursor-provider-turn-prepare.js +24 -11
  29. package/dist/cursor-provider-turn-runner.js +37 -11
  30. package/dist/cursor-provider-turn-send.js +22 -9
  31. package/dist/cursor-provider-turn-summary.js +89 -0
  32. package/dist/cursor-provider.js +9 -3
  33. package/dist/cursor-request-provenance.js +42 -0
  34. package/dist/cursor-ripgrep-path.js +14 -3
  35. package/dist/cursor-runtime-state.js +50 -43
  36. package/dist/cursor-sdk-billed-usage.js +54 -75
  37. package/dist/cursor-sdk-event-debug-session.js +5 -6
  38. package/dist/cursor-sdk-event-debug.js +9 -6
  39. package/dist/cursor-sdk-process-error-guard.js +8 -7
  40. package/dist/cursor-sdk-runtime.js +13 -0
  41. package/dist/cursor-session-agent-cleanup.js +2 -2
  42. package/dist/cursor-session-agent-lifecycle.js +18 -9
  43. package/dist/cursor-session-agent-lineage.js +28 -12
  44. package/dist/cursor-session-agent-resume.js +51 -35
  45. package/dist/cursor-session-agent.js +9 -9
  46. package/dist/cursor-session-compaction-prep.js +2 -7
  47. package/dist/cursor-session-scope.js +71 -39
  48. package/dist/cursor-session-settings.js +61 -0
  49. package/dist/cursor-skill-tool.js +32 -49
  50. package/dist/cursor-state.js +72 -62
  51. package/dist/cursor-usage-accounting.js +13 -70
  52. package/dist/cursor-usage-command.js +89 -0
  53. package/dist/cursor-usage-ledger.js +512 -0
  54. package/dist/index.js +11 -19
  55. package/dist/model-discovery.js +11 -19
  56. package/docs/cursor-dogfood-checklist.md +3 -3
  57. package/docs/cursor-live-smoke-checklist.md +15 -15
  58. package/docs/cursor-model-ux-spec.md +59 -18
  59. package/docs/cursor-native-tool-replay.md +5 -3
  60. package/docs/cursor-native-tool-visual-audit.md +8 -8
  61. package/docs/cursor-testing-lessons.md +47 -23
  62. package/docs/cursor-tool-surfaces.md +1 -1
  63. package/docs/platform-smoke-implementation.md +10 -6
  64. package/docs/platform-smoke.md +35 -46
  65. package/package.json +6 -6
  66. package/scripts/cloud-runtime-smoke.mjs +4 -2
  67. package/scripts/local-resume-smoke.mjs +1 -1
  68. package/scripts/platform-smoke/card-detect.mjs +2 -8
  69. package/scripts/platform-smoke/targets.mjs +8 -15
  70. package/scripts/platform-smoke/visual-evidence.mjs +7 -2
  71. package/scripts/tmux-live-smoke.sh +57 -90
  72. package/src/context-window-cache.ts +0 -6
  73. package/src/context.ts +8 -0
  74. package/src/cursor-cloud-lifecycle.ts +124 -98
  75. package/src/cursor-durable-fs.ts +65 -3
  76. package/src/cursor-fallback-models.generated.ts +288 -1
  77. package/src/cursor-fallback-warning.ts +2 -2
  78. package/src/cursor-http1.ts +8 -8
  79. package/src/cursor-live-run-coordinator.ts +12 -13
  80. package/src/cursor-native-replay-routing.ts +2 -1
  81. package/src/cursor-native-tool-display-registration.ts +48 -24
  82. package/src/cursor-native-tool-display-state.ts +65 -21
  83. package/src/cursor-native-tool-display-tools.ts +7 -4
  84. package/src/cursor-pi-tool-bridge-constants.ts +27 -2
  85. package/src/cursor-pi-tool-bridge-run.ts +11 -3
  86. package/src/cursor-pi-tool-bridge-snapshot.ts +1 -1
  87. package/src/cursor-pi-tool-bridge.ts +26 -9
  88. package/src/cursor-provider-binding.ts +62 -0
  89. package/src/cursor-provider-errors.ts +27 -7
  90. package/src/cursor-provider-lazy.ts +21 -15
  91. package/src/cursor-provider-live-run-drain.ts +20 -13
  92. package/src/cursor-provider-run-finalizer.ts +24 -15
  93. package/src/cursor-provider-turn-coordinator.ts +2 -0
  94. package/src/cursor-provider-turn-display-router.ts +5 -0
  95. package/src/cursor-provider-turn-emit.ts +3 -1
  96. package/src/cursor-provider-turn-finalize.ts +38 -23
  97. package/src/cursor-provider-turn-prepare.ts +23 -14
  98. package/src/cursor-provider-turn-runner.ts +40 -13
  99. package/src/cursor-provider-turn-send.ts +23 -15
  100. package/src/cursor-provider-turn-summary.ts +94 -0
  101. package/src/cursor-provider-turn-types.ts +37 -4
  102. package/src/cursor-provider.ts +25 -3
  103. package/src/cursor-request-provenance.ts +64 -0
  104. package/src/cursor-ripgrep-path.ts +15 -5
  105. package/src/cursor-runtime-state.ts +53 -48
  106. package/src/cursor-sdk-billed-usage.ts +54 -102
  107. package/src/cursor-sdk-event-debug-session.ts +5 -5
  108. package/src/cursor-sdk-event-debug.ts +10 -6
  109. package/src/cursor-sdk-process-error-guard.ts +7 -7
  110. package/src/cursor-sdk-runtime.ts +14 -0
  111. package/src/cursor-session-agent-cleanup.ts +2 -2
  112. package/src/cursor-session-agent-lifecycle.ts +14 -9
  113. package/src/cursor-session-agent-lineage.ts +26 -12
  114. package/src/cursor-session-agent-resume.ts +49 -36
  115. package/src/cursor-session-agent.ts +13 -10
  116. package/src/cursor-session-compaction-prep.ts +2 -7
  117. package/src/cursor-session-scope.ts +87 -39
  118. package/src/cursor-session-settings.ts +72 -0
  119. package/src/cursor-skill-tool.ts +37 -61
  120. package/src/cursor-state.ts +74 -67
  121. package/src/cursor-usage-accounting.ts +17 -82
  122. package/src/cursor-usage-command.ts +76 -0
  123. package/src/cursor-usage-ledger.ts +500 -0
  124. package/src/index.ts +14 -23
  125. package/src/model-discovery.ts +14 -22
@@ -29,15 +29,13 @@ type CursorActivateSkillParams = {
29
29
  };
30
30
 
31
31
  interface CursorSkillActivationDetails {
32
- name?: string;
33
- filePath?: string;
34
- baseDir?: string;
32
+ name: string;
33
+ filePath: string;
34
+ baseDir: string;
35
35
  resources: string[];
36
36
  availableSkillNames: string[];
37
37
  }
38
38
 
39
- let currentSkillsByName = new Map<string, Skill>();
40
-
41
39
  function escapeXml(value: string): string {
42
40
  return value
43
41
  .replace(/&/g, "&amp;")
@@ -51,14 +49,6 @@ function getVisibleSkills(skills: readonly Skill[] | undefined): Skill[] {
51
49
  return (skills ?? []).filter((skill) => !skill.disableModelInvocation);
52
50
  }
53
51
 
54
- function setCurrentSkills(skills: readonly Skill[] | undefined): void {
55
- currentSkillsByName = new Map(getVisibleSkills(skills).map((skill) => [skill.name, skill]));
56
- }
57
-
58
- function getAvailableSkillNames(): string[] {
59
- return [...currentSkillsByName.keys()].sort();
60
- }
61
-
62
52
  function resolveEffectiveRuntimeForSkillLifecycle(
63
53
  cursorModel: boolean,
64
54
  ctx: Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "isProjectTrusted">>,
@@ -66,27 +56,6 @@ function resolveEffectiveRuntimeForSkillLifecycle(
66
56
  return cursorModel ? resolveEffectiveCursorConfigForContext(ctx).runtime.value : "local";
67
57
  }
68
58
 
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 {
78
- const activeToolNames = new Set(pi.getActiveTools());
79
- const shouldBeActive = !arePiToolsDisabled(pi) && shouldExposeSkillTool(model, runtime);
80
- const alreadyActive = activeToolNames.has(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
81
- if (shouldBeActive === alreadyActive) return;
82
- if (shouldBeActive) {
83
- activeToolNames.add(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
84
- } else {
85
- activeToolNames.delete(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
86
- }
87
- pi.setActiveTools([...activeToolNames]);
88
- }
89
-
90
59
  export function formatCursorSkillsForPrompt(skills: readonly Skill[]): string {
91
60
  const visibleSkills = getVisibleSkills(skills);
92
61
  if (visibleSkills.length === 0) return "";
@@ -157,16 +126,6 @@ async function listSkillResourcePaths(baseDir: string): Promise<string[]> {
157
126
  return resources;
158
127
  }
159
128
 
160
- function buildActivationDetails(skill: Skill | undefined, resources: string[] = []): CursorSkillActivationDetails {
161
- return {
162
- name: skill?.name,
163
- filePath: skill?.filePath,
164
- baseDir: skill ? dirname(skill.filePath) : undefined,
165
- resources,
166
- availableSkillNames: getAvailableSkillNames(),
167
- };
168
- }
169
-
170
129
  function formatSkillResources(resources: readonly string[]): string {
171
130
  if (resources.length === 0) return "<skill_resources />";
172
131
  return [
@@ -190,18 +149,37 @@ function wrapSkillContent(skill: Skill, content: string, resources: readonly str
190
149
  }
191
150
 
192
151
  export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
152
+ let currentSkillsByName = new Map<string, Skill>();
153
+
154
+ function setCurrentSkills(skills: readonly Skill[] | undefined): void {
155
+ currentSkillsByName = new Map(getVisibleSkills(skills).map((skill) => [skill.name, skill]));
156
+ }
157
+
158
+ function getAvailableSkillNames(): string[] {
159
+ return [...currentSkillsByName.keys()].sort();
160
+ }
161
+
162
+ function syncCursorSkillToolForModel(model: ExtensionContext["model"], runtime: CursorRuntime): void {
163
+ const activeToolNames = new Set(pi.getActiveTools());
164
+ const shouldBeActive = !arePiToolsDisabled(pi) && runtime === "local" && isCursorModel(model)
165
+ && resolveCursorPiToolBridgeEnabled() && currentSkillsByName.size > 0;
166
+ const alreadyActive = activeToolNames.has(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
167
+ if (shouldBeActive === alreadyActive) return;
168
+ if (shouldBeActive) {
169
+ activeToolNames.add(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
170
+ } else {
171
+ activeToolNames.delete(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
172
+ }
173
+ pi.setActiveTools([...activeToolNames]);
174
+ }
175
+
193
176
  pi.registerTool({
194
177
  name: CURSOR_ACTIVATE_SKILL_TOOL_NAME,
195
178
  label: "Cursor skill",
196
179
  description: "Load full pi Agent Skill instructions for Cursor. Use with a skill name from the current <available_skills> catalog before applying that skill.",
197
- promptSnippet: "Load full pi Agent Skill instructions for a listed skill before Cursor applies that skill",
198
180
  parameters: Type.Object({
199
181
  name: Type.String({ description: "Skill name from the current <available_skills> catalog" }),
200
182
  }),
201
- promptGuidelines: [
202
- `Use ${CURSOR_ACTIVATE_SKILL_TOOL_NAME} only for skill names listed in the current <available_skills> catalog.`,
203
- "After loading a skill, follow its instructions and resolve relative skill paths against the returned skill directory.",
204
- ],
205
183
  async execute(_toolCallId, params) {
206
184
  const requestedName = (params as CursorActivateSkillParams).name?.trim();
207
185
  if (!requestedName) {
@@ -221,7 +199,13 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
221
199
  ]);
222
200
  return {
223
201
  content: [{ type: "text" as const, text: wrapSkillContent(skill, content, resources) }],
224
- details: buildActivationDetails(skill, resources),
202
+ details: {
203
+ name: skill.name,
204
+ filePath: skill.filePath,
205
+ baseDir: dirname(skill.filePath),
206
+ resources,
207
+ availableSkillNames: getAvailableSkillNames(),
208
+ } satisfies CursorSkillActivationDetails,
225
209
  };
226
210
  } catch (error) {
227
211
  throw new Error(
@@ -233,7 +217,7 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
233
217
 
234
218
  const clearSkillsAndSync = (model: ExtensionContext["model"], runtime: CursorRuntime = "local"): void => {
235
219
  setCurrentSkills([]);
236
- syncCursorSkillToolForModel(pi, model, runtime);
220
+ syncCursorSkillToolForModel(model, runtime);
237
221
  };
238
222
 
239
223
  registerCursorModelLifecycle(pi, {
@@ -247,7 +231,7 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
247
231
  const cursorModel = isCursorModel(ctx.model);
248
232
  const runtime = resolveEffectiveRuntimeForSkillLifecycle(cursorModel, ctx);
249
233
  if (!cursorModel || runtime === "cloud") setCurrentSkills([]);
250
- syncCursorSkillToolForModel(pi, ctx.model, runtime);
234
+ syncCursorSkillToolForModel(ctx.model, runtime);
251
235
  },
252
236
  beforeAgentStart: (event, ctx) => {
253
237
  const cursorModel = isCursorModel(ctx.model);
@@ -257,18 +241,10 @@ export function registerCursorSkillTool(pi: CursorSkillToolExtensionApi): void {
257
241
  } else {
258
242
  setCurrentSkills([]);
259
243
  }
260
- syncCursorSkillToolForModel(pi, ctx.model, runtime);
244
+ syncCursorSkillToolForModel(ctx.model, runtime);
261
245
  const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions, runtime);
262
246
  if (resolved === event.systemPrompt) return undefined;
263
247
  return { systemPrompt: resolved };
264
248
  },
265
249
  });
266
250
  }
267
-
268
- export const __testUtils = {
269
- AVAILABLE_SKILLS_SECTION_PATTERN,
270
- buildActivationDetails,
271
- setCurrentSkills,
272
- listSkillResourcePaths,
273
- wrapSkillContent,
274
- };
@@ -27,7 +27,8 @@ import {
27
27
  import { isCursorModel } from "./cursor-model.js";
28
28
  import { registerCursorModelLifecycle } from "./cursor-model-lifecycle.js";
29
29
  import { asRecord } from "./cursor-record-utils.js";
30
- import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
30
+ import { getCursorSessionScopeSnapshot, getCursorSessionScopeKey } from "./cursor-session-scope.js";
31
+ import { cursorSettingsScopeForContext, getCursorSessionSettings, registerCursorSessionSettings, resetCursorSessionSettingsForTests } from "./cursor-session-settings.js";
31
32
  import { refreshSessionCursorAgentConfig } from "./cursor-session-agent.js";
32
33
  import { getCursorModelMetadata } from "./model-discovery.js";
33
34
  import {
@@ -80,18 +81,8 @@ type CursorRuntimeControlsExtensionApi = Pick<
80
81
  "appendEntry" | "getFlag" | "registerFlag" | "registerCommand" | "on" | "getActiveTools" | "getAllTools"
81
82
  > & CursorRuntimeStateExtensionApi;
82
83
 
83
- type CursorCliModeState =
84
- | { kind: "unset" }
85
- | { kind: "valid"; mode: AgentModeOption }
86
- | { kind: "invalid"; raw: string; message: string };
87
-
88
- const sessionFastPreferences = new Map<string, boolean>();
89
84
  const authoritativeGlobalFastPreferenceIds = new Set<string>();
90
85
  let globalFastPreferences = new Map<string, boolean>();
91
- let cliForceFast = false;
92
- let cliForceNoFast = false;
93
- let sessionCursorAgentMode: AgentModeOption | undefined;
94
- let cliCursorModeState: CursorCliModeState = { kind: "unset" };
95
86
  const invalidCursorModeNotifiedSessionScopeKeys = new Set<string>();
96
87
 
97
88
  export function isCursorAgentMode(value: unknown): value is AgentModeOption {
@@ -152,7 +143,8 @@ function saveGlobalCursorHttp1Enabled(enabled: boolean): void {
152
143
  );
153
144
  }
154
145
 
155
- function restoreSessionFastPreferences(branch: readonly SessionEntry[]): void {
146
+ function restoreSessionFastPreferences(branch: readonly SessionEntry[], scopeKey?: string): void {
147
+ const sessionFastPreferences = getCursorSessionSettings(scopeKey).fast;
156
148
  sessionFastPreferences.clear();
157
149
  for (const entry of branch) {
158
150
  if (entry.type !== "custom" || entry.customType !== FAST_ENTRY_TYPE) continue;
@@ -163,30 +155,32 @@ function restoreSessionFastPreferences(branch: readonly SessionEntry[]): void {
163
155
  }
164
156
  }
165
157
 
166
- function restoreSessionCursorMode(branch: readonly SessionEntry[]): void {
167
- sessionCursorAgentMode = undefined;
158
+ function restoreSessionCursorMode(branch: readonly SessionEntry[], scopeKey?: string): void {
159
+ const state = getCursorSessionSettings(scopeKey);
160
+ state.mode = undefined;
168
161
  for (const entry of branch) {
169
162
  if (entry.type !== "custom" || entry.customType !== MODE_ENTRY_TYPE) continue;
170
163
  if (isCursorModeEntryData(entry.data)) {
171
- sessionCursorAgentMode = entry.data.mode;
164
+ state.mode = entry.data.mode;
172
165
  }
173
166
  }
174
167
  }
175
168
 
176
- function restoreSessionCursorPreferences(ctx: { sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch"> }): void {
169
+ function restoreSessionCursorPreferences(ctx: Pick<ExtensionContext, "sessionManager">): void {
177
170
  const branch = ctx.sessionManager.getBranch();
178
- restoreSessionCursorRuntimeState(branch);
179
- restoreSessionFastPreferences(branch);
180
- restoreSessionCursorMode(branch);
181
- restoreSessionCursorHttp1(branch);
171
+ const scopeKey = cursorSettingsScopeForContext(ctx);
172
+ restoreSessionCursorRuntimeState(branch, scopeKey);
173
+ restoreSessionFastPreferences(branch, scopeKey);
174
+ restoreSessionCursorMode(branch, scopeKey);
175
+ restoreSessionCursorHttp1(branch, scopeKey);
182
176
  }
183
177
 
184
- function restoreSessionCursorHttp1(branch: readonly SessionEntry[]): void {
185
- setStoredCursorHttp1Enabled(undefined);
178
+ function restoreSessionCursorHttp1(branch: readonly SessionEntry[], scopeKey?: string): void {
179
+ setStoredCursorHttp1Enabled(undefined, scopeKey);
186
180
  for (const entry of branch) {
187
181
  if (entry.type !== "custom" || entry.customType !== CURSOR_HTTP1_ENTRY_TYPE) continue;
188
182
  if (isCursorHttp1EntryData(entry.data)) {
189
- setStoredCursorHttp1Enabled(entry.data.enabled);
183
+ setStoredCursorHttp1Enabled(entry.data.enabled, scopeKey);
190
184
  }
191
185
  }
192
186
  }
@@ -207,7 +201,8 @@ function getMapFastPreference(
207
201
  return map.get(preferenceModelId) ?? (preferenceModelId !== metadata.baseModelId ? map.get(metadata.baseModelId) : undefined);
208
202
  }
209
203
 
210
- function getEffectiveFast(modelId: string): boolean | undefined {
204
+ function getEffectiveFast(modelId: string, scopeKey?: string): boolean | undefined {
205
+ const { forceNoFast: cliForceNoFast, forceFast: cliForceFast } = getCursorSessionSettings(scopeKey).cli;
211
206
  const metadata = getCursorModelMetadata(modelId);
212
207
  if (!metadata?.supportsFast) return undefined;
213
208
  return resolveCursorFastDefault({
@@ -216,7 +211,7 @@ function getEffectiveFast(modelId: string): boolean | undefined {
216
211
  aliasOverride: metadata.fastOverride,
217
212
  sessionValue: authoritativeGlobalFastPreferenceIds.has(getFastPreferenceModelId(metadata))
218
213
  ? undefined
219
- : getMapFastPreference(sessionFastPreferences, metadata),
214
+ : getMapFastPreference(getCursorSessionSettings(scopeKey).fast, metadata),
220
215
  userValue: getMapFastPreference(globalFastPreferences, metadata),
221
216
  modelDefault: metadata.defaultFast,
222
217
  }).value;
@@ -230,28 +225,29 @@ export type CursorAgentModeResolution =
230
225
  | { kind: "valid"; mode: AgentModeOption }
231
226
  | { kind: "invalid"; raw: string; message: string };
232
227
 
233
- export function getStoredCursorAgentMode(): AgentModeOption {
234
- return sessionCursorAgentMode ?? DEFAULT_CURSOR_AGENT_MODE;
228
+ export function getStoredCursorAgentMode(scopeKey?: string): AgentModeOption {
229
+ return getCursorSessionSettings(scopeKey).mode ?? DEFAULT_CURSOR_AGENT_MODE;
235
230
  }
236
231
 
237
- export function resolveCursorAgentMode(): CursorAgentModeResolution {
232
+ export function resolveCursorAgentMode(scopeKey?: string): CursorAgentModeResolution {
233
+ const cliCursorModeState = getCursorSessionSettings(scopeKey).cli.mode;
238
234
  switch (cliCursorModeState.kind) {
239
235
  case "valid":
240
236
  return { kind: "valid", mode: cliCursorModeState.mode };
241
237
  case "invalid":
242
238
  return { kind: "invalid", raw: cliCursorModeState.raw, message: cliCursorModeState.message };
243
239
  case "unset":
244
- return { kind: "valid", mode: getStoredCursorAgentMode() };
240
+ return { kind: "valid", mode: getStoredCursorAgentMode(scopeKey) };
245
241
  }
246
242
  }
247
243
 
248
- export function getCursorProviderAgentModeOrThrow(): AgentModeOption {
249
- const resolution = resolveCursorAgentMode();
244
+ export function getCursorProviderAgentModeOrThrow(scopeKey?: string): AgentModeOption {
245
+ const resolution = resolveCursorAgentMode(scopeKey);
250
246
  if (resolution.kind === "invalid") throw new Error(resolution.message);
251
247
  return resolution.mode;
252
248
  }
253
249
 
254
- type CursorStatusContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "isProjectTrusted">>;
250
+ type CursorStatusContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "isProjectTrusted" | "sessionManager">>;
255
251
 
256
252
  function updateCursorStatus(ctx: CursorStatusContext & Pick<ExtensionContext, "model" | "ui">, model = ctx.model): void {
257
253
  if (!model || !isCursorModel(model)) {
@@ -260,14 +256,14 @@ function updateCursorStatus(ctx: CursorStatusContext & Pick<ExtensionContext, "m
260
256
  }
261
257
  const metadata = getCursorModelMetadata(model.id);
262
258
  const resolution = resolveCursorStatusRuntime(ctx);
263
- const modeResolution = resolveCursorAgentMode();
259
+ const modeResolution = resolveCursorAgentMode(cursorSettingsScopeForContext(ctx));
264
260
  const mode = modeResolution.kind === "invalid" ? "invalid" : modeResolution.mode;
265
261
  if (resolution.kind === "invalid") {
266
262
  ctx.ui.setStatus("cursor", formatCursorStatus("invalid", undefined, mode));
267
263
  return;
268
264
  }
269
265
  const runtime = resolution.runtime.value;
270
- const fast = runtime === "cloud" ? undefined : metadata?.supportsFast ? getEffectiveFast(model.id) : undefined;
266
+ const fast = runtime === "cloud" ? undefined : metadata?.supportsFast ? getEffectiveFast(model.id, cursorSettingsScopeForContext(ctx)) : undefined;
271
267
  ctx.ui.setStatus(
272
268
  "cursor",
273
269
  formatCursorStatus(runtime, fast, mode, resolution.useHttp1ForAgent.value),
@@ -292,7 +288,9 @@ function persistFastPreference(
292
288
  pi: Pick<ExtensionAPI, "appendEntry">,
293
289
  modelId: string,
294
290
  fast: boolean,
291
+ scopeKey: string | undefined,
295
292
  ): unknown | undefined {
293
+ const sessionFastPreferences = getCursorSessionSettings(scopeKey).fast;
296
294
  const previousSession = sessionFastPreferences.get(modelId);
297
295
  const previousGlobal = globalFastPreferences.get(modelId);
298
296
  sessionFastPreferences.set(modelId, fast);
@@ -314,13 +312,14 @@ function persistFastPreference(
314
312
  }
315
313
  }
316
314
 
317
- function persistCursorModePreference(pi: Pick<ExtensionAPI, "appendEntry">, mode: AgentModeOption): void {
318
- const previousMode = sessionCursorAgentMode;
319
- sessionCursorAgentMode = mode;
315
+ function persistCursorModePreference(pi: Pick<ExtensionAPI, "appendEntry">, mode: AgentModeOption, scopeKey: string | undefined): void {
316
+ const state = getCursorSessionSettings(scopeKey);
317
+ const previousMode = state.mode;
318
+ state.mode = mode;
320
319
  try {
321
320
  pi.appendEntry<CursorModeEntryData>(MODE_ENTRY_TYPE, { mode });
322
321
  } catch (error) {
323
- sessionCursorAgentMode = previousMode;
322
+ state.mode = previousMode;
324
323
  throw error;
325
324
  }
326
325
  }
@@ -328,13 +327,14 @@ function persistCursorModePreference(pi: Pick<ExtensionAPI, "appendEntry">, mode
328
327
  function persistCursorHttp1Preference(
329
328
  pi: Pick<ExtensionAPI, "appendEntry">,
330
329
  enabled: boolean,
330
+ scopeKey: string | undefined,
331
331
  ): unknown | undefined {
332
- const previousSession = getStoredCursorHttp1Enabled();
333
- setStoredCursorHttp1Enabled(enabled);
332
+ const previousSession = getStoredCursorHttp1Enabled(scopeKey);
333
+ setStoredCursorHttp1Enabled(enabled, scopeKey);
334
334
  try {
335
335
  saveGlobalCursorHttp1Enabled(enabled);
336
336
  } catch (error) {
337
- setStoredCursorHttp1Enabled(previousSession);
337
+ setStoredCursorHttp1Enabled(previousSession, scopeKey);
338
338
  throw error;
339
339
  }
340
340
  try {
@@ -347,23 +347,24 @@ function persistCursorHttp1Preference(
347
347
  }
348
348
  }
349
349
 
350
- function restoreCliCursorMode(raw: boolean | string | undefined): void {
351
- cliCursorModeState = { kind: "unset" };
350
+ function restoreCliCursorMode(raw: boolean | string | undefined, scopeKey?: string): void {
351
+ const cli = getCursorSessionSettings(scopeKey).cli;
352
+ cli.mode = { kind: "unset" };
352
353
  if (raw === undefined || raw === "" || raw === false) return;
353
354
  const parsed = parseCursorAgentMode(raw);
354
355
  if (parsed) {
355
- cliCursorModeState = { kind: "valid", mode: parsed };
356
+ cli.mode = { kind: "valid", mode: parsed };
356
357
  return;
357
358
  }
358
359
  const rawText = String(raw);
359
360
  const message = formatInvalidCursorMode(rawText);
360
- cliCursorModeState = { kind: "invalid", raw: rawText, message };
361
+ cli.mode = { kind: "invalid", raw: rawText, message };
361
362
  }
362
363
 
363
- function notifyInvalidCursorModeIfCursorActive(ctx: Pick<ExtensionContext, "hasUI" | "mode" | "ui">): void {
364
- const modeResolution = resolveCursorAgentMode();
364
+ function notifyInvalidCursorModeIfCursorActive(ctx: Pick<ExtensionContext, "hasUI" | "mode" | "ui" | "sessionManager">): void {
365
+ const modeResolution = resolveCursorAgentMode(cursorSettingsScopeForContext(ctx));
365
366
  if (modeResolution.kind !== "invalid" || !ctx.hasUI || ctx.mode !== "tui") return;
366
- const scopeKey = getCursorSessionScopeKey();
367
+ const scopeKey = cursorSettingsScopeForContext(ctx) ?? getCursorSessionScopeKey();
367
368
  if (invalidCursorModeNotifiedSessionScopeKeys.has(scopeKey)) return;
368
369
  invalidCursorModeNotifiedSessionScopeKeys.add(scopeKey);
369
370
  ctx.ui.notify(modeResolution.message, "error");
@@ -414,11 +415,12 @@ function emitCursorToolsDebugReport(
414
415
  console.log(report);
415
416
  }
416
417
 
417
- export function getEffectiveFastForModelId(modelId: string): boolean | undefined {
418
- return getEffectiveFast(modelId);
418
+ export function getEffectiveFastForModelId(modelId: string, scopeKey?: string): boolean | undefined {
419
+ return getEffectiveFast(modelId, scopeKey);
419
420
  }
420
421
 
421
422
  export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtensionApi): void {
423
+ registerCursorSessionSettings(pi);
422
424
  registerCursorCloudRuntimeControls(pi, { refreshStatus: updateCursorStatus });
423
425
 
424
426
  pi.registerFlag("cursor-fast", {
@@ -448,6 +450,7 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
448
450
  ctx.ui.notify(`Fast mode not supported by ${modelName}`, "info");
449
451
  return;
450
452
  }
453
+ const { forceNoFast: cliForceNoFast, forceFast: cliForceFast } = getCursorSessionSettings(cursorSettingsScopeForContext(ctx)).cli;
451
454
  if (cliForceNoFast) {
452
455
  ctx.ui.notify("Cursor fast is forced off by --cursor-no-fast", "info");
453
456
  return;
@@ -466,11 +469,11 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
466
469
  }
467
470
 
468
471
  const preferenceModelId = getFastPreferenceModelId(metadata);
469
- const current = getEffectiveFast(metadata.piModelId) ?? false;
472
+ const current = getEffectiveFast(metadata.piModelId, cursorSettingsScopeForContext(ctx)) ?? false;
470
473
  const next = !current;
471
474
  let appendError: unknown;
472
475
  try {
473
- appendError = persistFastPreference(pi, preferenceModelId, next);
476
+ appendError = persistFastPreference(pi, preferenceModelId, next, cursorSettingsScopeForContext(ctx));
474
477
  } catch (error) {
475
478
  updateCursorStatus(ctx);
476
479
  ctx.ui.notify(`Failed to save Cursor fast preference: ${error instanceof Error ? error.message : String(error)}`, "error");
@@ -529,7 +532,7 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
529
532
  }
530
533
  let appendError: unknown;
531
534
  try {
532
- appendError = persistCursorHttp1Preference(pi, next);
535
+ appendError = persistCursorHttp1Preference(pi, next, cursorSettingsScopeForContext(ctx));
533
536
  } catch (error) {
534
537
  updateCursorStatus(ctx);
535
538
  ctx.ui.notify(
@@ -568,7 +571,7 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
568
571
  return;
569
572
  }
570
573
  try {
571
- const result = await refreshSessionCursorAgentConfig();
574
+ const result = await refreshSessionCursorAgentConfig(getCursorSessionScopeSnapshot(pi).scopeKey);
572
575
  const messages: Record<typeof result, string> = {
573
576
  reloaded: "Cursor SDK agent config refreshed.",
574
577
  "no-agent": "No Cursor SDK agent exists yet; config will load on the next Cursor run.",
@@ -588,7 +591,7 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
588
591
  const usage = "Usage: /cursor-mode agent|plan";
589
592
  const mode = parseCursorAgentMode(args);
590
593
  if (!args.trim()) {
591
- const modeResolution = resolveCursorAgentMode();
594
+ const modeResolution = resolveCursorAgentMode(cursorSettingsScopeForContext(ctx));
592
595
  if (modeResolution.kind === "invalid") {
593
596
  ctx.ui.notify(`${modeResolution.message} ${usage}`, "error");
594
597
  } else {
@@ -600,14 +603,16 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
600
603
  ctx.ui.notify(`Invalid Cursor mode "${args.trim()}". ${usage}`, "error");
601
604
  return;
602
605
  }
606
+ const cli = getCursorSessionSettings(cursorSettingsScopeForContext(ctx)).cli;
607
+ const cliCursorModeState = cli.mode;
603
608
  if (cliCursorModeState.kind === "valid") {
604
609
  ctx.ui.notify(`Cursor mode is forced to ${cliCursorModeState.mode} by --cursor-mode`, "info");
605
610
  return;
606
611
  }
607
612
  const clearedInvalidCliMode = cliCursorModeState.kind === "invalid";
608
613
  try {
609
- persistCursorModePreference(pi, mode);
610
- if (clearedInvalidCliMode) cliCursorModeState = { kind: "unset" };
614
+ persistCursorModePreference(pi, mode, cursorSettingsScopeForContext(ctx));
615
+ if (clearedInvalidCliMode) cli.mode = { kind: "unset" };
611
616
  } catch (error) {
612
617
  updateCursorStatus(ctx);
613
618
  ctx.ui.notify(`Failed to save Cursor mode preference: ${error instanceof Error ? error.message : String(error)}`, "error");
@@ -633,11 +638,13 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
633
638
  authoritativeGlobalFastPreferenceIds.clear();
634
639
  setCursorHttp1GlobalPreferenceAuthoritative(false);
635
640
  globalFastPreferences = loadGlobalFastPreferences();
636
- cliForceFast = pi.getFlag("cursor-fast") === true;
637
- cliForceNoFast = pi.getFlag("cursor-no-fast") === true;
638
- restoreCursorCliState(pi);
641
+ const scopeKey = cursorSettingsScopeForContext(ctx);
642
+ const cli = getCursorSessionSettings(scopeKey).cli;
643
+ cli.forceFast = pi.getFlag("cursor-fast") === true;
644
+ cli.forceNoFast = pi.getFlag("cursor-no-fast") === true;
645
+ restoreCursorCliState(pi, scopeKey);
639
646
  restoreSessionCursorPreferences(ctx);
640
- restoreCliCursorMode(pi.getFlag("cursor-mode"));
647
+ restoreCliCursorMode(pi.getFlag("cursor-mode"), scopeKey);
641
648
  },
642
649
  sync: (ctx) => {
643
650
  if (isCursorModel(ctx.model)) notifyInvalidCursorModeIfCursorActive(ctx);
@@ -647,10 +654,10 @@ export function registerCursorRuntimeControls(pi: CursorRuntimeControlsExtension
647
654
  }
648
655
 
649
656
  function resetCursorModeStateForTests(): void {
650
- sessionCursorAgentMode = undefined;
657
+ resetCursorSessionSettingsForTests();
651
658
  setCursorHttp1GlobalPreferenceAuthoritative(false);
652
659
  setStoredCursorHttp1Enabled(undefined);
653
- cliCursorModeState = { kind: "unset" };
660
+ getCursorSessionSettings().cli.mode = { kind: "unset" };
654
661
  resetCursorRuntimeStateForTests();
655
662
  invalidCursorModeNotifiedSessionScopeKeys.clear();
656
663
  authoritativeGlobalFastPreferenceIds.clear();
@@ -664,9 +671,9 @@ export const __testUtils = {
664
671
  DEFAULT_CURSOR_AGENT_MODE,
665
672
  getConfigPath,
666
673
  loadGlobalFastPreferences,
667
- sessionFastPreferences,
668
- getSessionCursorAgentMode: () => sessionCursorAgentMode,
669
- getCliCursorAgentMode: () => (cliCursorModeState.kind === "valid" ? cliCursorModeState.mode : undefined),
670
- getCliCursorModeState: () => cliCursorModeState,
674
+ get sessionFastPreferences() { return getCursorSessionSettings().fast; },
675
+ getSessionCursorAgentMode: () => getCursorSessionSettings().mode,
676
+ getCliCursorAgentMode: () => { const mode = getCursorSessionSettings().cli.mode; return mode.kind === "valid" ? mode.mode : undefined; },
677
+ getCliCursorModeState: () => getCursorSessionSettings().cli.mode,
671
678
  resetCursorModeStateForTests,
672
679
  };
@@ -1,3 +1,4 @@
1
+ import { calculateCost } from "@earendil-works/pi-ai";
1
2
  import type { Api, AssistantMessage, Context, Model } from "@earendil-works/pi-ai";
2
3
  import {
3
4
  CURSOR_APPROX_CHARS_PER_TOKEN,
@@ -112,7 +113,7 @@ export function isCursorSdkUsageSafeForPiMessage(turnUsage: CursorSdkTurnUsage,
112
113
  export interface CursorSdkUsageApplyOptions {
113
114
  runtime: CursorRuntime;
114
115
  turn?: CursorSdkTurnUsage;
115
- billed?: CursorSdkTurnUsage;
116
+ occupancyFloor?: number;
116
117
  }
117
118
 
118
119
  export function applyCursorSdkUsage(partial: AssistantMessage, turnUsage: CursorSdkTurnUsage): void {
@@ -121,84 +122,24 @@ export function applyCursorSdkUsage(partial: AssistantMessage, turnUsage: Cursor
121
122
  partial.usage.output = turnUsage.outputTokens;
122
123
  partial.usage.cacheRead = turnUsage.cacheReadTokens;
123
124
  partial.usage.cacheWrite = turnUsage.cacheWriteTokens;
124
- // totalTokens is context occupancy (full prompt + output), not the sum of spend components alone.
125
+ // Full prompt + output equals the sum of Pi's disjoint components.
125
126
  partial.usage.totalTokens = turnUsage.inputTokens + turnUsage.outputTokens;
126
127
  }
127
128
 
128
- function isCompatibleCursorAssistantMeasurement(assistant: AssistantMessage, model: Model<Api>): boolean {
129
- return assistant.api === model.api && assistant.provider === model.provider && assistant.model === model.id;
130
- }
131
-
132
- function getLatestCompactionBoundary(context: Context): { index: number; tokensBefore?: number } | undefined {
133
- for (let index = context.messages.length - 1; index >= 0; index -= 1) {
134
- const message = context.messages[index] as { role?: string; tokensBefore?: number };
135
- if (message.role !== "compactionSummary") continue;
136
- const tokensBefore = message.tokensBefore;
137
- return {
138
- index,
139
- tokensBefore: Number.isFinite(tokensBefore) && tokensBefore !== undefined && tokensBefore > 0 ? Math.floor(tokensBefore) : undefined,
140
- };
141
- }
142
- return undefined;
143
- }
144
-
145
- function getLastAcceptedContextOccupancy(context: Context, model: Model<Api>): number {
146
- const boundary = getLatestCompactionBoundary(context);
147
- for (let index = context.messages.length - 1; index >= 0; index -= 1) {
148
- if (boundary && index < boundary.index) break;
149
- const message = context.messages[index];
150
- if (message.role !== "assistant" || !("usage" in message)) continue;
151
- const assistant = message as AssistantMessage;
152
- if (assistant.stopReason === "aborted" || assistant.stopReason === "error" || !assistant.usage) continue;
153
- if (!isCompatibleCursorAssistantMeasurement(assistant, model)) continue;
154
- const { usage } = assistant;
155
- const total =
156
- usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
157
- if (!Number.isFinite(total) || total <= 0 || total > model.contextWindow) continue;
158
- if (boundary?.tokensBefore !== undefined && total >= boundary.tokensBefore) continue;
159
- return total;
160
- }
161
- return 0;
162
- }
163
-
164
- export function applyCursorApproximateUsage(partial: AssistantMessage, model: Model<Api>, context: Context, sessionInputTokens: number): void {
129
+ export function applyCursorApproximateUsage(partial: AssistantMessage, model: Model<Api>, context: Context, sessionInputTokens: number, occupancyFloor?: number): void {
165
130
  const outputTokens = estimateCursorAssistantSessionOutputTokens(partial);
166
- partial.usage.input = Math.max(0, sessionInputTokens);
131
+ const floor = occupancyFloor !== undefined && Number.isFinite(occupancyFloor) && occupancyFloor > 0 && occupancyFloor <= model.contextWindow ? occupancyFloor : 0;
132
+ const totalTokens = Math.max(
133
+ Math.max(0, sessionInputTokens) + outputTokens,
134
+ estimateCursorContextTotalTokens(partial, model, context),
135
+ floor,
136
+ );
137
+ // Estimated prompt components describe the same occupancy as totalTokens.
138
+ partial.usage.input = totalTokens - outputTokens;
167
139
  partial.usage.output = outputTokens;
168
140
  partial.usage.cacheRead = 0;
169
141
  partial.usage.cacheWrite = 0;
170
- // Never report less occupancy than the last compatible same-model in-window assistant measurement.
171
- partial.usage.totalTokens = Math.max(
172
- partial.usage.input + partial.usage.output,
173
- estimateCursorContextTotalTokens(partial, model, context),
174
- getLastAcceptedContextOccupancy(context, model),
175
- );
176
- }
177
-
178
- function applyCursorOccupancyEstimate(partial: AssistantMessage, model: Model<Api>, context: Context): void {
179
- partial.usage.totalTokens = Math.max(
180
- estimateCursorContextTotalTokens(partial, model, context),
181
- getLastAcceptedContextOccupancy(context, model),
182
- );
183
- }
184
-
185
- function isCurrentLocalOccupancy(turn: CursorSdkTurnUsage, model: Model<Api>, context: Context): boolean {
186
- if (!isCursorSdkUsageSafeForPiMessage(turn, model)) return false;
187
- const tokensBefore = getLatestCompactionBoundary(context)?.tokensBefore;
188
- return tokensBefore === undefined || turn.inputTokens + turn.outputTokens < tokensBefore;
189
- }
190
-
191
- function applyResolvedCursorOccupancy(
192
- partial: AssistantMessage,
193
- model: Model<Api>,
194
- context: Context,
195
- localTurn: CursorSdkTurnUsage | undefined,
196
- ): void {
197
- if (localTurn && isCurrentLocalOccupancy(localTurn, model, context)) {
198
- partial.usage.totalTokens = localTurn.inputTokens + localTurn.outputTokens;
199
- return;
200
- }
201
- applyCursorOccupancyEstimate(partial, model, context);
142
+ partial.usage.totalTokens = totalTokens;
202
143
  }
203
144
 
204
145
  export function applyCursorUsage(
@@ -208,18 +149,12 @@ export function applyCursorUsage(
208
149
  sessionInputTokens: number,
209
150
  sdkUsage?: CursorSdkUsageApplyOptions,
210
151
  ): void {
211
- const billed = sdkUsage?.billed;
212
152
  const localTurn = sdkUsage?.runtime === "local" ? sdkUsage.turn : undefined;
213
- if (billed && isCursorSdkUsagePartitionSafe(billed, model)) {
214
- applyCursorSdkUsage(partial, billed);
215
- applyResolvedCursorOccupancy(partial, model, context, localTurn);
216
- return;
217
- }
218
- // Only local raw turn-ended usage has a captured full-prompt/cache-partition occupancy contract.
219
153
  if (localTurn && isCursorSdkUsageSafeForPiMessage(localTurn, model)) {
154
+ // Fresh LOCAL occupancy supersedes any previous request-provenance floor.
220
155
  applyCursorSdkUsage(partial, localTurn);
221
- applyResolvedCursorOccupancy(partial, model, context, localTurn);
222
- return;
156
+ } else {
157
+ applyCursorApproximateUsage(partial, model, context, sessionInputTokens, sdkUsage?.occupancyFloor);
223
158
  }
224
- applyCursorApproximateUsage(partial, model, context, sessionInputTokens);
159
+ calculateCost(model, partial.usage);
225
160
  }