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
@@ -11,7 +11,6 @@ export const CURSOR_ACTIVATE_SKILL_MCP_NAME = "pi__cursor_activate_skill";
11
11
  const AVAILABLE_SKILLS_SECTION_PATTERN = /<skills>\nThe following skills provide specialized instructions for specific tasks\.[\s\S]*?<\/available_skills>\n<\/skills>|\n\nThe following skills provide specialized instructions for specific tasks\.[\s\S]*?<\/available_skills>/;
12
12
  const MAX_SKILL_RESOURCES = 80;
13
13
  const RESOURCE_DIR_NAMES = ["scripts", "references", "assets"];
14
- let currentSkillsByName = new Map();
15
14
  function escapeXml(value) {
16
15
  return value
17
16
  .replace(/&/g, "&amp;")
@@ -23,32 +22,9 @@ function escapeXml(value) {
23
22
  function getVisibleSkills(skills) {
24
23
  return (skills ?? []).filter((skill) => !skill.disableModelInvocation);
25
24
  }
26
- function setCurrentSkills(skills) {
27
- currentSkillsByName = new Map(getVisibleSkills(skills).map((skill) => [skill.name, skill]));
28
- }
29
- function getAvailableSkillNames() {
30
- return [...currentSkillsByName.keys()].sort();
31
- }
32
25
  function resolveEffectiveRuntimeForSkillLifecycle(cursorModel, ctx) {
33
26
  return cursorModel ? resolveEffectiveCursorConfigForContext(ctx).runtime.value : "local";
34
27
  }
35
- function shouldExposeSkillTool(model, runtime) {
36
- return runtime === "local" && isCursorModel(model) && resolveCursorPiToolBridgeEnabled() && currentSkillsByName.size > 0;
37
- }
38
- function syncCursorSkillToolForModel(pi, model, runtime) {
39
- const activeToolNames = new Set(pi.getActiveTools());
40
- const shouldBeActive = !arePiToolsDisabled(pi) && shouldExposeSkillTool(model, runtime);
41
- const alreadyActive = activeToolNames.has(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
42
- if (shouldBeActive === alreadyActive)
43
- return;
44
- if (shouldBeActive) {
45
- activeToolNames.add(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
46
- }
47
- else {
48
- activeToolNames.delete(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
49
- }
50
- pi.setActiveTools([...activeToolNames]);
51
- }
52
28
  export function formatCursorSkillsForPrompt(skills) {
53
29
  const visibleSkills = getVisibleSkills(skills);
54
30
  if (visibleSkills.length === 0)
@@ -119,15 +95,6 @@ async function listSkillResourcePaths(baseDir) {
119
95
  }
120
96
  return resources;
121
97
  }
122
- function buildActivationDetails(skill, resources = []) {
123
- return {
124
- name: skill?.name,
125
- filePath: skill?.filePath,
126
- baseDir: skill ? dirname(skill.filePath) : undefined,
127
- resources,
128
- availableSkillNames: getAvailableSkillNames(),
129
- };
130
- }
131
98
  function formatSkillResources(resources) {
132
99
  if (resources.length === 0)
133
100
  return "<skill_resources />";
@@ -150,18 +117,35 @@ function wrapSkillContent(skill, content, resources) {
150
117
  ].join("\n");
151
118
  }
152
119
  export function registerCursorSkillTool(pi) {
120
+ let currentSkillsByName = new Map();
121
+ function setCurrentSkills(skills) {
122
+ currentSkillsByName = new Map(getVisibleSkills(skills).map((skill) => [skill.name, skill]));
123
+ }
124
+ function getAvailableSkillNames() {
125
+ return [...currentSkillsByName.keys()].sort();
126
+ }
127
+ function syncCursorSkillToolForModel(model, runtime) {
128
+ const activeToolNames = new Set(pi.getActiveTools());
129
+ const shouldBeActive = !arePiToolsDisabled(pi) && runtime === "local" && isCursorModel(model)
130
+ && resolveCursorPiToolBridgeEnabled() && currentSkillsByName.size > 0;
131
+ const alreadyActive = activeToolNames.has(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
132
+ if (shouldBeActive === alreadyActive)
133
+ return;
134
+ if (shouldBeActive) {
135
+ activeToolNames.add(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
136
+ }
137
+ else {
138
+ activeToolNames.delete(CURSOR_ACTIVATE_SKILL_TOOL_NAME);
139
+ }
140
+ pi.setActiveTools([...activeToolNames]);
141
+ }
153
142
  pi.registerTool({
154
143
  name: CURSOR_ACTIVATE_SKILL_TOOL_NAME,
155
144
  label: "Cursor skill",
156
145
  description: "Load full pi Agent Skill instructions for Cursor. Use with a skill name from the current <available_skills> catalog before applying that skill.",
157
- promptSnippet: "Load full pi Agent Skill instructions for a listed skill before Cursor applies that skill",
158
146
  parameters: Type.Object({
159
147
  name: Type.String({ description: "Skill name from the current <available_skills> catalog" }),
160
148
  }),
161
- promptGuidelines: [
162
- `Use ${CURSOR_ACTIVATE_SKILL_TOOL_NAME} only for skill names listed in the current <available_skills> catalog.`,
163
- "After loading a skill, follow its instructions and resolve relative skill paths against the returned skill directory.",
164
- ],
165
149
  async execute(_toolCallId, params) {
166
150
  const requestedName = params.name?.trim();
167
151
  if (!requestedName) {
@@ -178,7 +162,13 @@ export function registerCursorSkillTool(pi) {
178
162
  ]);
179
163
  return {
180
164
  content: [{ type: "text", text: wrapSkillContent(skill, content, resources) }],
181
- details: buildActivationDetails(skill, resources),
165
+ details: {
166
+ name: skill.name,
167
+ filePath: skill.filePath,
168
+ baseDir: dirname(skill.filePath),
169
+ resources,
170
+ availableSkillNames: getAvailableSkillNames(),
171
+ },
182
172
  };
183
173
  }
184
174
  catch (error) {
@@ -188,7 +178,7 @@ export function registerCursorSkillTool(pi) {
188
178
  });
189
179
  const clearSkillsAndSync = (model, runtime = "local") => {
190
180
  setCurrentSkills([]);
191
- syncCursorSkillToolForModel(pi, model, runtime);
181
+ syncCursorSkillToolForModel(model, runtime);
192
182
  };
193
183
  registerCursorModelLifecycle(pi, {
194
184
  sessionStart: (_event, ctx) => {
@@ -202,7 +192,7 @@ export function registerCursorSkillTool(pi) {
202
192
  const runtime = resolveEffectiveRuntimeForSkillLifecycle(cursorModel, ctx);
203
193
  if (!cursorModel || runtime === "cloud")
204
194
  setCurrentSkills([]);
205
- syncCursorSkillToolForModel(pi, ctx.model, runtime);
195
+ syncCursorSkillToolForModel(ctx.model, runtime);
206
196
  },
207
197
  beforeAgentStart: (event, ctx) => {
208
198
  const cursorModel = isCursorModel(ctx.model);
@@ -213,7 +203,7 @@ export function registerCursorSkillTool(pi) {
213
203
  else {
214
204
  setCurrentSkills([]);
215
205
  }
216
- syncCursorSkillToolForModel(pi, ctx.model, runtime);
206
+ syncCursorSkillToolForModel(ctx.model, runtime);
217
207
  const resolved = resolveCursorSkillSystemPrompt(event.systemPrompt, ctx.model, event.systemPromptOptions, runtime);
218
208
  if (resolved === event.systemPrompt)
219
209
  return undefined;
@@ -221,10 +211,3 @@ export function registerCursorSkillTool(pi) {
221
211
  },
222
212
  });
223
213
  }
224
- export const __testUtils = {
225
- AVAILABLE_SKILLS_SECTION_PATTERN,
226
- buildActivationDetails,
227
- setCurrentSkills,
228
- listSkillResourcePaths,
229
- wrapSkillContent,
230
- };
@@ -6,7 +6,8 @@ import { CURSOR_SETTING_SOURCES_ENV, DEFAULT_CURSOR_SETTING_SOURCES, resolveCurs
6
6
  import { isCursorModel } from "./cursor-model.js";
7
7
  import { registerCursorModelLifecycle } from "./cursor-model-lifecycle.js";
8
8
  import { asRecord } from "./cursor-record-utils.js";
9
- import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
9
+ import { getCursorSessionScopeSnapshot, getCursorSessionScopeKey } from "./cursor-session-scope.js";
10
+ import { cursorSettingsScopeForContext, getCursorSessionSettings, registerCursorSessionSettings, resetCursorSessionSettingsForTests } from "./cursor-session-settings.js";
10
11
  import { refreshSessionCursorAgentConfig } from "./cursor-session-agent.js";
11
12
  import { getCursorModelMetadata } from "./model-discovery.js";
12
13
  import { cursorFastDefaultsFromConfig, getCursorSdkUserConfigPath, loadCursorSdkUserConfig, mergeCursorSdkConfigForUpdate, resolveCursorFastDefault, updateCursorSdkConfig, } from "./cursor-config.js";
@@ -15,13 +16,8 @@ export { consumeCursorLocalForceOverride, getCursorCliConfig, getCursorSessionCo
15
16
  const FAST_ENTRY_TYPE = "cursor-fast-state";
16
17
  const MODE_ENTRY_TYPE = "cursor-mode-state";
17
18
  const DEFAULT_CURSOR_AGENT_MODE = "agent";
18
- const sessionFastPreferences = new Map();
19
19
  const authoritativeGlobalFastPreferenceIds = new Set();
20
20
  let globalFastPreferences = new Map();
21
- let cliForceFast = false;
22
- let cliForceNoFast = false;
23
- let sessionCursorAgentMode;
24
- let cliCursorModeState = { kind: "unset" };
25
21
  const invalidCursorModeNotifiedSessionScopeKeys = new Set();
26
22
  export function isCursorAgentMode(value) {
27
23
  return value === "agent" || value === "plan";
@@ -64,7 +60,8 @@ function saveGlobalCursorHttp1Enabled(enabled) {
64
60
  local: { useHttp1ForAgent: enabled },
65
61
  }), { newFileMode: 0o600 });
66
62
  }
67
- function restoreSessionFastPreferences(branch) {
63
+ function restoreSessionFastPreferences(branch, scopeKey) {
64
+ const sessionFastPreferences = getCursorSessionSettings(scopeKey).fast;
68
65
  sessionFastPreferences.clear();
69
66
  for (const entry of branch) {
70
67
  if (entry.type !== "custom" || entry.customType !== FAST_ENTRY_TYPE)
@@ -76,30 +73,32 @@ function restoreSessionFastPreferences(branch) {
76
73
  }
77
74
  }
78
75
  }
79
- function restoreSessionCursorMode(branch) {
80
- sessionCursorAgentMode = undefined;
76
+ function restoreSessionCursorMode(branch, scopeKey) {
77
+ const state = getCursorSessionSettings(scopeKey);
78
+ state.mode = undefined;
81
79
  for (const entry of branch) {
82
80
  if (entry.type !== "custom" || entry.customType !== MODE_ENTRY_TYPE)
83
81
  continue;
84
82
  if (isCursorModeEntryData(entry.data)) {
85
- sessionCursorAgentMode = entry.data.mode;
83
+ state.mode = entry.data.mode;
86
84
  }
87
85
  }
88
86
  }
89
87
  function restoreSessionCursorPreferences(ctx) {
90
88
  const branch = ctx.sessionManager.getBranch();
91
- restoreSessionCursorRuntimeState(branch);
92
- restoreSessionFastPreferences(branch);
93
- restoreSessionCursorMode(branch);
94
- restoreSessionCursorHttp1(branch);
95
- }
96
- function restoreSessionCursorHttp1(branch) {
97
- setStoredCursorHttp1Enabled(undefined);
89
+ const scopeKey = cursorSettingsScopeForContext(ctx);
90
+ restoreSessionCursorRuntimeState(branch, scopeKey);
91
+ restoreSessionFastPreferences(branch, scopeKey);
92
+ restoreSessionCursorMode(branch, scopeKey);
93
+ restoreSessionCursorHttp1(branch, scopeKey);
94
+ }
95
+ function restoreSessionCursorHttp1(branch, scopeKey) {
96
+ setStoredCursorHttp1Enabled(undefined, scopeKey);
98
97
  for (const entry of branch) {
99
98
  if (entry.type !== "custom" || entry.customType !== CURSOR_HTTP1_ENTRY_TYPE)
100
99
  continue;
101
100
  if (isCursorHttp1EntryData(entry.data)) {
102
- setStoredCursorHttp1Enabled(entry.data.enabled);
101
+ setStoredCursorHttp1Enabled(entry.data.enabled, scopeKey);
103
102
  }
104
103
  }
105
104
  }
@@ -113,7 +112,8 @@ function getMapFastPreference(map, metadata) {
113
112
  const preferenceModelId = getFastPreferenceModelId(metadata);
114
113
  return map.get(preferenceModelId) ?? (preferenceModelId !== metadata.baseModelId ? map.get(metadata.baseModelId) : undefined);
115
114
  }
116
- function getEffectiveFast(modelId) {
115
+ function getEffectiveFast(modelId, scopeKey) {
116
+ const { forceNoFast: cliForceNoFast, forceFast: cliForceFast } = getCursorSessionSettings(scopeKey).cli;
117
117
  const metadata = getCursorModelMetadata(modelId);
118
118
  if (!metadata?.supportsFast)
119
119
  return undefined;
@@ -123,7 +123,7 @@ function getEffectiveFast(modelId) {
123
123
  aliasOverride: metadata.fastOverride,
124
124
  sessionValue: authoritativeGlobalFastPreferenceIds.has(getFastPreferenceModelId(metadata))
125
125
  ? undefined
126
- : getMapFastPreference(sessionFastPreferences, metadata),
126
+ : getMapFastPreference(getCursorSessionSettings(scopeKey).fast, metadata),
127
127
  userValue: getMapFastPreference(globalFastPreferences, metadata),
128
128
  modelDefault: metadata.defaultFast,
129
129
  }).value;
@@ -131,21 +131,22 @@ function getEffectiveFast(modelId) {
131
131
  function formatInvalidCursorMode(raw) {
132
132
  return `Invalid --cursor-mode "${raw}". Use "agent" or "plan".`;
133
133
  }
134
- export function getStoredCursorAgentMode() {
135
- return sessionCursorAgentMode ?? DEFAULT_CURSOR_AGENT_MODE;
134
+ export function getStoredCursorAgentMode(scopeKey) {
135
+ return getCursorSessionSettings(scopeKey).mode ?? DEFAULT_CURSOR_AGENT_MODE;
136
136
  }
137
- export function resolveCursorAgentMode() {
137
+ export function resolveCursorAgentMode(scopeKey) {
138
+ const cliCursorModeState = getCursorSessionSettings(scopeKey).cli.mode;
138
139
  switch (cliCursorModeState.kind) {
139
140
  case "valid":
140
141
  return { kind: "valid", mode: cliCursorModeState.mode };
141
142
  case "invalid":
142
143
  return { kind: "invalid", raw: cliCursorModeState.raw, message: cliCursorModeState.message };
143
144
  case "unset":
144
- return { kind: "valid", mode: getStoredCursorAgentMode() };
145
+ return { kind: "valid", mode: getStoredCursorAgentMode(scopeKey) };
145
146
  }
146
147
  }
147
- export function getCursorProviderAgentModeOrThrow() {
148
- const resolution = resolveCursorAgentMode();
148
+ export function getCursorProviderAgentModeOrThrow(scopeKey) {
149
+ const resolution = resolveCursorAgentMode(scopeKey);
149
150
  if (resolution.kind === "invalid")
150
151
  throw new Error(resolution.message);
151
152
  return resolution.mode;
@@ -157,14 +158,14 @@ function updateCursorStatus(ctx, model = ctx.model) {
157
158
  }
158
159
  const metadata = getCursorModelMetadata(model.id);
159
160
  const resolution = resolveCursorStatusRuntime(ctx);
160
- const modeResolution = resolveCursorAgentMode();
161
+ const modeResolution = resolveCursorAgentMode(cursorSettingsScopeForContext(ctx));
161
162
  const mode = modeResolution.kind === "invalid" ? "invalid" : modeResolution.mode;
162
163
  if (resolution.kind === "invalid") {
163
164
  ctx.ui.setStatus("cursor", formatCursorStatus("invalid", undefined, mode));
164
165
  return;
165
166
  }
166
167
  const runtime = resolution.runtime.value;
167
- const fast = runtime === "cloud" ? undefined : metadata?.supportsFast ? getEffectiveFast(model.id) : undefined;
168
+ const fast = runtime === "cloud" ? undefined : metadata?.supportsFast ? getEffectiveFast(model.id, cursorSettingsScopeForContext(ctx)) : undefined;
168
169
  ctx.ui.setStatus("cursor", formatCursorStatus(runtime, fast, mode, resolution.useHttp1ForAgent.value));
169
170
  }
170
171
  function getCurrentCursorMetadata(ctx) {
@@ -181,7 +182,8 @@ function restoreMapValue(map, key, previous) {
181
182
  map.set(key, previous);
182
183
  }
183
184
  }
184
- function persistFastPreference(pi, modelId, fast) {
185
+ function persistFastPreference(pi, modelId, fast, scopeKey) {
186
+ const sessionFastPreferences = getCursorSessionSettings(scopeKey).fast;
185
187
  const previousSession = sessionFastPreferences.get(modelId);
186
188
  const previousGlobal = globalFastPreferences.get(modelId);
187
189
  sessionFastPreferences.set(modelId, fast);
@@ -204,25 +206,26 @@ function persistFastPreference(pi, modelId, fast) {
204
206
  return error;
205
207
  }
206
208
  }
207
- function persistCursorModePreference(pi, mode) {
208
- const previousMode = sessionCursorAgentMode;
209
- sessionCursorAgentMode = mode;
209
+ function persistCursorModePreference(pi, mode, scopeKey) {
210
+ const state = getCursorSessionSettings(scopeKey);
211
+ const previousMode = state.mode;
212
+ state.mode = mode;
210
213
  try {
211
214
  pi.appendEntry(MODE_ENTRY_TYPE, { mode });
212
215
  }
213
216
  catch (error) {
214
- sessionCursorAgentMode = previousMode;
217
+ state.mode = previousMode;
215
218
  throw error;
216
219
  }
217
220
  }
218
- function persistCursorHttp1Preference(pi, enabled) {
219
- const previousSession = getStoredCursorHttp1Enabled();
220
- setStoredCursorHttp1Enabled(enabled);
221
+ function persistCursorHttp1Preference(pi, enabled, scopeKey) {
222
+ const previousSession = getStoredCursorHttp1Enabled(scopeKey);
223
+ setStoredCursorHttp1Enabled(enabled, scopeKey);
221
224
  try {
222
225
  saveGlobalCursorHttp1Enabled(enabled);
223
226
  }
224
227
  catch (error) {
225
- setStoredCursorHttp1Enabled(previousSession);
228
+ setStoredCursorHttp1Enabled(previousSession, scopeKey);
226
229
  throw error;
227
230
  }
228
231
  try {
@@ -235,24 +238,25 @@ function persistCursorHttp1Preference(pi, enabled) {
235
238
  return error;
236
239
  }
237
240
  }
238
- function restoreCliCursorMode(raw) {
239
- cliCursorModeState = { kind: "unset" };
241
+ function restoreCliCursorMode(raw, scopeKey) {
242
+ const cli = getCursorSessionSettings(scopeKey).cli;
243
+ cli.mode = { kind: "unset" };
240
244
  if (raw === undefined || raw === "" || raw === false)
241
245
  return;
242
246
  const parsed = parseCursorAgentMode(raw);
243
247
  if (parsed) {
244
- cliCursorModeState = { kind: "valid", mode: parsed };
248
+ cli.mode = { kind: "valid", mode: parsed };
245
249
  return;
246
250
  }
247
251
  const rawText = String(raw);
248
252
  const message = formatInvalidCursorMode(rawText);
249
- cliCursorModeState = { kind: "invalid", raw: rawText, message };
253
+ cli.mode = { kind: "invalid", raw: rawText, message };
250
254
  }
251
255
  function notifyInvalidCursorModeIfCursorActive(ctx) {
252
- const modeResolution = resolveCursorAgentMode();
256
+ const modeResolution = resolveCursorAgentMode(cursorSettingsScopeForContext(ctx));
253
257
  if (modeResolution.kind !== "invalid" || !ctx.hasUI || ctx.mode !== "tui")
254
258
  return;
255
- const scopeKey = getCursorSessionScopeKey();
259
+ const scopeKey = cursorSettingsScopeForContext(ctx) ?? getCursorSessionScopeKey();
256
260
  if (invalidCursorModeNotifiedSessionScopeKeys.has(scopeKey))
257
261
  return;
258
262
  invalidCursorModeNotifiedSessionScopeKeys.add(scopeKey);
@@ -293,10 +297,11 @@ function emitCursorToolsDebugReport(pi, ctx) {
293
297
  }
294
298
  console.log(report);
295
299
  }
296
- export function getEffectiveFastForModelId(modelId) {
297
- return getEffectiveFast(modelId);
300
+ export function getEffectiveFastForModelId(modelId, scopeKey) {
301
+ return getEffectiveFast(modelId, scopeKey);
298
302
  }
299
303
  export function registerCursorRuntimeControls(pi) {
304
+ registerCursorSessionSettings(pi);
300
305
  registerCursorCloudRuntimeControls(pi, { refreshStatus: updateCursorStatus });
301
306
  pi.registerFlag("cursor-fast", {
302
307
  description: "Force Cursor fast mode for this run when the selected Cursor model supports it",
@@ -322,6 +327,7 @@ export function registerCursorRuntimeControls(pi) {
322
327
  ctx.ui.notify(`Fast mode not supported by ${modelName}`, "info");
323
328
  return;
324
329
  }
330
+ const { forceNoFast: cliForceNoFast, forceFast: cliForceFast } = getCursorSessionSettings(cursorSettingsScopeForContext(ctx)).cli;
325
331
  if (cliForceNoFast) {
326
332
  ctx.ui.notify("Cursor fast is forced off by --cursor-no-fast", "info");
327
333
  return;
@@ -336,11 +342,11 @@ export function registerCursorRuntimeControls(pi) {
336
342
  return;
337
343
  }
338
344
  const preferenceModelId = getFastPreferenceModelId(metadata);
339
- const current = getEffectiveFast(metadata.piModelId) ?? false;
345
+ const current = getEffectiveFast(metadata.piModelId, cursorSettingsScopeForContext(ctx)) ?? false;
340
346
  const next = !current;
341
347
  let appendError;
342
348
  try {
343
- appendError = persistFastPreference(pi, preferenceModelId, next);
349
+ appendError = persistFastPreference(pi, preferenceModelId, next, cursorSettingsScopeForContext(ctx));
344
350
  }
345
351
  catch (error) {
346
352
  updateCursorStatus(ctx);
@@ -389,7 +395,7 @@ export function registerCursorRuntimeControls(pi) {
389
395
  }
390
396
  let appendError;
391
397
  try {
392
- appendError = persistCursorHttp1Preference(pi, next);
398
+ appendError = persistCursorHttp1Preference(pi, next, cursorSettingsScopeForContext(ctx));
393
399
  }
394
400
  catch (error) {
395
401
  updateCursorStatus(ctx);
@@ -418,7 +424,7 @@ export function registerCursorRuntimeControls(pi) {
418
424
  return;
419
425
  }
420
426
  try {
421
- const result = await refreshSessionCursorAgentConfig();
427
+ const result = await refreshSessionCursorAgentConfig(getCursorSessionScopeSnapshot(pi).scopeKey);
422
428
  const messages = {
423
429
  reloaded: "Cursor SDK agent config refreshed.",
424
430
  "no-agent": "No Cursor SDK agent exists yet; config will load on the next Cursor run.",
@@ -438,7 +444,7 @@ export function registerCursorRuntimeControls(pi) {
438
444
  const usage = "Usage: /cursor-mode agent|plan";
439
445
  const mode = parseCursorAgentMode(args);
440
446
  if (!args.trim()) {
441
- const modeResolution = resolveCursorAgentMode();
447
+ const modeResolution = resolveCursorAgentMode(cursorSettingsScopeForContext(ctx));
442
448
  if (modeResolution.kind === "invalid") {
443
449
  ctx.ui.notify(`${modeResolution.message} ${usage}`, "error");
444
450
  }
@@ -451,15 +457,17 @@ export function registerCursorRuntimeControls(pi) {
451
457
  ctx.ui.notify(`Invalid Cursor mode "${args.trim()}". ${usage}`, "error");
452
458
  return;
453
459
  }
460
+ const cli = getCursorSessionSettings(cursorSettingsScopeForContext(ctx)).cli;
461
+ const cliCursorModeState = cli.mode;
454
462
  if (cliCursorModeState.kind === "valid") {
455
463
  ctx.ui.notify(`Cursor mode is forced to ${cliCursorModeState.mode} by --cursor-mode`, "info");
456
464
  return;
457
465
  }
458
466
  const clearedInvalidCliMode = cliCursorModeState.kind === "invalid";
459
467
  try {
460
- persistCursorModePreference(pi, mode);
468
+ persistCursorModePreference(pi, mode, cursorSettingsScopeForContext(ctx));
461
469
  if (clearedInvalidCliMode)
462
- cliCursorModeState = { kind: "unset" };
470
+ cli.mode = { kind: "unset" };
463
471
  }
464
472
  catch (error) {
465
473
  updateCursorStatus(ctx);
@@ -481,11 +489,13 @@ export function registerCursorRuntimeControls(pi) {
481
489
  authoritativeGlobalFastPreferenceIds.clear();
482
490
  setCursorHttp1GlobalPreferenceAuthoritative(false);
483
491
  globalFastPreferences = loadGlobalFastPreferences();
484
- cliForceFast = pi.getFlag("cursor-fast") === true;
485
- cliForceNoFast = pi.getFlag("cursor-no-fast") === true;
486
- restoreCursorCliState(pi);
492
+ const scopeKey = cursorSettingsScopeForContext(ctx);
493
+ const cli = getCursorSessionSettings(scopeKey).cli;
494
+ cli.forceFast = pi.getFlag("cursor-fast") === true;
495
+ cli.forceNoFast = pi.getFlag("cursor-no-fast") === true;
496
+ restoreCursorCliState(pi, scopeKey);
487
497
  restoreSessionCursorPreferences(ctx);
488
- restoreCliCursorMode(pi.getFlag("cursor-mode"));
498
+ restoreCliCursorMode(pi.getFlag("cursor-mode"), scopeKey);
489
499
  },
490
500
  sync: (ctx) => {
491
501
  if (isCursorModel(ctx.model))
@@ -495,10 +505,10 @@ export function registerCursorRuntimeControls(pi) {
495
505
  });
496
506
  }
497
507
  function resetCursorModeStateForTests() {
498
- sessionCursorAgentMode = undefined;
508
+ resetCursorSessionSettingsForTests();
499
509
  setCursorHttp1GlobalPreferenceAuthoritative(false);
500
510
  setStoredCursorHttp1Enabled(undefined);
501
- cliCursorModeState = { kind: "unset" };
511
+ getCursorSessionSettings().cli.mode = { kind: "unset" };
502
512
  resetCursorRuntimeStateForTests();
503
513
  invalidCursorModeNotifiedSessionScopeKeys.clear();
504
514
  authoritativeGlobalFastPreferenceIds.clear();
@@ -511,9 +521,9 @@ export const __testUtils = {
511
521
  DEFAULT_CURSOR_AGENT_MODE,
512
522
  getConfigPath,
513
523
  loadGlobalFastPreferences,
514
- sessionFastPreferences,
515
- getSessionCursorAgentMode: () => sessionCursorAgentMode,
516
- getCliCursorAgentMode: () => (cliCursorModeState.kind === "valid" ? cliCursorModeState.mode : undefined),
517
- getCliCursorModeState: () => cliCursorModeState,
524
+ get sessionFastPreferences() { return getCursorSessionSettings().fast; },
525
+ getSessionCursorAgentMode: () => getCursorSessionSettings().mode,
526
+ getCliCursorAgentMode: () => { const mode = getCursorSessionSettings().cli.mode; return mode.kind === "valid" ? mode.mode : undefined; },
527
+ getCliCursorModeState: () => getCursorSessionSettings().cli.mode,
518
528
  resetCursorModeStateForTests,
519
529
  };
@@ -1,3 +1,4 @@
1
+ import { calculateCost } from "@earendil-works/pi-ai";
1
2
  import { CURSOR_APPROX_CHARS_PER_TOKEN, CURSOR_IMAGE_TOKEN_ESTIMATE, estimateCursorContextTokens, estimateCursorTextTokens, } from "./context.js";
2
3
  import { asRecord, getNumber } from "./cursor-record-utils.js";
3
4
  function getPromptInputTokenBudget(model) {
@@ -81,86 +82,28 @@ export function applyCursorSdkUsage(partial, turnUsage) {
81
82
  partial.usage.output = turnUsage.outputTokens;
82
83
  partial.usage.cacheRead = turnUsage.cacheReadTokens;
83
84
  partial.usage.cacheWrite = turnUsage.cacheWriteTokens;
84
- // totalTokens is context occupancy (full prompt + output), not the sum of spend components alone.
85
+ // Full prompt + output equals the sum of Pi's disjoint components.
85
86
  partial.usage.totalTokens = turnUsage.inputTokens + turnUsage.outputTokens;
86
87
  }
87
- function isCompatibleCursorAssistantMeasurement(assistant, model) {
88
- return assistant.api === model.api && assistant.provider === model.provider && assistant.model === model.id;
89
- }
90
- function getLatestCompactionBoundary(context) {
91
- for (let index = context.messages.length - 1; index >= 0; index -= 1) {
92
- const message = context.messages[index];
93
- if (message.role !== "compactionSummary")
94
- continue;
95
- const tokensBefore = message.tokensBefore;
96
- return {
97
- index,
98
- tokensBefore: Number.isFinite(tokensBefore) && tokensBefore !== undefined && tokensBefore > 0 ? Math.floor(tokensBefore) : undefined,
99
- };
100
- }
101
- return undefined;
102
- }
103
- function getLastAcceptedContextOccupancy(context, model) {
104
- const boundary = getLatestCompactionBoundary(context);
105
- for (let index = context.messages.length - 1; index >= 0; index -= 1) {
106
- if (boundary && index < boundary.index)
107
- break;
108
- const message = context.messages[index];
109
- if (message.role !== "assistant" || !("usage" in message))
110
- continue;
111
- const assistant = message;
112
- if (assistant.stopReason === "aborted" || assistant.stopReason === "error" || !assistant.usage)
113
- continue;
114
- if (!isCompatibleCursorAssistantMeasurement(assistant, model))
115
- continue;
116
- const { usage } = assistant;
117
- const total = usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
118
- if (!Number.isFinite(total) || total <= 0 || total > model.contextWindow)
119
- continue;
120
- if (boundary?.tokensBefore !== undefined && total >= boundary.tokensBefore)
121
- continue;
122
- return total;
123
- }
124
- return 0;
125
- }
126
- export function applyCursorApproximateUsage(partial, model, context, sessionInputTokens) {
88
+ export function applyCursorApproximateUsage(partial, model, context, sessionInputTokens, occupancyFloor) {
127
89
  const outputTokens = estimateCursorAssistantSessionOutputTokens(partial);
128
- partial.usage.input = Math.max(0, sessionInputTokens);
90
+ const floor = occupancyFloor !== undefined && Number.isFinite(occupancyFloor) && occupancyFloor > 0 && occupancyFloor <= model.contextWindow ? occupancyFloor : 0;
91
+ const totalTokens = Math.max(Math.max(0, sessionInputTokens) + outputTokens, estimateCursorContextTotalTokens(partial, model, context), floor);
92
+ // Estimated prompt components describe the same occupancy as totalTokens.
93
+ partial.usage.input = totalTokens - outputTokens;
129
94
  partial.usage.output = outputTokens;
130
95
  partial.usage.cacheRead = 0;
131
96
  partial.usage.cacheWrite = 0;
132
- // Never report less occupancy than the last compatible same-model in-window assistant measurement.
133
- partial.usage.totalTokens = Math.max(partial.usage.input + partial.usage.output, estimateCursorContextTotalTokens(partial, model, context), getLastAcceptedContextOccupancy(context, model));
134
- }
135
- function applyCursorOccupancyEstimate(partial, model, context) {
136
- partial.usage.totalTokens = Math.max(estimateCursorContextTotalTokens(partial, model, context), getLastAcceptedContextOccupancy(context, model));
137
- }
138
- function isCurrentLocalOccupancy(turn, model, context) {
139
- if (!isCursorSdkUsageSafeForPiMessage(turn, model))
140
- return false;
141
- const tokensBefore = getLatestCompactionBoundary(context)?.tokensBefore;
142
- return tokensBefore === undefined || turn.inputTokens + turn.outputTokens < tokensBefore;
143
- }
144
- function applyResolvedCursorOccupancy(partial, model, context, localTurn) {
145
- if (localTurn && isCurrentLocalOccupancy(localTurn, model, context)) {
146
- partial.usage.totalTokens = localTurn.inputTokens + localTurn.outputTokens;
147
- return;
148
- }
149
- applyCursorOccupancyEstimate(partial, model, context);
97
+ partial.usage.totalTokens = totalTokens;
150
98
  }
151
99
  export function applyCursorUsage(partial, model, context, sessionInputTokens, sdkUsage) {
152
- const billed = sdkUsage?.billed;
153
100
  const localTurn = sdkUsage?.runtime === "local" ? sdkUsage.turn : undefined;
154
- if (billed && isCursorSdkUsagePartitionSafe(billed, model)) {
155
- applyCursorSdkUsage(partial, billed);
156
- applyResolvedCursorOccupancy(partial, model, context, localTurn);
157
- return;
158
- }
159
- // Only local raw turn-ended usage has a captured full-prompt/cache-partition occupancy contract.
160
101
  if (localTurn && isCursorSdkUsageSafeForPiMessage(localTurn, model)) {
102
+ // Fresh LOCAL occupancy supersedes any previous request-provenance floor.
161
103
  applyCursorSdkUsage(partial, localTurn);
162
- applyResolvedCursorOccupancy(partial, model, context, localTurn);
163
- return;
164
104
  }
165
- applyCursorApproximateUsage(partial, model, context, sessionInputTokens);
105
+ else {
106
+ applyCursorApproximateUsage(partial, model, context, sessionInputTokens, sdkUsage?.occupancyFloor);
107
+ }
108
+ calculateCost(model, partial.usage);
166
109
  }