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
@@ -2,7 +2,8 @@ import { registerCursorCloudLifecycleLedger, runCursorCloudLifecycleCommand, } f
2
2
  import { CURSOR_AUTO_REVIEW_ENV, CURSOR_CLOUD_ACK_ENV, CURSOR_CLOUD_ALLOW_LOCAL_STATE_ENV, CURSOR_CLOUD_AUTO_CREATE_PR_ENV, CURSOR_CLOUD_BRANCH_ENV, CURSOR_CLOUD_CONTEXT_ENV, CURSOR_CLOUD_DIRECT_PUSH_ENV, CURSOR_CLOUD_ENV_ENV, CURSOR_CLOUD_ENV_FROM_FILES_ENV, CURSOR_CLOUD_ENV_NAME_ENV, CURSOR_CLOUD_ENV_TYPE_ENV, CURSOR_CLOUD_REPO_ENV, CURSOR_CLOUD_SKIP_REVIEWER_REQUEST_ENV, CURSOR_LOCAL_FORCE_ENV, CURSOR_LOCAL_RESUME_ENV, CURSOR_RUNTIME_ENV, CURSOR_SANDBOX_ENV, getCursorSdkProjectConfigPath, getCursorSdkUserConfigPath, loadCursorSdkConfig, mergeCursorSdkConfigForUpdate, parseExplicitCursorCloudEnvNames, resolveCursorSdkConfig, updateCursorSdkConfig, } from "./cursor-config.js";
3
3
  import { asRecord } from "./cursor-record-utils.js";
4
4
  import { getResolvedSessionCursorHttp1Enabled } from "./cursor-http1.js";
5
- import { getCursorSessionCwd, getCursorSessionProjectTrusted } from "./cursor-session-scope.js";
5
+ import { getCursorSessionScopeSnapshot } from "./cursor-session-scope.js";
6
+ import { cursorSettingsScopeForContext, getCursorSessionSettings } from "./cursor-session-settings.js";
6
7
  export const CURSOR_RUNTIME_ENTRY_TYPE = "cursor-runtime-state";
