pi-cursor-sdk 0.1.36 → 0.1.38

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 (74) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/docs/cursor-model-ux-spec.md +1 -1
  3. package/docs/cursor-native-tool-replay.md +9 -9
  4. package/package.json +1 -1
  5. package/scripts/platform-smoke/card-detect.mjs +1 -1
  6. package/src/context-window-cache.ts +10 -14
  7. package/src/context.ts +1 -1
  8. package/src/cursor-agent-message-web-tools.ts +2 -1
  9. package/src/cursor-agents-context-registration.ts +18 -0
  10. package/src/cursor-agents-context.ts +21 -30
  11. package/src/cursor-edit-diff.ts +4 -2
  12. package/src/cursor-fallback-warning.ts +22 -0
  13. package/src/cursor-incomplete-tool-visibility.ts +5 -11
  14. package/src/cursor-live-run-coordinator.ts +1 -1
  15. package/src/cursor-mcp-timeout-override.ts +0 -2
  16. package/src/cursor-model-lifecycle.ts +72 -0
  17. package/src/cursor-native-replay-routing.ts +1 -1
  18. package/src/cursor-native-replay-trace.ts +1 -1
  19. package/src/cursor-native-tool-display-registration.ts +16 -28
  20. package/src/cursor-native-tool-display-replay.ts +12 -47
  21. package/src/cursor-native-tool-display-state.ts +1 -1
  22. package/src/cursor-native-tool-display-tools.ts +10 -18
  23. package/src/cursor-native-tool-names.ts +16 -0
  24. package/src/cursor-pi-tool-bridge-env.ts +12 -0
  25. package/src/cursor-pi-tool-bridge-mcp.ts +16 -21
  26. package/src/cursor-pi-tool-bridge-run.ts +5 -5
  27. package/src/cursor-pi-tool-bridge-server.ts +8 -3
  28. package/src/cursor-pi-tool-bridge-snapshot.ts +7 -13
  29. package/src/cursor-pi-tool-bridge.ts +7 -7
  30. package/src/cursor-provider-lazy.ts +51 -0
  31. package/src/cursor-provider-live-run-drain.ts +1 -1
  32. package/src/cursor-provider-run-finalizer.ts +5 -5
  33. package/src/cursor-provider-run-outcome.ts +0 -1
  34. package/src/cursor-provider-turn-coordinator.ts +4 -5
  35. package/src/cursor-provider-turn-display-router.ts +5 -1
  36. package/src/cursor-provider-turn-emit.ts +1 -1
  37. package/src/cursor-provider-turn-lifecycle-emitter.ts +1 -5
  38. package/src/cursor-provider-turn-prepare.ts +13 -9
  39. package/src/cursor-provider-turn-runner.ts +3 -11
  40. package/src/cursor-provider-turn-sdk-normalizer.ts +28 -5
  41. package/src/cursor-provider-turn-send.ts +7 -2
  42. package/src/cursor-provider-turn-types.ts +1 -3
  43. package/src/cursor-provider.ts +3 -2
  44. package/src/cursor-question-tool.ts +5 -18
  45. package/src/cursor-record-utils.ts +42 -0
  46. package/src/cursor-replay-activity-builders.ts +16 -122
  47. package/src/cursor-replay-tool-details.ts +57 -197
  48. package/src/cursor-sdk-event-debug.ts +6 -6
  49. package/src/cursor-sensitive-text.ts +4 -4
  50. package/src/cursor-session-agent-lifecycle.ts +47 -0
  51. package/src/cursor-session-agent.ts +9 -47
  52. package/src/cursor-session-scope.ts +23 -4
  53. package/src/cursor-setting-sources.ts +8 -8
  54. package/src/cursor-skill-tool.ts +25 -32
  55. package/src/cursor-state.ts +66 -45
  56. package/src/cursor-tool-lifecycle.ts +16 -9
  57. package/src/cursor-tool-presentation-registry.ts +42 -169
  58. package/src/cursor-tool-result-display-readers.ts +185 -0
  59. package/src/cursor-tool-transcript.ts +17 -33
  60. package/src/cursor-tool-visibility.ts +9 -1
  61. package/src/cursor-transcript-tool-formatters.ts +23 -172
  62. package/src/cursor-transcript-tool-specs.ts +17 -57
  63. package/src/cursor-transcript-utils.ts +2 -34
  64. package/src/cursor-usage-accounting.ts +0 -6
  65. package/src/cursor-web-tool-activity.ts +4 -12
  66. package/src/cursor-web-tool-args.ts +1 -9
  67. package/src/index.ts +15 -16
  68. package/src/model-discovery.ts +5 -4
  69. package/src/model-list-cache.ts +37 -38
  70. package/src/cursor-native-tool-display.ts +0 -10
  71. package/src/cursor-provider-turn-api-key.ts +0 -1
  72. package/src/cursor-provider-turn-message-offset.ts +0 -15
  73. package/src/cursor-session-cwd.ts +0 -28
  74. package/src/cursor-tool-names.ts +0 -18
@@ -1,3 +1,4 @@
1
+ import { asRecord, getBoolean, getNumber, getString } from "./cursor-record-utils.js";
1
2
  import { isCursorReplayActivitySourceName, type CursorReplayActivitySourceName } from "./cursor-replay-source-names.js";
