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
@@ -36,7 +36,8 @@ import {
36
36
  } from "./cursor-config.js";
37
37
  import { asRecord } from "./cursor-record-utils.js";
38
38
  import { getResolvedSessionCursorHttp1Enabled } from "./cursor-http1.js";
39
- import { getCursorSessionCwd, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
39
+ import { getCursorSessionScopeSnapshot } from "./cursor-session-scope.js";
40
+ import { cursorSettingsScopeForContext, getCursorSessionSettings } from "./cursor-session-settings.js";
40
41
 
41
42
  export const CURSOR_RUNTIME_ENTRY_TYPE = "cursor-runtime-state";
42
43
 
@@ -59,20 +60,12 @@ export type CursorRuntimeStateExtensionApi = Pick<
59
60
  "appendEntry" | "getFlag" | "registerFlag" | "registerCommand" | "on"
60
61
  >;
61
62
 
62
- type CursorRuntimeContext = Pick<ExtensionContext, "cwd">;
63
+ type CursorRuntimeContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext, "sessionManager">>;
63
64
 
64
65
  type CursorStatusRefresh = (ctx: ExtensionContext) => void;
65
66
 
66
- interface CursorCliConfigSnapshot {
67
- config: CursorExplicitSdkConfig;
68
- cloudEnvNames?: string;
69
- }
70
67
 
71
- let cliCursorSnapshot: CursorCliConfigSnapshot = { config: {} };
72
- let cliLocalForceConsumed = false;
73
68
  let envLocalForceConsumed = false;
74
- let sessionCursorRuntime: CursorRuntime | undefined;
75
- let sessionCursorCloudAcknowledged = false;
76
69
 
77
70
  function isCursorRuntimeEntryData(value: unknown): value is CursorRuntimeEntryData {
78
71
  const record = asRecord(value);
@@ -85,15 +78,17 @@ function stringFlagValue(value: boolean | string | undefined): string | undefine
85
78
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
86
79
  }
87
80
 
88
- export function getCursorCliConfig(): CursorExplicitSdkConfig {
89
- const config = structuredClone(cliCursorSnapshot.config);
90
- const envNames = parseExplicitCursorCloudEnvNames(cliCursorSnapshot.cloudEnvNames, "--cursor-cloud-env");
81
+ export function getCursorCliConfig(scopeKey?: string): CursorExplicitSdkConfig {
82
+ const cli = getCursorSessionSettings(scopeKey).cli;
83
+ const config = structuredClone(cli.config);
84
+ const envNames = parseExplicitCursorCloudEnvNames(cli.cloudEnvNames, "--cursor-cloud-env");
91
85
  if (envNames) config.cloud = { ...config.cloud, envNames };
92
86
  return config;
93
87
  }
94
88
 
95
- export function getCursorSessionConfig(): CursorSdkConfig {
96
- const useHttp1ForAgent = getResolvedSessionCursorHttp1Enabled();
89
+ export function getCursorSessionConfig(scopeKey?: string): CursorSdkConfig {
90
+ const { runtime: sessionCursorRuntime, cloudAcknowledged: sessionCursorCloudAcknowledged } = getCursorSessionSettings(scopeKey);
91
+ const useHttp1ForAgent = getResolvedSessionCursorHttp1Enabled(scopeKey);
97
92
  return {
98
93
  ...(sessionCursorRuntime
99
94
  ? {
@@ -108,11 +103,12 @@ export function getCursorSessionConfig(): CursorSdkConfig {
108
103
  export function resolveEffectiveCursorConfig(options: {
109
104
  cwd: string;
110
105
  projectTrusted?: boolean;
106
+ scopeKey?: string;
111
107
  }): CursorResolvedSdkConfig {
112
108
  const loadedConfig = loadCursorSdkConfig({ cwd: options.cwd, projectTrusted: options.projectTrusted === true });
113
109
  return resolveCursorSdkConfig({
114
- cli: getCursorCliConfig(),
115
- session: getCursorSessionConfig(),
110
+ cli: getCursorCliConfig(options.scopeKey),
111
+ session: getCursorSessionConfig(options.scopeKey),
116
112
  user: loadedConfig.user,
117
113
  project: loadedConfig.project,
118
114
  });
@@ -121,7 +117,10 @@ export function resolveEffectiveCursorConfig(options: {
121
117
  export function resolveEffectiveCursorConfigForContext(ctx: CursorRuntimeContext): CursorResolvedSdkConfig {
122
118
  return resolveEffectiveCursorConfig({
123
119
  cwd: ctx.cwd,
124
- projectTrusted: getCursorSessionCwd() === ctx.cwd && getCursorSessionProjectTrusted(),
120
+ projectTrusted: cursorSettingsScopeForContext(ctx) === undefined
121
+ ? getCursorSessionScopeSnapshot().cwd === ctx.cwd && getCursorSessionScopeSnapshot().projectTrusted
122
+ : getCursorSessionSettings(cursorSettingsScopeForContext(ctx)).projectTrusted,
123
+ scopeKey: cursorSettingsScopeForContext(ctx),
125
124
  });
126
125
  }
127
126
 
@@ -166,14 +165,15 @@ export function formatCursorStatus(
166
165
  return parts.join(" · ");
167
166
  }
168
167
 
169
- export function consumeCursorLocalForceOverride(resolved: { value: boolean; source: string }): boolean {
168
+ export function consumeCursorLocalForceOverride(resolved: { value: boolean; source: string }, scopeKey?: string): boolean {
170
169
  if (!resolved.value) return false;
171
- if (resolved.source === "cli" && !cliLocalForceConsumed) {
172
- if (cliCursorSnapshot.config.local) {
173
- const { force: _, ...local } = cliCursorSnapshot.config.local;
174
- cliCursorSnapshot.config.local = local;
170
+ const cli = getCursorSessionSettings(scopeKey).cli;
171
+ if (resolved.source === "cli" && !cli.localForceConsumed) {
172
+ if (cli.config.local) {
173
+ const { force: _, ...local } = cli.config.local;
174
+ cli.config.local = local;
175
175
  }
176
- cliLocalForceConsumed = true;
176
+ cli.localForceConsumed = true;
177
177
  return true;
178
178
  }
179
179
  if (resolved.source === "environment" && !envLocalForceConsumed) {
@@ -183,19 +183,21 @@ export function consumeCursorLocalForceOverride(resolved: { value: boolean; sour
183
183
  return false;
184
184
  }
185
185
 
186
- export function restoreSessionCursorRuntimeState(branch: readonly SessionEntry[]): void {
187
- sessionCursorRuntime = undefined;
188
- sessionCursorCloudAcknowledged = false;
186
+ export function restoreSessionCursorRuntimeState(branch: readonly SessionEntry[], scopeKey?: string): void {
187
+ const state = getCursorSessionSettings(scopeKey);
188
+ state.runtime = undefined;
189
+ state.cloudAcknowledged = false;
189
190
  for (const entry of branch) {
190
191
  if (entry.type !== "custom" || entry.customType !== CURSOR_RUNTIME_ENTRY_TYPE) continue;
191
192
  if (isCursorRuntimeEntryData(entry.data)) {
192
- sessionCursorRuntime = entry.data.runtime;
193
- sessionCursorCloudAcknowledged ||= entry.data.cloudAcknowledged === true;
193
+ state.runtime = entry.data.runtime;
194
+ state.cloudAcknowledged ||= entry.data.cloudAcknowledged === true;
194
195
  }
195
196
  }
196
197
  }
197
198
 
198
- export function restoreCursorCliState(pi: Pick<ExtensionAPI, "getFlag">): void {
199
+ export function restoreCursorCliState(pi: Pick<ExtensionAPI, "getFlag">, scopeKey?: string): void {
200
+ const cli = getCursorSessionSettings(scopeKey).cli;
199
201
  const runtime = stringFlagValue(pi.getFlag("cursor-runtime"));
200
202
  const repo = stringFlagValue(pi.getFlag("cursor-cloud-repo"));
201
203
  const branch = stringFlagValue(pi.getFlag("cursor-cloud-branch"));
@@ -220,35 +222,35 @@ export function restoreCursorCliState(pi: Pick<ExtensionAPI, "getFlag">): void {
220
222
  const local: NonNullable<CursorExplicitSdkConfig["local"]> = {
221
223
  ...(pi.getFlag("cursor-auto-review") === true ? { autoReview: true } : {}),
222
224
  ...(pi.getFlag("cursor-sandbox") === true ? { sandboxOptions: { enabled: true } } : {}),
223
- ...(!cliLocalForceConsumed && pi.getFlag("cursor-local-force") === true ? { force: true } : {}),
225
+ ...(!cli.localForceConsumed && pi.getFlag("cursor-local-force") === true ? { force: true } : {}),
224
226
  ...(pi.getFlag("cursor-no-local-resume") === true
225
227
  ? { resume: false }
226
228
  : pi.getFlag("cursor-local-resume") === true
227
229
  ? { resume: true }
228
230
  : {}),
229
231
  };
230
- cliCursorSnapshot = {
231
- config: {
232
- ...(runtime ? { runtime } : {}),
233
- ...(Object.keys(cloud).length ? { cloud } : {}),
234
- ...(Object.keys(local).length ? { local } : {}),
235
- },
236
- ...(cloudEnvNames ? { cloudEnvNames } : {}),
232
+ cli.config = {
233
+ ...(runtime ? { runtime } : {}),
234
+ ...(Object.keys(cloud).length ? { cloud } : {}),
235
+ ...(Object.keys(local).length ? { local } : {}),
237
236
  };
237
+ cli.cloudEnvNames = cloudEnvNames;
238
238
  }
239
239
 
240
240
  function persistCursorRuntimePreference(
241
241
  pi: Pick<ExtensionAPI, "appendEntry">,
242
242
  runtime: CursorRuntime,
243
- cloudAcknowledged = false,
243
+ cloudAcknowledged: boolean,
244
+ scopeKey: string | undefined,
244
245
  ): void {
245
- const acknowledged = sessionCursorCloudAcknowledged || cloudAcknowledged;
246
+ const state = getCursorSessionSettings(scopeKey);
247
+ const acknowledged = state.cloudAcknowledged || cloudAcknowledged;
246
248
  pi.appendEntry<CursorRuntimeEntryData>(CURSOR_RUNTIME_ENTRY_TYPE, {
247
249
  runtime,
248
250
  ...(acknowledged ? { cloudAcknowledged: true } : {}),
249
251
  });
250
- sessionCursorRuntime = runtime;
251
- sessionCursorCloudAcknowledged = acknowledged;
252
+ state.runtime = runtime;
253
+ state.cloudAcknowledged = acknowledged;
252
254
  }
253
255
 
254
256
  function registerCursorRuntimeFlags(pi: Pick<ExtensionAPI, "registerFlag">): void {
@@ -368,6 +370,7 @@ function registerCursorRuntimeCommand(
368
370
  pi.registerCommand("cursor-runtime", {
369
371
  description: "Set Cursor runtime for this session: local or cloud",
370
372
  handler: async (args, ctx) => {
373
+ const scopeKey = cursorSettingsScopeForContext(ctx);
371
374
  const usage = "Usage: /cursor-runtime local|cloud [--save-user|--save-project]";
372
375
  const tokens = args.trim().split(/\s+/).filter(Boolean);
373
376
  const raw = tokens[0];
@@ -392,7 +395,7 @@ function registerCursorRuntimeCommand(
392
395
  ctx.ui.notify(`${currentResolution.message} Fix the explicit override before changing the session runtime.`, "error");
393
396
  return;
394
397
  }
395
- if (saveProject && (getCursorSessionCwd() !== ctx.cwd || !getCursorSessionProjectTrusted())) {
398
+ if (saveProject && (getCursorSessionScopeSnapshot(pi).cwd !== ctx.cwd || !getCursorSessionScopeSnapshot(pi).projectTrusted)) {
396
399
  ctx.ui.notify(
397
400
  "Cannot save Cursor project config without explicit project-trust provenance. Ensure .pi/settings.json or another Pi project resource exists, trust the project, then restart pi; or restart with --approve. Project-local package installs must use --approve on every run that reads or writes .pi/cursor-sdk.json.",
398
401
  "error",
@@ -435,7 +438,7 @@ function registerCursorRuntimeCommand(
435
438
  }
436
439
  }
437
440
  try {
438
- persistCursorRuntimePreference(pi, raw, cloudAcknowledged);
441
+ persistCursorRuntimePreference(pi, raw, cloudAcknowledged, scopeKey);
439
442
  } catch (error) {
440
443
  const effectiveResolution = resolveCursorStatusRuntime(ctx);
441
444
  refreshStatus(ctx);
@@ -489,9 +492,11 @@ export function registerCursorCloudRuntimeControls(
489
492
  }
490
493
 
491
494
  export function resetCursorRuntimeStateForTests(): void {
492
- cliCursorSnapshot = { config: {} };
493
- cliLocalForceConsumed = false;
495
+ const cli = getCursorSessionSettings().cli;
496
+ cli.config = {};
497
+ cli.cloudEnvNames = undefined;
498
+ cli.localForceConsumed = false;
494
499
  envLocalForceConsumed = false;
495
- sessionCursorRuntime = undefined;
496
- sessionCursorCloudAcknowledged = false;
500
+ getCursorSessionSettings().runtime = undefined;
501
+ getCursorSessionSettings().cloudAcknowledged = false;
497
502
  }
@@ -1,107 +1,59 @@
1
- import type { SDKAgent } from "@cursor/sdk";
2
- import type { CursorRuntime } from "./cursor-config.js";
3
- import { asRecord, getArray, getString } from "./cursor-record-utils.js";
4
- import { readCursorSdkTurnUsage, type CursorSdkTurnUsage } from "./cursor-usage-accounting.js";
5
-
6
- const BILLED_USAGE_TIMEOUT_MS = 5000;
7
-
8
- // ponytail: process-lifetime watermark of billed usage UUIDs per agentId; reset on process exit.
9
- // Upgrade to session-scoped storage if multi-day processes retain enough agent IDs to matter.
10
- const seenBilledRunIdsByAgent = new Map<string, Set<string>>();
11
-
12
- export function isCursorSdkClientMintedRunId(runId: string): boolean {
13
- return runId.startsWith("run-");
14
- }
15
-
16
- export function sumCursorSdkTurnUsage(usages: readonly CursorSdkTurnUsage[]): CursorSdkTurnUsage | undefined {
17
- if (usages.length === 0) return undefined;
18
- return usages.reduce(
19
- (total, usage) => ({
20
- inputTokens: total.inputTokens + usage.inputTokens,
21
- outputTokens: total.outputTokens + usage.outputTokens,
22
- cacheReadTokens: total.cacheReadTokens + usage.cacheReadTokens,
23
- cacheWriteTokens: total.cacheWriteTokens + usage.cacheWriteTokens,
24
- }),
25
- );
26
- }
27
-
28
- export function peekCursorBilledUsageRunIds(agentId: string): ReadonlySet<string> {
29
- return seenBilledRunIdsByAgent.get(agentId) ?? new Set();
30
- }
31
-
32
- export function rememberCursorBilledUsageRunIds(agentId: string, runIds: readonly string[]): void {
33
- if (runIds.length === 0) return;
34
- let seen = seenBilledRunIdsByAgent.get(agentId);
35
- if (!seen) {
36
- seen = new Set();
37
- seenBilledRunIdsByAgent.set(agentId, seen);
1
+ import type { AgentUsage, SDKAgent, TokenUsage, UsageCost } from "@cursor/sdk";
2
+ import { asRecord } from "./cursor-record-utils.js";
3
+
4
+ export type CursorBillingObservation =
5
+ | { status: "observed-pending-settlement"; snapshot: AgentUsage }
6
+ | { status: "unavailable"; reason: "timeout" | "unsupported" | "request-failed" | "invalid-response" };
7
+
8
+ const fields = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "totalTokens"] as const;
9
+ export function readCursorUsageTokens(value: unknown): TokenUsage | undefined {
10
+ const record = asRecord(value);
11
+ if (!record || fields.some(key => !Number.isSafeInteger(record[key]) || (record[key] as number) < 0)) return undefined;
12
+ if (record.reasoningTokens !== undefined && (!Number.isSafeInteger(record.reasoningTokens) || (record.reasoningTokens as number) < 0 || (record.reasoningTokens as number) > (record.outputTokens as number))) return undefined;
13
+ return { inputTokens: record.inputTokens as number, outputTokens: record.outputTokens as number, cacheReadTokens: record.cacheReadTokens as number, cacheWriteTokens: record.cacheWriteTokens as number, totalTokens: record.totalTokens as number, ...(record.reasoningTokens === undefined ? {} : { reasoningTokens: record.reasoningTokens as number }) };
14
+ }
15
+ function readCost(value: unknown): UsageCost | undefined {
16
+ const record = asRecord(value);
17
+ if (!record || ["rawCostCents", "chargedCents"].some(key => typeof record[key] !== "number" || !Number.isFinite(record[key]) || (record[key] as number) < 0)) return undefined;
18
+ return { rawCostCents: record.rawCostCents as number, chargedCents: record.chargedCents as number };
19
+ }
20
+ function billedTokens(value: unknown): TokenUsage | undefined {
21
+ const usage = readCursorUsageTokens(value);
22
+ if (!usage || usage.totalTokens !== usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens) return undefined;
23
+ return usage;
24
+ }
25
+ export function readCursorBilledSnapshot(value: unknown): AgentUsage | undefined {
26
+ const record = asRecord(value);
27
+ const usage = billedTokens(record?.usage);
28
+ if (!record || !usage || !Array.isArray(record.runs)) return undefined;
29
+ const cost = record.cost === undefined ? undefined : readCost(record.cost);
30
+ if (record.cost !== undefined && !cost) return undefined;
31
+ const runs: AgentUsage["runs"] = [];
32
+ const ids = new Set<string>();
33
+ for (const item of record.runs) {
34
+ const row = asRecord(item);
35
+ const tokens = billedTokens(row?.usage);
36
+ const rowCost = row?.cost === undefined ? undefined : readCost(row.cost);
37
+ if (!row || typeof row.runId !== "string" || !row.runId || row.runId.length > 256 || ids.has(row.runId) || !tokens || (row.cost !== undefined && !rowCost)) return undefined;
38
+ ids.add(row.runId);
39
+ runs.push({ runId: row.runId, usage: tokens, ...(rowCost ? { cost: rowCost } : {}) });
38
40
  }
39
- for (const runId of runIds) seen.add(runId);
41
+ if (fields.some(key => runs.reduce((sum, row) => sum + row.usage[key], 0) > usage[key])) return undefined;
42
+ return { usage, runs, ...(cost ? { cost } : {}) };
40
43
  }
41
44
 
42
- function withTimeout<T>(promise: Promise<T>, timeoutMs = BILLED_USAGE_TIMEOUT_MS): Promise<T | undefined> {
45
+ /** Fetch is only observation; the origin journal owns recognition. No run-ID guesses or private fallback. */
46
+ export async function fetchCursorSdkAgentUsage(agent: Pick<SDKAgent, "getUsage">): Promise<CursorBillingObservation> {
47
+ if (typeof agent.getUsage !== "function") return { status: "unavailable", reason: "unsupported" };
43
48
  let timer: ReturnType<typeof setTimeout> | undefined;
44
- const timeout = new Promise<undefined>((resolve) => {
45
- timer = setTimeout(() => resolve(undefined), timeoutMs);
46
- timer.unref?.();
47
- });
48
- return Promise.race([promise.catch(() => undefined), timeout]).finally(() => {
49
- if (timer) clearTimeout(timer);
50
- });
49
+ try {
50
+ const timeout = new Promise<CursorBillingObservation>(resolve => {
51
+ timer = setTimeout(() => resolve({ status: "unavailable", reason: "timeout" }), 5000);
52
+ timer.unref?.();
53
+ });
54
+ return await Promise.race([Promise.resolve().then(() => agent.getUsage()).then(value => {
55
+ const snapshot = readCursorBilledSnapshot(value);
56
+ return snapshot ? { status: "observed-pending-settlement" as const, snapshot } : { status: "unavailable" as const, reason: "invalid-response" as const };
57
+ }, () => ({ status: "unavailable" as const, reason: "request-failed" as const })), timeout]);
58
+ } finally { if (timer) clearTimeout(timer); }
51
59
  }
52
-
53
- export function selectCursorBilledTurnUsage(
54
- agentUsage: unknown,
55
- options: { runtime: CursorRuntime; runId?: string; seenRunIds?: ReadonlySet<string> },
56
- ): { turn?: CursorSdkTurnUsage; runIds: string[] } {
57
- const runs = (getArray(asRecord(agentUsage), "runs") ?? []).flatMap((item) => {
58
- const record = asRecord(item);
59
- const runId = getString(record, "runId");
60
- const usage = readCursorSdkTurnUsage(record?.usage);
61
- return runId && usage ? [{ runId, usage }] : [];
62
- });
63
- if (options.runtime === "cloud" && options.runId) {
64
- const match = runs.find((run) => run.runId === options.runId);
65
- return match ? { turn: match.usage, runIds: [match.runId] } : { runIds: [] };
66
- }
67
- const unseen = runs.filter((run) => !options.seenRunIds?.has(run.runId));
68
- return { turn: sumCursorSdkTurnUsage(unseen.map((run) => run.usage)), runIds: unseen.map((run) => run.runId) };
69
- }
70
-
71
- export async function fetchCursorSdkAgentUsage(
72
- agent: SDKAgent,
73
- options: { runtime: CursorRuntime; runId?: string },
74
- ): Promise<unknown | undefined> {
75
- if (typeof agent.getUsage !== "function") return undefined;
76
- const query =
77
- options.runtime === "cloud" && options.runId && isCursorSdkClientMintedRunId(options.runId)
78
- ? { runId: options.runId }
79
- : undefined;
80
- return withTimeout(Promise.resolve(agent.getUsage(query)));
81
- }
82
-
83
- export async function attachCursorSdkBilledTurnUsage(options: {
84
- agent: SDKAgent;
85
- agentId: string;
86
- runtime: CursorRuntime;
87
- runId?: string;
88
- }): Promise<{ agentUsage?: unknown; turn?: CursorSdkTurnUsage }> {
89
- const agentUsage = await fetchCursorSdkAgentUsage(options.agent, {
90
- runtime: options.runtime,
91
- runId: options.runId,
92
- });
93
- if (!agentUsage) return {};
94
- const selected = selectCursorBilledTurnUsage(agentUsage, {
95
- runtime: options.runtime,
96
- runId: options.runtime === "cloud" ? options.runId : undefined,
97
- seenRunIds: peekCursorBilledUsageRunIds(options.agentId),
98
- });
99
- rememberCursorBilledUsageRunIds(options.agentId, selected.runIds);
100
- return { agentUsage, turn: selected.turn };
101
- }
102
-
103
- export const __testUtils = {
104
- reset(): void {
105
- seenBilledRunIdsByAgent.clear();
106
- },
107
- };
@@ -7,7 +7,7 @@ import {
7
7
  SESSION_MANIFEST,
8
8
  resolveCursorSdkEventDebugBaseDir,
9
9
  } from "./cursor-sdk-event-debug-constants.js";
10
- import { getCursorSessionFile, getCursorSessionScopeKey } from "./cursor-session-scope.js";
10
+ import { getCursorSessionScopeSnapshot, type CursorTurnScope } from "./cursor-session-scope.js";
11
11
 
12
12
  const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__";
13
13
 
@@ -93,13 +93,14 @@ function resolveSessionDebugDir(
93
93
  export function allocateCursorSdkEventDebugTurn(
94
94
  cwd: string,
95
95
  env: Record<string, string | undefined>,
96
+ scope: CursorTurnScope = getCursorSessionScopeSnapshot(),
96
97
  ): CursorSdkEventDebugTurnAllocation {
97
98
  const pinnedRunDir = resolvePinnedRunArtifactDir(env[CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV]);
98
99
  if (pinnedRunDir) {
99
100
  return { artifactDir: pinnedRunDir, pinnedRun: true };
100
101
  }
101
102
 
102
- const scopeKey = getCursorSessionScopeKey();
103
+ const scopeKey = scope.scopeKey;
103
104
  const sessionDir = resolveSessionDebugDir(cwd, env, scopeKey);
104
105
  mkdirSync(sessionDir, { recursive: true });
105
106
 
@@ -118,13 +119,13 @@ export function allocateCursorSdkEventDebugTurn(
118
119
  const existing = readSessionManifest(sessionDir);
119
120
  const manifest: CursorSdkEventDebugSessionManifest = existing ?? {
120
121
  sessionKey: scopeKey,
121
- sessionFile: getCursorSessionFile(),
122
+ sessionFile: scope.sessionFile,
122
123
  sessionDir,
123
124
  createdAt: new Date().toISOString(),
124
125
  updatedAt: new Date().toISOString(),
125
126
  turns: [],
126
127
  };
127
- manifest.sessionFile = getCursorSessionFile();
128
+ manifest.sessionFile = scope.sessionFile;
128
129
  manifest.updatedAt = new Date().toISOString();
129
130
  manifest.turns.push({
130
131
  turn: state.turnCounter,
@@ -154,7 +155,6 @@ export function updateCursorSdkEventDebugSessionManifest(
154
155
  turnEntry.finalizedAt = new Date().toISOString();
155
156
  turnEntry.summary = summary;
156
157
  manifest.updatedAt = new Date().toISOString();
157
- manifest.sessionFile = getCursorSessionFile();
158
158
  writeSessionManifest(sessionDir, manifest);
159
159
  }
160
160
 
@@ -8,7 +8,7 @@ import { serializeCursorPiToolBridgeDiagnostic } from "./cursor-pi-tool-bridge-d
8
8
  import type { CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge-types.js";
9
9
  import type { CursorLiveQueuedEvent } from "./cursor-live-run-coordinator.js";
10
10
  import { asRecord } from "./cursor-record-utils.js";
11
- import { getCursorSessionFile } from "./cursor-session-scope.js";
11
+ import { getCursorSessionScopeSnapshot, type CursorTurnScope } from "./cursor-session-scope.js";
12
12
  import { parseEnvBoolean } from "./cursor-env-boolean.js";
13
13
  import {
14
14
  ARTIFACTS,
@@ -58,6 +58,7 @@ export interface CursorSdkDisplayDecisionRecord {
58
58
  }
59
59
 
60
60
  export interface CursorSdkEventDebugSinkOptions {
61
+ scope?: CursorTurnScope;
61
62
  cwd: string;
62
63
  modelId: string;
63
64
  provider: string;
@@ -250,6 +251,7 @@ export class CursorSdkEventDebugSink {
250
251
  timeline: {} as Record<string, number>,
251
252
  errors: 0,
252
253
  };
254
+ private readonly sessionFile: string | undefined;
253
255
  private metadata: Record<string, unknown>;
254
256
  private readonly jsonlBuffers = new Map<string, string[]>();
255
257
  private readonly jsonlBufferBytes = new Map<string, number>();
@@ -263,8 +265,9 @@ export class CursorSdkEventDebugSink {
263
265
  static maybeCreate(options: CursorSdkEventDebugSinkOptions): CursorSdkEventDebugSink | undefined {
264
266
  const env = options.env ?? process.env;
265
267
  if (!resolveCursorSdkEventDebugEnabled(env)) return undefined;
266
- const allocation = allocateCursorSdkEventDebugTurn(options.cwd, env);
267
- return new CursorSdkEventDebugSink(allocation, options, env);
268
+ const scope = options.scope ?? getCursorSessionScopeSnapshot();
269
+ const allocation = allocateCursorSdkEventDebugTurn(options.cwd, env, scope);
270
+ return new CursorSdkEventDebugSink(allocation, { ...options, scope }, env);
268
271
  }
269
272
 
270
273
  private constructor(
@@ -278,6 +281,7 @@ export class CursorSdkEventDebugSink {
278
281
  this.sessionKey = allocation.sessionKey;
279
282
  this.pinnedRun = allocation.pinnedRun;
280
283
  this.env = env;
284
+ this.sessionFile = options.scope?.sessionFile;
281
285
  this.metadata = {
282
286
  capturedAt: new Date().toISOString(),
283
287
  modelId: options.modelId,
@@ -285,7 +289,7 @@ export class CursorSdkEventDebugSink {
285
289
  cwd: options.cwd,
286
290
  sessionDir: allocation.sessionDir,
287
291
  sessionKey: allocation.sessionKey,
288
- sessionFile: getCursorSessionFile(),
292
+ sessionFile: this.sessionFile,
289
293
  turn: allocation.turn,
290
294
  pinnedRun: allocation.pinnedRun,
291
295
  artifacts: ARTIFACTS,
@@ -452,7 +456,7 @@ export class CursorSdkEventDebugSink {
452
456
  }
453
457
 
454
458
  private capturePiSessionSnapshot(): { copied: boolean; sessionFile?: string; reason?: string } {
455
- const sessionFile = getCursorSessionFile();
459
+ const sessionFile = this.sessionFile;
456
460
  if (!sessionFile) {
457
461
  return { copied: false, reason: "session file unknown" };
458
462
  }
@@ -509,7 +513,7 @@ export class CursorSdkEventDebugSink {
509
513
  artifactDir: this.artifactDir,
510
514
  sessionDir: this.sessionDir,
511
515
  sessionKey: this.sessionKey,
512
- sessionFile: getCursorSessionFile(),
516
+ sessionFile: this.sessionFile,
513
517
  turn: this.turn,
514
518
  elapsedMs: Date.now() - this.startedAt,
515
519
  counts: {
@@ -23,7 +23,7 @@ type GenericProcessEmit = (event: string | symbol, ...args: unknown[]) => boolea
23
23
  // ConnectRPC suppression remains scoped to active provider turns.
24
24
  const activeProviderTurns = new Set<CursorSdkProcessErrorGuardToken>();
25
25
  const activeSessions = new Set<CursorSdkSessionProcessErrorGuardToken>();
26
- let activeLifecycleSessionGuard: CursorSdkSessionProcessErrorGuard | undefined;
26
+ const lifecycleSessionGuards = new Map<object, CursorSdkSessionProcessErrorGuard>();
27
27
  let originalProcessEmit: GenericProcessEmit | undefined;
28
28
  let cursorProcessEmit: GenericProcessEmit | undefined;
29
29
  let bunUnhandledRejectionListenerInstalled = false;
@@ -143,8 +143,8 @@ export const __testUtils = {
143
143
  activeProviderTurnCount: (): number => activeProviderTurns.size,
144
144
  activeSessionCount: (): number => activeSessions.size,
145
145
  resetLifecycleSessionGuard(): void {
146
- activeLifecycleSessionGuard?.dispose();
147
- activeLifecycleSessionGuard = undefined;
146
+ for (const guard of lifecycleSessionGuards.values()) guard.dispose();
147
+ lifecycleSessionGuards.clear();
148
148
  },
149
149
  };
150
150
 
@@ -186,11 +186,11 @@ export function installCursorSdkSessionProcessErrorGuard(): CursorSdkSessionProc
186
186
 
187
187
  export function registerCursorSdkSessionProcessErrorGuard(pi: Pick<ExtensionAPI, "on">): void {
188
188
  pi.on("session_start", () => {
189
- activeLifecycleSessionGuard?.dispose();
190
- activeLifecycleSessionGuard = installCursorSdkSessionProcessErrorGuard();
189
+ lifecycleSessionGuards.get(pi)?.dispose();
190
+ lifecycleSessionGuards.set(pi, installCursorSdkSessionProcessErrorGuard());
191
191
  });
192
192
  pi.on("session_shutdown", () => {
193
- activeLifecycleSessionGuard?.dispose();
194
- activeLifecycleSessionGuard = undefined;
193
+ lifecycleSessionGuards.get(pi)?.dispose();
194
+ lifecycleSessionGuards.delete(pi);
195
195
  });
196
196
  }
@@ -1,5 +1,19 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { resolveCursorSdkPlatformPackageDirectory } from "./cursor-ripgrep-path.js";
4
+
1
5
  export type CursorSdkModule = typeof import("@cursor/sdk");
2
6
 
3
7
  export async function loadCursorSdk(): Promise<CursorSdkModule> {
8
+ // The SDK searches launcher ancestors, which miss Pi's separately installed extension packages.
9
+ if (process.env.CURSOR_TREE_SITTER_VENDOR_DIR === undefined) {
10
+ const packageDirectory = resolveCursorSdkPlatformPackageDirectory();
11
+ if (packageDirectory) {
12
+ const vendor = join(packageDirectory, "vendor");
13
+ if (existsSync(join(vendor, "tree-sitter", "index.js")) && existsSync(join(vendor, "tree-sitter-bash", "index.js"))) {
14
+ process.env.CURSOR_TREE_SITTER_VENDOR_DIR = vendor;
15
+ }
16
+ }
17
+ }
4
18
  return import("@cursor/sdk");
5
19
  }
@@ -4,7 +4,7 @@ import { asRecord, getString } from "./cursor-record-utils.js";
4
4
  import { fsyncExistingRegularFile } from "./cursor-durable-fs.js";
5
5
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
6
6
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
7
- import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
7
+ import { cursorSessionScopeKeyForManager } from "./cursor-session-scope.js";
8
8
  import {
9
9
  CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE,
10
10
  isCursorLocalAgentId,
@@ -96,7 +96,7 @@ function getCurrentCleanupScope(ctx: LocalResumeCleanupCommandContext): CursorSe
96
96
  const sessionId = ctx.sessionManager.getSessionId();
97
97
  const repoRoot = resolveCursorSessionRepoRoot(ctx.cwd);
98
98
  return {
99
- scopeKey: getCursorSessionScopeKey(),
99
+ scopeKey: cursorSessionScopeKeyForManager(ctx.sessionManager),
100
100
  ...(sessionFile ? { sessionFile } : {}),
101
101
  ...(sessionId ? { sessionId } : {}),
102
102
  cwd: ctx.cwd,
@@ -4,9 +4,10 @@ import type {
4
4
  SessionCompactEvent,
5
5
  SessionShutdownEvent,
6
6
  SessionTreeEvent,
7
+ SessionStartEvent,
7
8
  } from "@earendil-works/pi-coding-agent";
8
9
  import { clearCursorSdkHttp1 } from "./cursor-http1.js";
9
- import { onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
10
+ import { cursorSessionScopeKeyForManager, hasLiveCursorSessions, onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
10
11
  import {
11
12
  disposeSessionCursorAgent,
12
13
  invalidateSessionAgent,
@@ -14,6 +15,7 @@ import {
14
15
  } from "./cursor-session-agent.js";
15
16
 
16
17
  export interface CursorSessionAgentLifecycleExtensionApi {
18
+ on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
17
19
  on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
18
20
  on(event: "session_compact", handler: ExtensionHandler<SessionCompactEvent>): void;
19
21
  on(event: "session_before_tree", handler: ExtensionHandler<SessionBeforeTreeEvent>): void;
@@ -22,30 +24,33 @@ export interface CursorSessionAgentLifecycleExtensionApi {
22
24
  }
23
25
 
24
26
  export function registerCursorSessionAgentLifecycle(pi: CursorSessionAgentLifecycleExtensionApi): void {
25
- onCursorSessionScopeKeyChange(async (previousScopeKey) => {
27
+ let scopeKey: string | undefined;
28
+ pi.on("session_start", (_event, ctx) => { scopeKey = cursorSessionScopeKeyForManager(ctx.sessionManager); });
29
+ onCursorSessionScopeKeyChange(pi, async (previousScopeKey) => {
26
30
  await disposeSessionCursorAgent(previousScopeKey);
27
31
  });
28
32
  pi.on("session_shutdown", async (event) => {
29
33
  try {
34
+ if (scopeKey === undefined) return;
30
35
  if (event.reason === "reload") {
31
- await resetSessionCursorAgent();
36
+ await resetSessionCursorAgent(scopeKey);
32
37
  return;
33
38
  }
34
- await disposeSessionCursorAgent();
39
+ await disposeSessionCursorAgent(scopeKey);
35
40
  } finally {
36
- clearCursorSdkHttp1();
41
+ if (!hasLiveCursorSessions()) clearCursorSdkHttp1();
37
42
  }
38
43
  });
39
44
  pi.on("session_compact", () => {
40
- invalidateSessionAgent();
45
+ if (scopeKey !== undefined) invalidateSessionAgent(scopeKey);
41
46
  });
42
47
  pi.on("session_before_tree", () => {
43
- invalidateSessionAgent();
48
+ if (scopeKey !== undefined) invalidateSessionAgent(scopeKey);
44
49
  });
45
50
  pi.on("session_tree", async () => {
46
- await resetSessionCursorAgent();
51
+ if (scopeKey !== undefined) await resetSessionCursorAgent(scopeKey);
47
52
  });
48
53
  pi.on("model_select", () => {
49
- invalidateSessionAgent();
54
+ if (scopeKey !== undefined) invalidateSessionAgent(scopeKey);
50
55
  });
51
56
  }