7
8
  export const CURSOR_CLOUD_ACK_DISCLOSURE = [
8
9
  "Cursor Cloud executes this work remotely.",
@@ -12,11 +13,7 @@ export const CURSOR_CLOUD_ACK_DISCLOSURE = [
12
13
  "Cloud agents remain until you archive or delete them.",
13
14
  "Cloud Agents run in Max Mode, are billed at Cursor API pricing, and may require spend-limit setup.",
14
15
  ].join("\n\n");
15
- let cliCursorSnapshot = { config: {} };
16
- let cliLocalForceConsumed = false;
17
16
  let envLocalForceConsumed = false;
18
- let sessionCursorRuntime;
19
- let sessionCursorCloudAcknowledged = false;
20
17
  function isCursorRuntimeEntryData(value) {
21
18
  const record = asRecord(value);
22
19
  if (!record)
@@ -27,15 +24,17 @@ function isCursorRuntimeEntryData(value) {
27
24
  function stringFlagValue(value) {
28
25
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
29
26
  }
30
- export function getCursorCliConfig() {
31
- const config = structuredClone(cliCursorSnapshot.config);
32
- const envNames = parseExplicitCursorCloudEnvNames(cliCursorSnapshot.cloudEnvNames, "--cursor-cloud-env");
27
+ export function getCursorCliConfig(scopeKey) {
28
+ const cli = getCursorSessionSettings(scopeKey).cli;
29
+ const config = structuredClone(cli.config);
30
+ const envNames = parseExplicitCursorCloudEnvNames(cli.cloudEnvNames, "--cursor-cloud-env");
33
31
  if (envNames)
34
32
  config.cloud = { ...config.cloud, envNames };
35
33
  return config;
36
34
  }
37
- export function getCursorSessionConfig() {
38
- const useHttp1ForAgent = getResolvedSessionCursorHttp1Enabled();
35
+ export function getCursorSessionConfig(scopeKey) {
36
+ const { runtime: sessionCursorRuntime, cloudAcknowledged: sessionCursorCloudAcknowledged } = getCursorSessionSettings(scopeKey);
37
+ const useHttp1ForAgent = getResolvedSessionCursorHttp1Enabled(scopeKey);
39
38
  return {
40
39
  ...(sessionCursorRuntime
41
40
  ? {
@@ -49,8 +48,8 @@ export function getCursorSessionConfig() {
49
48
  export function resolveEffectiveCursorConfig(options) {
50
49
  const loadedConfig = loadCursorSdkConfig({ cwd: options.cwd, projectTrusted: options.projectTrusted === true });
51
50
  return resolveCursorSdkConfig({
52
- cli: getCursorCliConfig(),
53
- session: getCursorSessionConfig(),
51
+ cli: getCursorCliConfig(options.scopeKey),
52
+ session: getCursorSessionConfig(options.scopeKey),
54
53
  user: loadedConfig.user,
55
54
  project: loadedConfig.project,
56
55
  });
@@ -58,7 +57,10 @@ export function resolveEffectiveCursorConfig(options) {
58
57
  export function resolveEffectiveCursorConfigForContext(ctx) {
59
58
  return resolveEffectiveCursorConfig({
60
59
  cwd: ctx.cwd,
61
- projectTrusted: getCursorSessionCwd() === ctx.cwd && getCursorSessionProjectTrusted(),
60
+ projectTrusted: cursorSettingsScopeForContext(ctx) === undefined
61
+ ? getCursorSessionScopeSnapshot().cwd === ctx.cwd && getCursorSessionScopeSnapshot().projectTrusted
62
+ : getCursorSessionSettings(cursorSettingsScopeForContext(ctx)).projectTrusted,
63
+ scopeKey: cursorSettingsScopeForContext(ctx),
62
64
  });
63
65
  }
64
66
  export function resolveCursorStatusRuntime(ctx) {
@@ -90,15 +92,16 @@ export function formatCursorStatus(runtime, fast, mode, useHttp1ForAgent = false
90
92
  parts.push("plan");
91
93
  return parts.join(" · ");
92
94
  }
93
- export function consumeCursorLocalForceOverride(resolved) {
95
+ export function consumeCursorLocalForceOverride(resolved, scopeKey) {
94
96
  if (!resolved.value)
95
97
  return false;
96
- if (resolved.source === "cli" && !cliLocalForceConsumed) {
97
- if (cliCursorSnapshot.config.local) {
98
- const { force: _, ...local } = cliCursorSnapshot.config.local;
99
- cliCursorSnapshot.config.local = local;
98
+ const cli = getCursorSessionSettings(scopeKey).cli;
99
+ if (resolved.source === "cli" && !cli.localForceConsumed) {
100
+ if (cli.config.local) {
101
+ const { force: _, ...local } = cli.config.local;
102
+ cli.config.local = local;
100
103
  }
101
- cliLocalForceConsumed = true;
104
+ cli.localForceConsumed = true;
102
105
  return true;
103
106
  }
104
107
  if (resolved.source === "environment" && !envLocalForceConsumed) {
@@ -107,19 +110,21 @@ export function consumeCursorLocalForceOverride(resolved) {
107
110
  }
108
111
  return false;
109
112
  }
110
- export function restoreSessionCursorRuntimeState(branch) {
111
- sessionCursorRuntime = undefined;
112
- sessionCursorCloudAcknowledged = false;
113
+ export function restoreSessionCursorRuntimeState(branch, scopeKey) {
114
+ const state = getCursorSessionSettings(scopeKey);
115
+ state.runtime = undefined;
116
+ state.cloudAcknowledged = false;
113
117
  for (const entry of branch) {
114
118
  if (entry.type !== "custom" || entry.customType !== CURSOR_RUNTIME_ENTRY_TYPE)
115
119
  continue;
116
120
  if (isCursorRuntimeEntryData(entry.data)) {
117
- sessionCursorRuntime = entry.data.runtime;
118
- sessionCursorCloudAcknowledged ||= entry.data.cloudAcknowledged === true;
121
+ state.runtime = entry.data.runtime;
122
+ state.cloudAcknowledged ||= entry.data.cloudAcknowledged === true;
119
123
  }
120
124
  }
121
125
  }
122
- export function restoreCursorCliState(pi) {
126
+ export function restoreCursorCliState(pi, scopeKey) {
127
+ const cli = getCursorSessionSettings(scopeKey).cli;
123
128
  const runtime = stringFlagValue(pi.getFlag("cursor-runtime"));
124
129
  const repo = stringFlagValue(pi.getFlag("cursor-cloud-repo"));
125
130
  const branch = stringFlagValue(pi.getFlag("cursor-cloud-branch"));
@@ -144,30 +149,29 @@ export function restoreCursorCliState(pi) {
144
149
  const local = {
145
150
  ...(pi.getFlag("cursor-auto-review") === true ? { autoReview: true } : {}),
146
151
  ...(pi.getFlag("cursor-sandbox") === true ? { sandboxOptions: { enabled: true } } : {}),
147
- ...(!cliLocalForceConsumed && pi.getFlag("cursor-local-force") === true ? { force: true } : {}),
152
+ ...(!cli.localForceConsumed && pi.getFlag("cursor-local-force") === true ? { force: true } : {}),
148
153
  ...(pi.getFlag("cursor-no-local-resume") === true
149
154
  ? { resume: false }
150
155
  : pi.getFlag("cursor-local-resume") === true
151
156
  ? { resume: true }
152
157
  : {}),
153
158
  };
154
- cliCursorSnapshot = {
155
- config: {
156
- ...(runtime ? { runtime } : {}),
157
- ...(Object.keys(cloud).length ? { cloud } : {}),
158
- ...(Object.keys(local).length ? { local } : {}),
159
- },
160
- ...(cloudEnvNames ? { cloudEnvNames } : {}),
159
+ cli.config = {
160
+ ...(runtime ? { runtime } : {}),
161
+ ...(Object.keys(cloud).length ? { cloud } : {}),
162
+ ...(Object.keys(local).length ? { local } : {}),
161
163
  };
164
+ cli.cloudEnvNames = cloudEnvNames;
162
165
  }
163
- function persistCursorRuntimePreference(pi, runtime, cloudAcknowledged = false) {
164
- const acknowledged = sessionCursorCloudAcknowledged || cloudAcknowledged;
166
+ function persistCursorRuntimePreference(pi, runtime, cloudAcknowledged, scopeKey) {
167
+ const state = getCursorSessionSettings(scopeKey);
168
+ const acknowledged = state.cloudAcknowledged || cloudAcknowledged;
165
169
  pi.appendEntry(CURSOR_RUNTIME_ENTRY_TYPE, {
166
170
  runtime,
167
171
  ...(acknowledged ? { cloudAcknowledged: true } : {}),
168
172
  });
169
- sessionCursorRuntime = runtime;
170
- sessionCursorCloudAcknowledged = acknowledged;
173
+ state.runtime = runtime;
174
+ state.cloudAcknowledged = acknowledged;
171
175
  }
172
176
  function registerCursorRuntimeFlags(pi) {
173
177
  pi.registerFlag("cursor-runtime", {
@@ -277,6 +281,7 @@ function registerCursorRuntimeCommand(pi, refreshStatus) {
277
281
  pi.registerCommand("cursor-runtime", {
278
282
  description: "Set Cursor runtime for this session: local or cloud",
279
283
  handler: async (args, ctx) => {
284
+ const scopeKey = cursorSettingsScopeForContext(ctx);
280
285
  const usage = "Usage: /cursor-runtime local|cloud [--save-user|--save-project]";
281
286
  const tokens = args.trim().split(/\s+/).filter(Boolean);
282
287
  const raw = tokens[0];
@@ -298,7 +303,7 @@ function registerCursorRuntimeCommand(pi, refreshStatus) {
298
303
  ctx.ui.notify(`${currentResolution.message} Fix the explicit override before changing the session runtime.`, "error");
299
304
  return;
300
305
  }
301
- if (saveProject && (getCursorSessionCwd() !== ctx.cwd || !getCursorSessionProjectTrusted())) {
306
+ if (saveProject && (getCursorSessionScopeSnapshot(pi).cwd !== ctx.cwd || !getCursorSessionScopeSnapshot(pi).projectTrusted)) {
302
307
  ctx.ui.notify("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.", "error");
303
308
  return;
304
309
  }
@@ -328,7 +333,7 @@ function registerCursorRuntimeCommand(pi, refreshStatus) {
328
333
  }
329
334
  }
330
335
  try {
331
- persistCursorRuntimePreference(pi, raw, cloudAcknowledged);
336
+ persistCursorRuntimePreference(pi, raw, cloudAcknowledged, scopeKey);
332
337
  }
333
338
  catch (error) {
334
339
  const effectiveResolution = resolveCursorStatusRuntime(ctx);
@@ -372,9 +377,11 @@ export function registerCursorCloudRuntimeControls(pi, options) {
372
377
  });
373
378
  }
374
379
  export function resetCursorRuntimeStateForTests() {
375
- cliCursorSnapshot = { config: {} };
376
- cliLocalForceConsumed = false;
380
+ const cli = getCursorSessionSettings().cli;
381
+ cli.config = {};
382
+ cli.cloudEnvNames = undefined;
383
+ cli.localForceConsumed = false;
377
384
  envLocalForceConsumed = false;
378
- sessionCursorRuntime = undefined;
379
- sessionCursorCloudAcknowledged = false;
385
+ getCursorSessionSettings().runtime = undefined;
386
+ getCursorSessionSettings().cloudAcknowledged = false;
380
387
  }
@@ -1,86 +1,65 @@
1
- import { asRecord, getArray, getString } from "./cursor-record-utils.js";
2
- import { readCursorSdkTurnUsage } from "./cursor-usage-accounting.js";
3
- const BILLED_USAGE_TIMEOUT_MS = 5000;
4
- // ponytail: process-lifetime watermark of billed usage UUIDs per agentId; reset on process exit.
5
- // Upgrade to session-scoped storage if multi-day processes retain enough agent IDs to matter.
6
- const seenBilledRunIdsByAgent = new Map();
7
- export function isCursorSdkClientMintedRunId(runId) {
8
- return runId.startsWith("run-");
1
+ import { asRecord } from "./cursor-record-utils.js";
2
+ const fields = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "totalTokens"];
3
+ export function readCursorUsageTokens(value) {
4
+ const record = asRecord(value);
5
+ if (!record || fields.some(key => !Number.isSafeInteger(record[key]) || record[key] < 0))
6
+ return undefined;
7
+ if (record.reasoningTokens !== undefined && (!Number.isSafeInteger(record.reasoningTokens) || record.reasoningTokens < 0 || record.reasoningTokens > record.outputTokens))
8
+ return undefined;
9
+ return { inputTokens: record.inputTokens, outputTokens: record.outputTokens, cacheReadTokens: record.cacheReadTokens, cacheWriteTokens: record.cacheWriteTokens, totalTokens: record.totalTokens, ...(record.reasoningTokens === undefined ? {} : { reasoningTokens: record.reasoningTokens }) };
9
10
  }
10
- export function sumCursorSdkTurnUsage(usages) {
11
- if (usages.length === 0)
11
+ function readCost(value) {
12
+ const record = asRecord(value);
13
+ if (!record || ["rawCostCents", "chargedCents"].some(key => typeof record[key] !== "number" || !Number.isFinite(record[key]) || record[key] < 0))
12
14
  return undefined;
13
- return usages.reduce((total, usage) => ({
14
- inputTokens: total.inputTokens + usage.inputTokens,
15
- outputTokens: total.outputTokens + usage.outputTokens,
16
- cacheReadTokens: total.cacheReadTokens + usage.cacheReadTokens,
17
- cacheWriteTokens: total.cacheWriteTokens + usage.cacheWriteTokens,
18
- }));
15
+ return { rawCostCents: record.rawCostCents, chargedCents: record.chargedCents };
19
16
  }
20
- export function peekCursorBilledUsageRunIds(agentId) {
21
- return seenBilledRunIdsByAgent.get(agentId) ?? new Set();
17
+ function billedTokens(value) {
18
+ const usage = readCursorUsageTokens(value);
19
+ if (!usage || usage.totalTokens !== usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens)
20
+ return undefined;
21
+ return usage;
22
22
  }
23
- export function rememberCursorBilledUsageRunIds(agentId, runIds) {
24
- if (runIds.length === 0)
25
- return;
26
- let seen = seenBilledRunIdsByAgent.get(agentId);
27
- if (!seen) {
28
- seen = new Set();
29
- seenBilledRunIdsByAgent.set(agentId, seen);
23
+ export function readCursorBilledSnapshot(value) {
24
+ const record = asRecord(value);
25
+ const usage = billedTokens(record?.usage);
26
+ if (!record || !usage || !Array.isArray(record.runs))
27
+ return undefined;
28
+ const cost = record.cost === undefined ? undefined : readCost(record.cost);
29
+ if (record.cost !== undefined && !cost)
30
+ return undefined;
31
+ const runs = [];
32
+ const ids = new Set();
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))
38
+ return undefined;
39
+ ids.add(row.runId);
40
+ runs.push({ runId: row.runId, usage: tokens, ...(rowCost ? { cost: rowCost } : {}) });
30
41
  }
31
- for (const runId of runIds)
32
- seen.add(runId);
42
+ if (fields.some(key => runs.reduce((sum, row) => sum + row.usage[key], 0) > usage[key]))
43
+ return undefined;
44
+ return { usage, runs, ...(cost ? { cost } : {}) };
33
45
  }
34
- function withTimeout(promise, timeoutMs = BILLED_USAGE_TIMEOUT_MS) {
46
+ /** Fetch is only observation; the origin journal owns recognition. No run-ID guesses or private fallback. */
47
+ export async function fetchCursorSdkAgentUsage(agent) {
48
+ if (typeof agent.getUsage !== "function")
49
+ return { status: "unavailable", reason: "unsupported" };
35
50
  let timer;
36
- const timeout = new Promise((resolve) => {
37
- timer = setTimeout(() => resolve(undefined), timeoutMs);
38
- timer.unref?.();
39
- });
40
- return Promise.race([promise.catch(() => undefined), timeout]).finally(() => {
51
+ try {
52
+ const timeout = new Promise(resolve => {
53
+ timer = setTimeout(() => resolve({ status: "unavailable", reason: "timeout" }), 5000);
54
+ timer.unref?.();
55
+ });
56
+ return await Promise.race([Promise.resolve().then(() => agent.getUsage()).then(value => {
57
+ const snapshot = readCursorBilledSnapshot(value);
58
+ return snapshot ? { status: "observed-pending-settlement", snapshot } : { status: "unavailable", reason: "invalid-response" };
59
+ }, () => ({ status: "unavailable", reason: "request-failed" })), timeout]);
60
+ }
61
+ finally {
41
62
  if (timer)
42
63
  clearTimeout(timer);
43
- });
44
- }
45
- export function selectCursorBilledTurnUsage(agentUsage, options) {
46
- const runs = (getArray(asRecord(agentUsage), "runs") ?? []).flatMap((item) => {
47
- const record = asRecord(item);
48
- const runId = getString(record, "runId");
49
- const usage = readCursorSdkTurnUsage(record?.usage);
50
- return runId && usage ? [{ runId, usage }] : [];
51
- });
52
- if (options.runtime === "cloud" && options.runId) {
53
- const match = runs.find((run) => run.runId === options.runId);
54
- return match ? { turn: match.usage, runIds: [match.runId] } : { runIds: [] };
55
64
  }
56
- const unseen = runs.filter((run) => !options.seenRunIds?.has(run.runId));
57
- return { turn: sumCursorSdkTurnUsage(unseen.map((run) => run.usage)), runIds: unseen.map((run) => run.runId) };
58
- }
59
- export async function fetchCursorSdkAgentUsage(agent, options) {
60
- if (typeof agent.getUsage !== "function")
61
- return undefined;
62
- const query = options.runtime === "cloud" && options.runId && isCursorSdkClientMintedRunId(options.runId)
63
- ? { runId: options.runId }
64
- : undefined;
65
- return withTimeout(Promise.resolve(agent.getUsage(query)));
66
- }
67
- export async function attachCursorSdkBilledTurnUsage(options) {
68
- const agentUsage = await fetchCursorSdkAgentUsage(options.agent, {
69
- runtime: options.runtime,
70
- runId: options.runId,
71
- });
72
- if (!agentUsage)
73
- return {};
74
- const selected = selectCursorBilledTurnUsage(agentUsage, {
75
- runtime: options.runtime,
76
- runId: options.runtime === "cloud" ? options.runId : undefined,
77
- seenRunIds: peekCursorBilledUsageRunIds(options.agentId),
78
- });
79
- rememberCursorBilledUsageRunIds(options.agentId, selected.runIds);
80
- return { agentUsage, turn: selected.turn };
81
65
  }
82
- export const __testUtils = {
83
- reset() {
84
- seenBilledRunIdsByAgent.clear();
85
- },
86
- };
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { basename, join, resolve } from "node:path";
4
4
  import { CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV, CURSOR_SDK_EVENT_DEBUG_SESSION_DIR_ENV, SESSION_MANIFEST, resolveCursorSdkEventDebugBaseDir, } from "./cursor-sdk-event-debug-constants.js";
5
- import { getCursorSessionFile, getCursorSessionScopeKey } from "./cursor-session-scope.js";
5
+ import { getCursorSessionScopeSnapshot } from "./cursor-session-scope.js";
6
6
  const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__";
7
7
  const sessionDebugStates = new Map();
8
8
  function sanitizePathSegment(value) {
@@ -46,12 +46,12 @@ function resolveSessionDebugDir(cwd, env, scopeKey) {
46
46
  return resolve(pinned);
47
47
  return join(resolveCursorSdkEventDebugBaseDir(cwd, env), "sessions", slugSessionKey(scopeKey));
48
48
  }
49
- export function allocateCursorSdkEventDebugTurn(cwd, env) {
49
+ export function allocateCursorSdkEventDebugTurn(cwd, env, scope = getCursorSessionScopeSnapshot()) {
50
50
  const pinnedRunDir = resolvePinnedRunArtifactDir(env[CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV]);
51
51
  if (pinnedRunDir) {
52
52
  return { artifactDir: pinnedRunDir, pinnedRun: true };
53
53
  }
54
- const scopeKey = getCursorSessionScopeKey();
54
+ const scopeKey = scope.scopeKey;
55
55
  const sessionDir = resolveSessionDebugDir(cwd, env, scopeKey);
56
56
  mkdirSync(sessionDir, { recursive: true });
57
57
  let state = sessionDebugStates.get(scopeKey);
@@ -67,13 +67,13 @@ export function allocateCursorSdkEventDebugTurn(cwd, env) {
67
67
  const existing = readSessionManifest(sessionDir);
68
68
  const manifest = existing ?? {
69
69
  sessionKey: scopeKey,
70
- sessionFile: getCursorSessionFile(),
70
+ sessionFile: scope.sessionFile,
71
71
  sessionDir,
72
72
  createdAt: new Date().toISOString(),
73
73
  updatedAt: new Date().toISOString(),
74
74
  turns: [],
75
75
  };
76
- manifest.sessionFile = getCursorSessionFile();
76
+ manifest.sessionFile = scope.sessionFile;
77
77
  manifest.updatedAt = new Date().toISOString();
78
78
  manifest.turns.push({
79
79
  turn: state.turnCounter,
@@ -99,7 +99,6 @@ export function updateCursorSdkEventDebugSessionManifest(sessionDir, artifactDir
99
99
  turnEntry.finalizedAt = new Date().toISOString();
100
100
  turnEntry.summary = summary;
101
101
  manifest.updatedAt = new Date().toISOString();
102
- manifest.sessionFile = getCursorSessionFile();
103
102
  writeSessionManifest(sessionDir, manifest);
104
103
  }
105
104
  export function resetCursorSdkEventDebugSessionStateForTests() {
@@ -3,7 +3,7 @@ import { copyFileSync, existsSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { serializeCursorPiToolBridgeDiagnostic } from "./cursor-pi-tool-bridge-diagnostics.js";
5
5
  import { asRecord } from "./cursor-record-utils.js";
6
- import { getCursorSessionFile } from "./cursor-session-scope.js";
6
+ import { getCursorSessionScopeSnapshot } from "./cursor-session-scope.js";
7
7
  import { parseEnvBoolean } from "./cursor-env-boolean.js";
8
8
  import { ARTIFACTS, CURSOR_SDK_EVENT_DEBUG_ENV, CURSOR_SDK_EVENT_DEBUG_LOG_PREFIX, CURSOR_SDK_EVENT_DEBUG_STDERR_ENV, SESSION_MANIFEST, SESSION_PI_SESSION_SNAPSHOT, } from "./cursor-sdk-event-debug-constants.js";
9
9
  import { allocateCursorSdkEventDebugTurn, resetCursorSdkEventDebugSessionStateForTests, slugSessionKey, updateCursorSdkEventDebugSessionManifest, } from "./cursor-sdk-event-debug-session.js";
@@ -122,6 +122,7 @@ export class CursorSdkEventDebugSink {
122
122
  timeline: {},
123
123
  errors: 0,
124
124
  };
125
+ sessionFile;
125
126
  metadata;
126
127
  jsonlBuffers = new Map();
127
128
  jsonlBufferBytes = new Map();
@@ -135,8 +136,9 @@ export class CursorSdkEventDebugSink {
135
136
  const env = options.env ?? process.env;
136
137
  if (!resolveCursorSdkEventDebugEnabled(env))
137
138
  return undefined;
138
- const allocation = allocateCursorSdkEventDebugTurn(options.cwd, env);
139
- return new CursorSdkEventDebugSink(allocation, options, env);
139
+ const scope = options.scope ?? getCursorSessionScopeSnapshot();
140
+ const allocation = allocateCursorSdkEventDebugTurn(options.cwd, env, scope);
141
+ return new CursorSdkEventDebugSink(allocation, { ...options, scope }, env);
140
142
  }
141
143
  constructor(allocation, options, env) {
142
144
  this.artifactDir = allocation.artifactDir;
@@ -145,6 +147,7 @@ export class CursorSdkEventDebugSink {
145
147
  this.sessionKey = allocation.sessionKey;
146
148
  this.pinnedRun = allocation.pinnedRun;
147
149
  this.env = env;
150
+ this.sessionFile = options.scope?.sessionFile;
148
151
  this.metadata = {
149
152
  capturedAt: new Date().toISOString(),
150
153
  modelId: options.modelId,
@@ -152,7 +155,7 @@ export class CursorSdkEventDebugSink {
152
155
  cwd: options.cwd,
153
156
  sessionDir: allocation.sessionDir,
154
157
  sessionKey: allocation.sessionKey,
155
- sessionFile: getCursorSessionFile(),
158
+ sessionFile: this.sessionFile,
156
159
  turn: allocation.turn,
157
160
  pinnedRun: allocation.pinnedRun,
158
161
  artifacts: ARTIFACTS,
@@ -285,7 +288,7 @@ export class CursorSdkEventDebugSink {
285
288
  writeFileSync(join(this.artifactDir, ARTIFACTS.waitResult), `${JSON.stringify(result, null, 2)}\n`);
286
289
  }
287
290
  capturePiSessionSnapshot() {
288
- const sessionFile = getCursorSessionFile();
291
+ const sessionFile = this.sessionFile;
289
292
  if (!sessionFile) {
290
293
  return { copied: false, reason: "session file unknown" };
291
294
  }
@@ -342,7 +345,7 @@ export class CursorSdkEventDebugSink {
342
345
  artifactDir: this.artifactDir,
343
346
  sessionDir: this.sessionDir,
344
347
  sessionKey: this.sessionKey,
345
- sessionFile: getCursorSessionFile(),
348
+ sessionFile: this.sessionFile,
346
349
  turn: this.turn,
347
350
  elapsedMs: Date.now() - this.startedAt,
348
351
  counts: {
@@ -4,7 +4,7 @@ import { classifyCursorConnectError, isCursorSdkAbortConnectError, isCursorSdkCo
4
4
  // ConnectRPC suppression remains scoped to active provider turns.
5
5
  const activeProviderTurns = new Set();
6
6
  const activeSessions = new Set();
7
- let activeLifecycleSessionGuard;
7
+ const lifecycleSessionGuards = new Map();
8
8
  let originalProcessEmit;
9
9
  let cursorProcessEmit;
10
10
  let bunUnhandledRejectionListenerInstalled = false;
@@ -125,8 +125,9 @@ export const __testUtils = {
125
125
  activeProviderTurnCount: () => activeProviderTurns.size,
126
126
  activeSessionCount: () => activeSessions.size,
127
127
  resetLifecycleSessionGuard() {
128
- activeLifecycleSessionGuard?.dispose();
129
- activeLifecycleSessionGuard = undefined;
128
+ for (const guard of lifecycleSessionGuards.values())
129
+ guard.dispose();
130
+ lifecycleSessionGuards.clear();
130
131
  },
131
132
  };
132
133
  export { isCursorSdkAbortConnectError };
@@ -167,11 +168,11 @@ export function installCursorSdkSessionProcessErrorGuard() {
167
168
  }
168
169
  export function registerCursorSdkSessionProcessErrorGuard(pi) {
169
170
  pi.on("session_start", () => {
170
- activeLifecycleSessionGuard?.dispose();
171
- activeLifecycleSessionGuard = installCursorSdkSessionProcessErrorGuard();
171
+ lifecycleSessionGuards.get(pi)?.dispose();
172
+ lifecycleSessionGuards.set(pi, installCursorSdkSessionProcessErrorGuard());
172
173
  });
173
174
  pi.on("session_shutdown", () => {
174
- activeLifecycleSessionGuard?.dispose();
175
- activeLifecycleSessionGuard = undefined;
175
+ lifecycleSessionGuards.get(pi)?.dispose();
176
+ lifecycleSessionGuards.delete(pi);
176
177
  });
177
178
  }
@@ -1,3 +1,16 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { resolveCursorSdkPlatformPackageDirectory } from "./cursor-ripgrep-path.js";
1
4
  export async function loadCursorSdk() {
5
+ // The SDK searches launcher ancestors, which miss Pi's separately installed extension packages.
6
+ if (process.env.CURSOR_TREE_SITTER_VENDOR_DIR === undefined) {
7
+ const packageDirectory = resolveCursorSdkPlatformPackageDirectory();
8
+ if (packageDirectory) {
9
+ const vendor = join(packageDirectory, "vendor");
10
+ if (existsSync(join(vendor, "tree-sitter", "index.js")) && existsSync(join(vendor, "tree-sitter-bash", "index.js"))) {
11
+ process.env.CURSOR_TREE_SITTER_VENDOR_DIR = vendor;
12
+ }
13
+ }
14
+ }
2
15
  return import("@cursor/sdk");
3
16
  }
@@ -2,7 +2,7 @@ import { asRecord, getString } from "./cursor-record-utils.js";
2
2
  import { fsyncExistingRegularFile } from "./cursor-durable-fs.js";
3
3
  import { scrubSensitiveText } from "./cursor-sensitive-text.js";
4
4
  import { loadCursorSdk } from "./cursor-sdk-runtime.js";
5
- import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
5
+ import { cursorSessionScopeKeyForManager } from "./cursor-session-scope.js";
6
6
  import { CURSOR_SESSION_AGENT_RESUME_ENTRY_TYPE, isCursorLocalAgentId, parseCursorSessionAgentResumeEntryData, readResumableCursorSessionAgentIds, resolveCursorSessionRepoRoot, } from "./cursor-session-agent-resume.js";
7
7
  import { cursorSessionStoreIdentitiesEqual, getCursorSessionStoreIdentities, openCursorSessionStore, } from "./cursor-session-store.js";
8
8
  export const CURSOR_SESSION_AGENT_CLEANUP_ENTRY_TYPE = "cursor-sdk-agent-cleanup";
@@ -41,7 +41,7 @@ function getCurrentCleanupScope(ctx) {
41
41
  const sessionId = ctx.sessionManager.getSessionId();
42
42
  const repoRoot = resolveCursorSessionRepoRoot(ctx.cwd);
43
43
  return {
44
- scopeKey: getCursorSessionScopeKey(),
44
+ scopeKey: cursorSessionScopeKeyForManager(ctx.sessionManager),
45
45
  ...(sessionFile ? { sessionFile } : {}),
46
46
  ...(sessionId ? { sessionId } : {}),
47
47
  cwd: ctx.cwd,
@@ -1,32 +1,41 @@
1
1
  import { clearCursorSdkHttp1 } from "./cursor-http1.js";
2
- import { onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
2
+ import { cursorSessionScopeKeyForManager, hasLiveCursorSessions, onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
3
3
  import { disposeSessionCursorAgent, invalidateSessionAgent, resetSessionCursorAgent, } from "./cursor-session-agent.js";
4
4
  export function registerCursorSessionAgentLifecycle(pi) {
5
- onCursorSessionScopeKeyChange(async (previousScopeKey) => {
5
+ let scopeKey;
6
+ pi.on("session_start", (_event, ctx) => { scopeKey = cursorSessionScopeKeyForManager(ctx.sessionManager); });
7
+ onCursorSessionScopeKeyChange(pi, async (previousScopeKey) => {
6
8
  await disposeSessionCursorAgent(previousScopeKey);
7
9
  });
8
10
  pi.on("session_shutdown", async (event) => {
9
11
  try {
12
+ if (scopeKey === undefined)
13
+ return;
10
14
  if (event.reason === "reload") {
11
- await resetSessionCursorAgent();
15
+ await resetSessionCursorAgent(scopeKey);
12
16
  return;
13
17
  }
14
- await disposeSessionCursorAgent();
18
+ await disposeSessionCursorAgent(scopeKey);
15
19
  }
16
20
  finally {
17
- clearCursorSdkHttp1();
21
+ if (!hasLiveCursorSessions())
22
+ clearCursorSdkHttp1();
18
23
  }
19
24
  });
20
25
  pi.on("session_compact", () => {
21
- invalidateSessionAgent();
26
+ if (scopeKey !== undefined)
27
+ invalidateSessionAgent(scopeKey);
22
28
  });
23
29
  pi.on("session_before_tree", () => {
24
- invalidateSessionAgent();
30
+ if (scopeKey !== undefined)
31
+ invalidateSessionAgent(scopeKey);
25
32
  });
26
33
  pi.on("session_tree", async () => {
27
- await resetSessionCursorAgent();
34
+ if (scopeKey !== undefined)
35
+ await resetSessionCursorAgent(scopeKey);
28
36
  });
29
37
  pi.on("model_select", () => {
30
- invalidateSessionAgent();
38
+ if (scopeKey !== undefined)
39
+ invalidateSessionAgent(scopeKey);
31
40
  });
32
41
  }