2
3
 
3
4
  /** Replay detail variants keyed by replay card disposition, not SDK source tool alone. */
@@ -55,8 +56,6 @@ export interface CursorReplayGenerateImageDetails {
55
56
  imageMimeType?: string;
56
57
  summary?: string;
57
58
  expandedText?: string;
58
- /** Legacy parsed title retained on older payloads; display always uses `Cursor generateImage`. */
59
- title?: string;
60
59
  collapseDetailsByDefault?: boolean;
61
60
  }
62
61
 
@@ -79,7 +78,7 @@ export interface CursorReplayActivityDetails {
79
78
  fileContentAfterWrite?: string;
80
79
  }
81
80
 
82
- /** Parsed replay details without a display title (legacy or malformed payloads). */
81
+ /** Parsed replay details without a display title. */
83
82
  export interface CursorReplayGenericFallbackDetails {
84
83
  variant: "genericFallback";
85
84
  sourceToolName: CursorReplayUnknownSourceToolName;
@@ -94,21 +93,6 @@ export type CursorReplayToolDetails =
94
93
  | CursorReplayActivityDetails
95
94
  | CursorReplayGenericFallbackDetails;
96
95
 
97
- /** @deprecated Use {@link CursorReplayNativeEditDetails}. */
98
- export type CursorReplayEditDetails = CursorReplayNativeEditDetails;
99
-
100
- /** @deprecated Use {@link CursorReplayNativeWriteDetails}. */
101
- export type CursorReplayWriteDetails = CursorReplayNativeWriteDetails;
102
-
103
- /** @deprecated Use {@link CursorReplayActivityDetails}. */
104
- export type CursorReplayTitledActivityDetails = CursorReplayActivityDetails;
105
-
106
- /** @deprecated Use {@link CursorReplayActivitySourceToolName}. */
107
- export type CursorReplayActivityCursorToolName = CursorReplayActivitySourceToolName;
108
-
109
- /** @deprecated Use {@link CursorReplayUnknownSourceToolName}. */
110
- export type CursorReplayUnknownCursorToolName = CursorReplayUnknownSourceToolName;
111
-
112
96
  export type CursorReplayActivityDetailFields = Pick<
113
97
  CursorReplayActivityDetails,
114
98
  | "summary"
@@ -128,79 +112,51 @@ export type CursorReplayGenerateImageDetailFields = Pick<
128
112
  "summary" | "expandedText" | "imagePath" | "imageDisplayPath" | "imageMimeType"
129
113
  >;
130
114
 
131
- function isRecord(value: unknown): value is Record<string, unknown> {
132
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
133
- }
134
-
135
- function readOptionalString(record: Record<string, unknown>, key: string): string | undefined {
136
- const value = record[key];
137
- return typeof value === "string" ? value : undefined;
138
- }
139
-
140
- function readOptionalNumber(record: Record<string, unknown>, key: string): number | undefined {
141
- const value = record[key];
142
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
143
- }
144
-
145
- function readOptionalBoolean(record: Record<string, unknown>, key: string): boolean | undefined {
146
- const value = record[key];
147
- return typeof value === "boolean" ? value : undefined;
148
- }
149
-
150
- function readCurrentSourceToolName(record: Record<string, unknown>): string | undefined {
151
- const sourceToolName = readOptionalString(record, "sourceToolName");
115
+ function readSourceToolName(record: Record<string, unknown>): string | undefined {
116
+ const sourceToolName = getString(record, "sourceToolName");
152
117
  return sourceToolName?.trim() ? sourceToolName.trim() : undefined;
153
118
  }
154
119
 
155
- function readLegacySourceToolName(record: Record<string, unknown>): string | undefined {
156
- const sourceToolName = readCurrentSourceToolName(record);
157
- if (sourceToolName) return sourceToolName;
158
- const cursorToolName = readOptionalString(record, "cursorToolName");
159
- return cursorToolName?.trim() ? cursorToolName.trim() : undefined;
160
- }
161
-
162
- function readLegacyVariant(record: Record<string, unknown>): string | undefined {
163
- const variant = readOptionalString(record, "variant");
120
+ function readVariant(record: Record<string, unknown>): string | undefined {
121
+ const variant = getString(record, "variant");
164
122
  return variant?.trim() ? variant.trim() : undefined;
165
123
  }
166
124
 
167
125
  function parseCursorReplayNativeEditDetails(record: Record<string, unknown>): CursorReplayNativeEditDetails {
168
126
  return {
169
127
  variant: "nativeEdit",
170
- path: readOptionalString(record, "path"),
171
- linesAdded: readOptionalNumber(record, "linesAdded"),
172
- linesRemoved: readOptionalNumber(record, "linesRemoved"),
173
- diffString: readOptionalString(record, "diffString"),
174
- diff: readOptionalString(record, "diff"),
175
- firstChangedLine: readOptionalNumber(record, "firstChangedLine"),
176
- summary: readOptionalString(record, "summary"),
177
- expandedText: readOptionalString(record, "expandedText"),
128
+ path: getString(record, "path"),
129
+ linesAdded: getNumber(record, "linesAdded"),
130
+ linesRemoved: getNumber(record, "linesRemoved"),
131
+ diffString: getString(record, "diffString"),
132
+ diff: getString(record, "diff"),
133
+ firstChangedLine: getNumber(record, "firstChangedLine"),
134
+ summary: getString(record, "summary"),
135
+ expandedText: getString(record, "expandedText"),
178
136
  };
179
137
  }
180
138
 
181
139
  function parseCursorReplayNativeWriteDetails(record: Record<string, unknown>): CursorReplayNativeWriteDetails {
182
140
  return {
183
141
  variant: "nativeWrite",
184
- path: readOptionalString(record, "path"),
185
- linesCreated: readOptionalNumber(record, "linesCreated"),
186
- fileSize: readOptionalNumber(record, "fileSize"),
187
- fileContentAfterWrite: readOptionalString(record, "fileContentAfterWrite"),
188
- expandedText: readOptionalString(record, "expandedText"),
189
- summary: readOptionalString(record, "summary"),
142
+ path: getString(record, "path"),
143
+ linesCreated: getNumber(record, "linesCreated"),
144
+ fileSize: getNumber(record, "fileSize"),
145
+ fileContentAfterWrite: getString(record, "fileContentAfterWrite"),
146
+ expandedText: getString(record, "expandedText"),
147
+ summary: getString(record, "summary"),
190
148
  };
191
149
  }
192
150
 
193
151
  function parseCursorReplayGenerateImageDetails(record: Record<string, unknown>): CursorReplayGenerateImageDetails {
194
- const title = readOptionalString(record, "title");
195
- const collapseDetailsByDefault = readOptionalBoolean(record, "collapseDetailsByDefault");
152
+ const collapseDetailsByDefault = getBoolean(record, "collapseDetailsByDefault");
196
153
  return {
197
154
  variant: "generateImage",
198
- imagePath: readOptionalString(record, "imagePath"),
199
- imageDisplayPath: readOptionalString(record, "imageDisplayPath"),
200
- imageMimeType: readOptionalString(record, "imageMimeType"),
201
- summary: readOptionalString(record, "summary"),
202
- expandedText: readOptionalString(record, "expandedText"),
203
- ...(title !== undefined ? { title } : {}),
155
+ imagePath: getString(record, "imagePath"),
156
+ imageDisplayPath: getString(record, "imageDisplayPath"),
157
+ imageMimeType: getString(record, "imageMimeType"),
158
+ summary: getString(record, "summary"),
159
+ expandedText: getString(record, "expandedText"),
204
160
  ...(collapseDetailsByDefault !== undefined ? { collapseDetailsByDefault } : {}),
205
161
  };
206
162
  }
@@ -214,16 +170,16 @@ function parseCursorReplayActivityDetails(
214
170
  variant: "activity",
215
171
  sourceToolName,
216
172
  title,
217
- summary: readOptionalString(record, "summary"),
218
- expandedText: readOptionalString(record, "expandedText"),
219
- collapseDetailsByDefault: readOptionalBoolean(record, "collapseDetailsByDefault"),
220
- path: readOptionalString(record, "path"),
221
- fileSize: readOptionalNumber(record, "fileSize"),
222
- diffString: readOptionalString(record, "diffString"),
223
- diff: readOptionalString(record, "diff"),
224
- linesAdded: readOptionalNumber(record, "linesAdded"),
225
- linesRemoved: readOptionalNumber(record, "linesRemoved"),
226
- fileContentAfterWrite: readOptionalString(record, "fileContentAfterWrite"),
173
+ summary: getString(record, "summary"),
174
+ expandedText: getString(record, "expandedText"),
175
+ collapseDetailsByDefault: getBoolean(record, "collapseDetailsByDefault"),
176
+ path: getString(record, "path"),
177
+ fileSize: getNumber(record, "fileSize"),
178
+ diffString: getString(record, "diffString"),
179
+ diff: getString(record, "diff"),
180
+ linesAdded: getNumber(record, "linesAdded"),
181
+ linesRemoved: getNumber(record, "linesRemoved"),
182
+ fileContentAfterWrite: getString(record, "fileContentAfterWrite"),
227
183
  };
228
184
  }
229
185
 
@@ -238,8 +194,8 @@ function parseCursorReplayGenericFallbackDetails(
238
194
  return {
239
195
  variant: "genericFallback",
240
196
  sourceToolName: brandCursorReplayUnknownSourceToolName(sourceToolName),
241
- summary: readOptionalString(record, "summary"),
242
- expandedText: readOptionalString(record, "expandedText"),
197
+ summary: getString(record, "summary"),
198
+ expandedText: getString(record, "expandedText"),
243
199
  };
244
200
  }
245
201
 
@@ -263,38 +219,8 @@ export function resolveIncompleteReplayActivitySourceToolName(
263
219
  return resolveParseActivitySourceToolName(sourceToolName);
264
220
  }
265
221
 
266
- function hasNativeEditChanges(record: Record<string, unknown>): boolean {
267
- return Boolean(
268
- readOptionalString(record, "diffString")?.trim()
269
- || readOptionalString(record, "diff")?.trim()
270
- || readOptionalNumber(record, "linesAdded")
271
- || readOptionalNumber(record, "linesRemoved"),
272
- );
273
- }
274
-
275
- function parseLegacyEditDetails(record: Record<string, unknown>): CursorReplayToolDetails {
276
- const title = readOptionalString(record, "title")?.trim();
277
- if (title) {
278
- return parseCursorReplayActivityDetails(record, resolveParseActivitySourceToolName("edit"), title);
279
- }
280
- return parseCursorReplayNativeEditDetails(record);
281
- }
282
-
283
- function parseLegacyWriteDetails(record: Record<string, unknown>): CursorReplayToolDetails {
284
- const title = readOptionalString(record, "title")?.trim();
285
- if (title) {
286
- return parseCursorReplayActivityDetails(record, resolveParseActivitySourceToolName("write"), title);
287
- }
288
- return parseCursorReplayNativeWriteDetails(record);
289
- }
290
-
291
- type CursorReplayVariantParser = (record: Record<string, unknown>) => CursorReplayToolDetails | undefined;
292
-
293
- function parseActivityVariantDetails(
294
- record: Record<string, unknown>,
295
- readSourceToolName: (record: Record<string, unknown>) => string | undefined,
296
- ): CursorReplayActivityDetails | undefined {
297
- const title = readOptionalString(record, "title")?.trim();
222
+ function parseActivityVariantDetails(record: Record<string, unknown>): CursorReplayActivityDetails | undefined {
223
+ const title = getString(record, "title")?.trim();
298
224
  if (!title) return undefined;
299
225
  return parseCursorReplayActivityDetails(
300
226
  record,
@@ -303,79 +229,25 @@ function parseActivityVariantDetails(
303
229
  );
304
230
  }
305
231
 
306
- const CURRENT_REPLAY_VARIANT_PARSERS: Readonly<Record<CursorReplayToolDetailsVariant, CursorReplayVariantParser>> = {
307
- nativeEdit: parseCursorReplayNativeEditDetails,
308
- nativeWrite: parseCursorReplayNativeWriteDetails,
309
- generateImage: parseCursorReplayGenerateImageDetails,
310
- activity: (record) => {
311
- if (!readCurrentSourceToolName(record) && readOptionalString(record, "cursorToolName")?.trim()) return undefined;
312
- return parseActivityVariantDetails(record, readCurrentSourceToolName);
313
- },
314
- genericFallback: (record) => parseCursorReplayGenericFallbackDetails(record, readCurrentSourceToolName(record) ?? "tool"),
315
- };
316
-
317
- const LEGACY_REPLAY_VARIANT_UPGRADERS: Readonly<Record<string, CursorReplayVariantParser>> = {
318
- edit: parseLegacyEditDetails,
319
- write: parseLegacyWriteDetails,
320
- titledActivity: (record) => parseActivityVariantDetails(record, readLegacySourceToolName),
321
- };
322
-
323
- export function parseStrictCurrentCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
324
- if (!isRecord(value)) return undefined;
325
- const variant = readLegacyVariant(value);
326
- if (!variant) return undefined;
327
- return CURRENT_REPLAY_VARIANT_PARSERS[variant as CursorReplayToolDetailsVariant]?.(value);
328
- }
329
-
330
- export function upgradeLegacyCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
331
- if (!isRecord(value)) return undefined;
332
- const explicitVariant = readLegacyVariant(value);
333
- if (explicitVariant) {
334
- return LEGACY_REPLAY_VARIANT_UPGRADERS[explicitVariant]?.(value);
335
- }
336
- const sourceToolName = readLegacySourceToolName(value);
337
- if (sourceToolName === "edit") return parseLegacyEditDetails(value);
338
- if (sourceToolName === "write") return parseLegacyWriteDetails(value);
339
- if (sourceToolName === "generateImage") return parseCursorReplayGenerateImageDetails(value);
340
- const title = readOptionalString(value, "title")?.trim();
341
- if (title) {
342
- return parseCursorReplayActivityDetails(
343
- value,
344
- resolveParseActivitySourceToolName(sourceToolName ?? CURSOR_REPLAY_UNREGISTERED_ACTIVITY_TOOL_NAME),
345
- title,
346
- );
347
- }
348
- if (sourceToolName === undefined && hasNativeEditChanges(value)) {
349
- return parseCursorReplayNativeEditDetails(value);
350
- }
351
- return undefined;
352
- }
353
-
354
232
  export function parseCursorReplayToolDetails(value: unknown): CursorReplayToolDetails | undefined {
355
- return parseStrictCurrentCursorReplayToolDetails(value) ?? upgradeLegacyCursorReplayToolDetails(value);
356
- }
357
-
358
- /** @deprecated Prefer {@link parseCursorReplayToolDetails} for validated narrowing. */
359
- export const asCursorReplayToolDetails = parseCursorReplayToolDetails;
360
-
361
- export function buildCursorReplayNativeEditDetails(
362
- fields: Omit<CursorReplayNativeEditDetails, "variant">,
363
- ): CursorReplayNativeEditDetails {
364
- return { variant: "nativeEdit", ...fields };
365
- }
366
-
367
- /** @deprecated Prefer {@link buildCursorReplayNativeEditDetails}. */
368
- export const buildCursorReplayEditDetails = buildCursorReplayNativeEditDetails;
369
-
370
- export function buildCursorReplayNativeWriteDetails(
371
- fields: Omit<CursorReplayNativeWriteDetails, "variant">,
372
- ): CursorReplayNativeWriteDetails {
373
- return { variant: "nativeWrite", ...fields };
233
+ const record = asRecord(value);
234
+ if (!record) return undefined;
235
+ switch (readVariant(record)) {
236
+ case "nativeEdit":
237
+ return parseCursorReplayNativeEditDetails(record);
238
+ case "nativeWrite":
239
+ return parseCursorReplayNativeWriteDetails(record);
240
+ case "generateImage":
241
+ return parseCursorReplayGenerateImageDetails(record);
242
+ case "activity":
243
+ return parseActivityVariantDetails(record);
244
+ case "genericFallback":
245
+ return parseCursorReplayGenericFallbackDetails(record, readSourceToolName(record) ?? "tool");
246
+ default:
247
+ return undefined;
248
+ }
374
249
  }
375
250
 
376
- /** @deprecated Prefer {@link buildCursorReplayNativeWriteDetails}. */
377
- export const buildCursorReplayWriteDetails = buildCursorReplayNativeWriteDetails;
378
-
379
251
  export function assembleCursorReplayActivityDetails(
380
252
  sourceToolName: CursorReplayActivitySourceToolName,
381
253
  title: string,
@@ -402,10 +274,7 @@ export function assembleCursorReplayActivityDetails(
402
274
  };
403
275
  }
404
276
 
405
- /** @deprecated Prefer {@link assembleCursorReplayActivityDetails}. */
406
- export const assembleCursorReplayTitledActivityDetails = assembleCursorReplayActivityDetails;
407
-
408
- export const CURSOR_REPLAY_GENERATE_IMAGE_RESULT_TITLE = "Cursor generateImage" as const;
277
+ export const CURSOR_REPLAY_GENERATE_IMAGE_RESULT_TITLE = "Cursor image generation" as const;
409
278
 
410
279
  export function assembleCursorReplayGenerateImageDetails(
411
280
  fields: CursorReplayGenerateImageDetailFields,
@@ -430,18 +299,12 @@ export function isCursorReplayNativeEditDetails(
430
299
  return details.variant === "nativeEdit";
431
300
  }
432
301
 
433
- /** @deprecated Prefer {@link isCursorReplayNativeEditDetails}. */
434
- export const isCursorReplayEditDetails = isCursorReplayNativeEditDetails;
435
-
436
302
  export function isCursorReplayNativeWriteDetails(
437
303
  details: CursorReplayToolDetails,
438
304
  ): details is CursorReplayNativeWriteDetails {
439
305
  return details.variant === "nativeWrite";
440
306
  }
441
307
 
442
- /** @deprecated Prefer {@link isCursorReplayNativeWriteDetails}. */
443
- export const isCursorReplayWriteDetails = isCursorReplayNativeWriteDetails;
444
-
445
308
  export function isCursorReplayGenerateImageDetails(
446
309
  details: CursorReplayToolDetails,
447
310
  ): details is CursorReplayGenerateImageDetails {
@@ -454,9 +317,6 @@ export function isCursorReplayActivityDetails(
454
317
  return details.variant === "activity";
455
318
  }
456
319
 
457
- /** @deprecated Prefer {@link isCursorReplayActivityDetails}. */
458
- export const isCursorReplayTitledActivityDetails = isCursorReplayActivityDetails;
459
-
460
320
  export function isCursorReplayGenericFallbackDetails(
461
321
  details: CursorReplayToolDetails,
462
322
  ): details is CursorReplayGenericFallbackDetails {
@@ -7,6 +7,7 @@ import type { CursorPiToolBridgeDiagnosticEvent } from "./cursor-pi-tool-bridge-
7
7
  import { serializeCursorPiToolBridgeDiagnostic } from "./cursor-pi-tool-bridge-diagnostics.js";
8
8
  import type { CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge-types.js";
9
9
  import type { CursorLiveQueuedEvent } from "./cursor-live-run-coordinator.js";
10
+ import { asRecord } from "./cursor-record-utils.js";
10
11
  import { getCursorSessionFile } from "./cursor-session-scope.js";
11
12
  import { parseEnvBoolean } from "./cursor-env-boolean.js";
12
13
  import {
@@ -93,11 +94,10 @@ interface CursorSdkRunLike {
93
94
  }
94
95
 
95
96
  function eventType(value: unknown): string {
96
- if (value && typeof value === "object") {
97
- if ("type" in value && typeof value.type === "string") return value.type;
98
- if ("event" in value && typeof value.event === "string") return value.event;
99
- if ("kind" in value && typeof value.kind === "string") return value.kind;
100
- }
97
+ const record = asRecord(value);
98
+ if (typeof record?.type === "string") return record.type;
99
+ if (typeof record?.event === "string") return record.event;
100
+ if (typeof record?.kind === "string") return record.kind;
101
101
  return "unknown";
102
102
  }
103
103
 
@@ -106,7 +106,7 @@ function resolveCursorSdkEventDebugStderrEnabled(env: Record<string, string | un
106
106
  }
107
107
 
108
108
  function isNodeErrorWithCode(error: unknown, code: string): boolean {
109
- return typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === code;
109
+ return asRecord(error)?.code === code;
110
110
  }
111
111
 
112
112
  function snapshotCursorSdkEventDebugRecord(record: unknown): unknown {
@@ -1,3 +1,4 @@
1
+ import { asRecord } from "./cursor-record-utils.js";
1
2
  import type { CursorPiToolDisplay } from "./cursor-transcript-utils.js";
2
3
  /** Provider-facing wrapper; canonical scrubbing lives in shared/cursor-sensitive-text.mjs. */
3
4
  import { scrubSensitiveText as scrubSensitiveTextJs } from "../shared/cursor-sensitive-text.mjs";
@@ -9,10 +10,9 @@ export function scrubSensitiveText(text: string, apiKey?: string): string {
9
10
  function scrubDisplayValue(value: unknown, apiKey?: string): unknown {
10
11
  if (typeof value === "string") return scrubSensitiveText(value, apiKey);
11
12
  if (Array.isArray(value)) return value.map((entry) => scrubDisplayValue(entry, apiKey));
12
- if (value && typeof value === "object") {
13
- return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, scrubDisplayValue(entry, apiKey)]));
14
- }
15
- return value;
13
+ const record = asRecord(value);
14
+ if (!record) return value;
15
+ return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, scrubDisplayValue(entry, apiKey)]));
16
16
  }
17
17
 
18
18
  export function scrubPiToolDisplay(display: CursorPiToolDisplay, apiKey?: string): CursorPiToolDisplay {
@@ -0,0 +1,47 @@
1
+ import type {
2
+ ExtensionHandler,
3
+ SessionBeforeTreeEvent,
4
+ SessionCompactEvent,
5
+ SessionShutdownEvent,
6
+ SessionTreeEvent,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import { onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
9
+
10
+ export interface CursorSessionAgentLifecycleExtensionApi {
11
+ on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
12
+ on(event: "session_compact", handler: ExtensionHandler<SessionCompactEvent>): void;
13
+ on(event: "session_before_tree", handler: ExtensionHandler<SessionBeforeTreeEvent>): void;
14
+ on(event: "session_tree", handler: ExtensionHandler<SessionTreeEvent>): void;
15
+ on(event: "model_select", handler: () => Promise<void> | void): void;
16
+ }
17
+
18
+ export function registerCursorSessionAgentLifecycle(pi: CursorSessionAgentLifecycleExtensionApi): void {
19
+ onCursorSessionScopeKeyChange(async (previousScopeKey) => {
20
+ const { disposeSessionCursorAgent } = await import("./cursor-session-agent.js");
21
+ await disposeSessionCursorAgent(previousScopeKey);
22
+ });
23
+ pi.on("session_shutdown", async (event) => {
24
+ const { disposeSessionCursorAgent, resetSessionCursorAgent } = await import("./cursor-session-agent.js");
25
+ if (event.reason === "reload") {
26
+ await resetSessionCursorAgent();
27
+ return;
28
+ }
29
+ await disposeSessionCursorAgent();
30
+ });
31
+ pi.on("session_compact", async () => {
32
+ const { invalidateSessionAgent } = await import("./cursor-session-agent.js");
33
+ invalidateSessionAgent();
34
+ });
35
+ pi.on("session_before_tree", async () => {
36
+ const { invalidateSessionAgent } = await import("./cursor-session-agent.js");
37
+ invalidateSessionAgent();
38
+ });
39
+ pi.on("session_tree", async () => {
40
+ const { resetSessionCursorAgent } = await import("./cursor-session-agent.js");
41
+ await resetSessionCursorAgent();
42
+ });
43
+ pi.on("model_select", async () => {
44
+ const { invalidateSessionAgent } = await import("./cursor-session-agent.js");
45
+ invalidateSessionAgent();
46
+ });
47
+ }
@@ -1,10 +1,3 @@
1
- import type {
2
- ExtensionHandler,
3
- SessionBeforeTreeEvent,
4
- SessionCompactEvent,
5
- SessionShutdownEvent,
6
- SessionTreeEvent,
7
- } from "@earendil-works/pi-coding-agent";
8
1
  import { createHash } from "node:crypto";
9
2
  import type { AgentModeOption, ModelSelection, SDKAgent, SettingSource } from "@cursor/sdk";
10
3
  import type { Context } from "@earendil-works/pi-ai";
@@ -14,7 +7,7 @@ import {
14
7
  type CursorPiToolBridgeRun,
15
8
  } from "./cursor-pi-tool-bridge.js";
16
9
  import { computeCursorContextFingerprint } from "./context.js";
17
- import { getCursorSessionScopeKey, onCursorSessionScopeKeyChange } from "./cursor-session-scope.js";
10
+ import { getCursorSessionScopeGeneration, getCursorSessionScopeKey } from "./cursor-session-scope.js";
18
11
  import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
19
12
  import { loadCursorSdk, type CursorSdkModule } from "./cursor-sdk-runtime.js";
20
13
 
@@ -88,9 +81,12 @@ export class SessionCursorAgentScopeClosedError extends Error {
88
81
  }
89
82
 
90
83
  function assertScopeAcceptsAcquire(scopeKey: string): void {
91
- if (terminalDisposedScopeKeys.has(scopeKey)) {
84
+ const terminalGeneration = terminalDisposedScopeGenerations.get(scopeKey);
85
+ if (terminalGeneration === undefined) return;
86
+ if (terminalGeneration >= getCursorSessionScopeGeneration(scopeKey)) {
92
87
  throw new SessionCursorAgentScopeClosedError();
93
88
  }
89
+ terminalDisposedScopeGenerations.delete(scopeKey);
94
90
  }
95
91
 
96
92
  function rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey: string, poolKey: string, error: unknown): void {
@@ -112,17 +108,9 @@ interface SessionCursorAgentCreateParams {
112
108
  createAgent?: CursorSdkModule["Agent"]["create"];
113
109
  }
114
110
 
115
- interface CursorSessionAgentExtensionApi {
116
- on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
117
- on(event: "session_compact", handler: ExtensionHandler<SessionCompactEvent>): void;
118
- on(event: "session_before_tree", handler: ExtensionHandler<SessionBeforeTreeEvent>): void;
119
- on(event: "session_tree", handler: ExtensionHandler<SessionTreeEvent>): void;
120
- on(event: "model_select", handler: ExtensionHandler<{ model: unknown }>): void;
121
- }
122
-
123
111
  const sessionAgentsByScope = new Map<string, SessionCursorAgentPoolEntry>();
124
112
  const invalidatedScopeKeys = new Set<string>();
125
- const terminalDisposedScopeKeys = new Set<string>();
113
+ const terminalDisposedScopeGenerations = new Map<string, number>();
126
114
  const scopeCreationGenerations = new Map<string, number>();
127
115
  const EMPTY_POOL_STATE: SessionCursorAgentPoolState = { status: "empty" };
128
116
  let nextSessionAgentInstanceId = 1;
@@ -194,7 +182,7 @@ async function disposePoolEntry(entry: SessionCursorAgentPoolEntry): Promise<voi
194
182
  async function disposePoolEntryForScope(scopeKey: string, options?: { terminal?: boolean }): Promise<void> {
195
183
  invalidateScopeCreations(scopeKey);
196
184
  if (options?.terminal) {
197
- terminalDisposedScopeKeys.add(scopeKey);
185
+ terminalDisposedScopeGenerations.set(scopeKey, getCursorSessionScopeGeneration(scopeKey));
198
186
  }
199
187
  const entry = sessionAgentsByScope.get(scopeKey);
200
188
  invalidatedScopeKeys.delete(scopeKey);
@@ -416,7 +404,6 @@ export {
416
404
  planCursorSessionSend,
417
405
  type CursorSessionSendPlan,
418
406
  } from "./cursor-session-send-policy.js";
419
- export { shouldBootstrapCursorContext, shouldBootstrapCursorSend } from "./context.js";
420
407
 
421
408
  export function invalidateSessionAgent(scopeKey: string = getCursorSessionScopeKey()): void {
422
409
  invalidatedScopeKeys.add(scopeKey);
@@ -526,35 +513,10 @@ export async function disposeSessionCursorAgent(scopeKey: string = getCursorSess
526
513
  }
527
514
 
528
515
  export async function disposeAllSessionCursorAgents(): Promise<void> {
529
- const scopeKeys = [...new Set([...sessionAgentsByScope.keys(), ...terminalDisposedScopeKeys])];
516
+ const scopeKeys = [...new Set([...sessionAgentsByScope.keys(), ...terminalDisposedScopeGenerations.keys()])];
530
517
  await Promise.all(scopeKeys.map((scopeKey) => disposePoolEntryForScope(scopeKey, { terminal: true })));
531
518
  invalidatedScopeKeys.clear();
532
- terminalDisposedScopeKeys.clear();
533
- }
534
-
535
- export function registerCursorSessionAgent(_pi: CursorSessionAgentExtensionApi): void {
536
- onCursorSessionScopeKeyChange((previousScopeKey) => {
537
- void disposePoolEntryForScope(previousScopeKey, { terminal: true });
538
- });
539
- _pi.on("session_shutdown", async (event) => {
540
- if (event.reason === "reload") {
541
- await resetSessionCursorAgent();
542
- return;
543
- }
544
- await disposeSessionCursorAgent();
545
- });
546
- _pi.on("session_compact", () => {
547
- invalidateSessionAgent();
548
- });
549
- _pi.on("session_before_tree", () => {
550
- invalidateSessionAgent();
551
- });
552
- _pi.on("session_tree", async () => {
553
- await resetSessionCursorAgent();
554
- });
555
- _pi.on("model_select", () => {
556
- invalidateSessionAgent();
557
- });
519
+ terminalDisposedScopeGenerations.clear();
558
520
  }
559
521
 
560
522
  export const __testUtils = {
@@ -7,14 +7,17 @@ interface CursorSessionScopeExtensionApi {
7
7
  const ANONYMOUS_SESSION_SCOPE_KEY = "__anonymous__";
8
8
  const EPHEMERAL_SESSION_SCOPE_PREFIX = "__ephemeral__:";
9
9
 
10
- type CursorSessionScopeChangeHandler = (previousScopeKey: string) => void;
10
+ type CursorSessionScopeChangeHandler = (previousScopeKey: string) => Promise<void> | void;
11
11
 
12
12
  const state = {
13
13
  sessionCwd: process.cwd(),
14
14
  sessionFile: undefined as string | undefined,
15
15
  sessionId: undefined as string | undefined,
16
+ sessionGeneration: 0,
16
17
  };
17
18
 
19
+ const scopeGenerations = new Map<string, number>([[ANONYMOUS_SESSION_SCOPE_KEY, state.sessionGeneration]]);
20
+ let nextSessionGeneration = 1;
18
21
  let scopeChangeHandler: CursorSessionScopeChangeHandler | undefined;
19
22
 
20
23
  /**
@@ -34,7 +37,16 @@ export function getCursorSessionScopeKey(): string {
34
37
  return ANONYMOUS_SESSION_SCOPE_KEY;
35
38
  }
36
39
 
37
- export function getCursorSessionCwdFromScope(): string {
40
+ export function getCursorSessionScopeGeneration(scopeKey: string = getCursorSessionScopeKey()): number {
41
+ return scopeGenerations.get(scopeKey) ?? 0;
42
+ }
43
+
44
+ /**
45
+ * Pi session cwd when known; falls back to process.cwd() before session_start.
46
+ * Updated on session_start only until pi threads cwd into streamSimple—mid-session cwd
47
+ * changes without a new session_start event are not reflected here.
48
+ */
49
+ export function getCursorSessionCwd(): string {
38
50
  return state.sessionCwd;
39
51
  }
40
52
 
@@ -42,12 +54,19 @@ function setCursorSessionScope(cwd: string, sessionFile: string | undefined, ses
42
54
  state.sessionCwd = cwd;
43
55
  state.sessionFile = sessionFile;
44
56
  state.sessionId = sessionId;
57
+ state.sessionGeneration = nextSessionGeneration;
58
+ nextSessionGeneration += 1;
59
+ scopeGenerations.set(getCursorSessionScopeKey(), state.sessionGeneration);
45
60
  }
46
61
 
47
62
  function resetCursorSessionScope(): void {
48
63
  state.sessionCwd = process.cwd();
49
64
  state.sessionFile = undefined;
50
65
  state.sessionId = undefined;
66
+ state.sessionGeneration = 0;
67
+ nextSessionGeneration = 1;
68
+ scopeGenerations.clear();
69
+ scopeGenerations.set(ANONYMOUS_SESSION_SCOPE_KEY, state.sessionGeneration);
51
70
  }
52
71
 
53
72
  export function onCursorSessionScopeKeyChange(handler: CursorSessionScopeChangeHandler): void {
@@ -55,7 +74,7 @@ export function onCursorSessionScopeKeyChange(handler: CursorSessionScopeChangeH
55
74
  }
56
75
 
57
76
  export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi): void {
58
- pi.on("session_start", (_event, ctx) => {
77
+ pi.on("session_start", async (_event, ctx) => {
59
78
  const previousScopeKey = getCursorSessionScopeKey();
60
79
  setCursorSessionScope(
61
80
  ctx.cwd,
@@ -63,7 +82,7 @@ export function registerCursorSessionScope(pi: CursorSessionScopeExtensionApi):
63
82
  ctx.sessionManager?.getSessionId?.() ?? undefined,
64
83
  );
65
84
  if (previousScopeKey !== getCursorSessionScopeKey()) {
66
- scopeChangeHandler?.(previousScopeKey);
85
+ await scopeChangeHandler?.(previousScopeKey);
67
86
  }
68
87
  });
69
88
  }
@@ -11,16 +11,16 @@ export function resolveCursorSettingSources(raw?: string): SettingSource[] | und
11
11
  return resolveCursorSettingSourcesJs(raw) as SettingSource[] | undefined;
12
12
  }
13
13
 
14
- export function getEffectiveCursorSettingSources(raw: string | undefined = process.env[CURSOR_SETTING_SOURCES_ENV]): SettingSource[] | undefined {
14
+ export function getEffectiveCursorSettingSources(
15
+ raw: string | undefined = process.env[CURSOR_SETTING_SOURCES_ENV],
16
+ ): SettingSource[] | undefined {
15
17
  return resolveCursorSettingSources(raw);
16
18
  }
17
19
 
18
- export function cursorSettingSourcesLoadUserAgentsRules(settingSources: SettingSource[] | undefined): boolean {
20
+ export function cursorSettingSourcesIncludes(
21
+ settingSources: SettingSource[] | undefined,
22
+ source: Extract<SettingSource, "user" | "project">,
23
+ ): boolean {
19
24
  if (!settingSources?.length) return false;
20
- return settingSources.includes("all") || settingSources.includes("user");
21
- }
22
-
23
- export function cursorSettingSourcesLoadProjectAgentsRules(settingSources: SettingSource[] | undefined): boolean {
24
- if (!settingSources?.length) return false;
25
- return settingSources.includes("all") || settingSources.includes("project");
25
+ return settingSources.includes("all") || settingSources.includes(source);
26
26
  }