pi-cursor-sdk 0.1.20 → 0.1.22

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 (89) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +49 -9
  3. package/docs/cursor-dogfood-checklist.md +57 -0
  4. package/docs/cursor-live-smoke-checklist.md +115 -9
  5. package/docs/cursor-model-ux-spec.md +58 -18
  6. package/docs/cursor-native-tool-replay.md +15 -7
  7. package/docs/cursor-native-tool-visual-audit.md +104 -59
  8. package/docs/cursor-testing-lessons.md +8 -3
  9. package/docs/cursor-tool-surfaces.md +69 -0
  10. package/package.json +34 -10
  11. package/scripts/debug-provider-events.d.mts +59 -0
  12. package/scripts/debug-provider-events.mjs +70 -175
  13. package/scripts/debug-sdk-events.d.mts +90 -0
  14. package/scripts/debug-sdk-events.mjs +36 -98
  15. package/scripts/fixtures/plan-strip-shim/index.ts +12 -0
  16. package/scripts/isolated-cursor-smoke.sh +264 -102
  17. package/scripts/lib/cursor-child-process.d.mts +10 -0
  18. package/scripts/lib/cursor-child-process.mjs +50 -0
  19. package/scripts/lib/cursor-cli-args.d.mts +63 -0
  20. package/scripts/lib/cursor-cli-args.mjs +129 -0
  21. package/scripts/lib/cursor-script-fail.d.mts +1 -0
  22. package/scripts/lib/cursor-script-fail.mjs +13 -0
  23. package/scripts/lib/cursor-sdk-output-filter.d.mts +5 -0
  24. package/scripts/lib/cursor-smoke-env.d.mts +38 -0
  25. package/scripts/lib/cursor-smoke-env.mjs +81 -0
  26. package/scripts/lib/cursor-smoke-shell.sh +174 -0
  27. package/scripts/lib/cursor-visual-render.d.mts +15 -0
  28. package/scripts/lib/cursor-visual-render.mjs +131 -0
  29. package/scripts/probe-mcp-coldstart.mjs +20 -38
  30. package/scripts/refresh-cursor-model-snapshots.mjs +29 -65
  31. package/scripts/steering-rpc-smoke.mjs +170 -65
  32. package/scripts/tmux-live-smoke.sh +152 -98
  33. package/scripts/visual-tui-smoke.mjs +659 -0
  34. package/shared/cursor-sdk-event-debug-env.d.mts +12 -0
  35. package/shared/cursor-sdk-event-debug-env.mjs +13 -0
  36. package/shared/cursor-sensitive-text.d.mts +1 -0
  37. package/{scripts/lib/cursor-probe-utils.mjs → shared/cursor-sensitive-text.mjs} +1 -13
  38. package/shared/cursor-setting-sources.d.mts +5 -0
  39. package/shared/cursor-setting-sources.mjs +22 -0
  40. package/src/context.ts +21 -12
  41. package/src/cursor-bridge-contract.ts +1 -3
  42. package/src/cursor-incomplete-tool-visibility.ts +22 -5
  43. package/src/cursor-native-tool-display-registration.ts +63 -27
  44. package/src/cursor-native-tool-display-replay.ts +246 -144
  45. package/src/cursor-native-tool-display-state.ts +2 -0
  46. package/src/cursor-native-tool-display-tools.ts +149 -41
  47. package/src/cursor-provider-live-run-drain.ts +1 -52
  48. package/src/cursor-provider-run-finalizer.ts +237 -0
  49. package/src/cursor-provider-run-outcome.ts +149 -0
  50. package/src/cursor-provider-turn-api-key.ts +8 -0
  51. package/src/cursor-provider-turn-coordinator.ts +98 -446
  52. package/src/cursor-provider-turn-display-router.ts +216 -0
  53. package/src/cursor-provider-turn-emit.ts +59 -0
  54. package/src/cursor-provider-turn-finalize.ts +119 -0
  55. package/src/cursor-provider-turn-lifecycle-emitter.ts +97 -0
  56. package/src/cursor-provider-turn-message-offset.ts +15 -0
  57. package/src/cursor-provider-turn-prepare.ts +216 -0
  58. package/src/cursor-provider-turn-runner.ts +140 -0
  59. package/src/cursor-provider-turn-sdk-normalizer.ts +88 -0
  60. package/src/cursor-provider-turn-send.ts +103 -0
  61. package/src/cursor-provider-turn-shell-output.ts +107 -0
  62. package/src/cursor-provider-turn-tool-ledger.ts +126 -0
  63. package/src/cursor-provider-turn-types.ts +87 -0
  64. package/src/cursor-provider.ts +16 -504
  65. package/src/cursor-replay-activity-builders.ts +276 -0
  66. package/src/cursor-replay-source-names.ts +33 -0
  67. package/src/cursor-replay-summary-args.ts +191 -0
  68. package/src/cursor-replay-tool-details.ts +464 -0
  69. package/src/cursor-run-final-text.ts +56 -0
  70. package/src/cursor-sdk-abort-error-guard.ts +4 -0
  71. package/src/cursor-sdk-event-debug-constants.ts +14 -5
  72. package/src/cursor-sdk-event-debug.ts +2 -1
  73. package/src/cursor-sensitive-text.ts +3 -36
  74. package/src/cursor-session-agent.ts +3 -1
  75. package/src/cursor-session-compaction-prep.ts +19 -0
  76. package/src/cursor-setting-sources.ts +7 -10
  77. package/src/cursor-state.ts +232 -28
  78. package/src/cursor-tool-lifecycle.ts +9 -8
  79. package/src/cursor-tool-manifest.ts +41 -0
  80. package/src/cursor-tool-names.ts +18 -106
  81. package/src/cursor-tool-presentation-registry.ts +556 -0
  82. package/src/cursor-tool-transcript.ts +1 -1
  83. package/src/cursor-tool-visibility.ts +3 -27
  84. package/src/cursor-transcript-tool-formatters.ts +0 -59
  85. package/src/cursor-transcript-tool-specs.ts +158 -233
  86. package/src/cursor-transcript-utils.ts +0 -44
  87. package/src/cursor-web-tool-activity.ts +10 -60
  88. package/src/cursor-web-tool-args.ts +39 -0
  89. package/src/index.ts +8 -10
@@ -0,0 +1,464 @@
1
+ import { isCursorReplayActivitySourceName, type CursorReplayActivitySourceName } from "./cursor-replay-source-names.js";
2
+
3
+ /** Replay detail variants keyed by replay card disposition, not SDK source tool alone. */
4
+ export type CursorReplayToolDetailsVariant =
5
+ | "nativeEdit"
6
+ | "nativeWrite"
7
+ | "activity"
8
+ | "generateImage"
9
+ | "genericFallback";
10
+
11
+ /**
12
+ * Sentinel source tool name for activity cards whose SDK name is not a known registry entry.
13
+ * Display identity lives in `title` and replay args.
14
+ */
15
+ export const CURSOR_REPLAY_UNREGISTERED_ACTIVITY_TOOL_NAME = "unregisteredActivity" as const;
16
+
17
+ /** SDK source tool names carried on neutral activity replay cards. */
18
+ export type CursorReplayActivitySourceToolName =
19
+ | CursorReplayActivitySourceName
20
+ | typeof CURSOR_REPLAY_UNREGISTERED_ACTIVITY_TOOL_NAME;
21
+
22
+ declare const cursorReplayUnknownSourceToolNameBrand: unique symbol;
23
+
24
+ /** Opaque unknown/future Cursor tool names on generic-fallback replay cards. */
25
+ export type CursorReplayUnknownSourceToolName = string & {
26
+ readonly [cursorReplayUnknownSourceToolNameBrand]: unique symbol;
27
+ };
28
+
29
+ export interface CursorReplayNativeEditDetails {
30
+ variant: "nativeEdit";
31
+ path?: string;
32
+ linesAdded?: number;
33
+ linesRemoved?: number;
34
+ diffString?: string;
35
+ diff?: string;
36
+ firstChangedLine?: number;
37
+ summary?: string;
38
+ expandedText?: string;
39
+ }
40
+
41
+ export interface CursorReplayNativeWriteDetails {
42
+ variant: "nativeWrite";
43
+ path?: string;
44
+ linesCreated?: number;
45
+ fileSize?: number;
46
+ fileContentAfterWrite?: string;
47
+ expandedText?: string;
48
+ summary?: string;
49
+ }
50
+
51
+ export interface CursorReplayGenerateImageDetails {
52
+ variant: "generateImage";
53
+ imagePath?: string;
54
+ imageDisplayPath?: string;
55
+ imageMimeType?: string;
56
+ summary?: string;
57
+ expandedText?: string;
58
+ /** Legacy parsed title retained on older payloads; display always uses `Cursor generateImage`. */
59
+ title?: string;
60
+ collapseDetailsByDefault?: boolean;
61
+ }
62
+
63
+ /** Neutral Cursor activity cards and unknown-tool fallbacks with a display title. */
64
+ export interface CursorReplayActivityDetails {
65
+ variant: "activity";
66
+ sourceToolName: CursorReplayActivitySourceToolName;
67
+ title: string;
68
+ summary?: string;
69
+ expandedText?: string;
70
+ collapseDetailsByDefault?: boolean;
71
+ path?: string;
72
+ fileSize?: number;
73
+ /** Structured unified diff for edit (and similar) activity cards; drives canonical colored diff rendering. */
74
+ diffString?: string;
75
+ diff?: string;
76
+ linesAdded?: number;
77
+ linesRemoved?: number;
78
+ /** Optional post-write content for write activity fallbacks (mirrors nativeWrite). */
79
+ fileContentAfterWrite?: string;
80
+ }
81
+
82
+ /** Parsed replay details without a display title (legacy or malformed payloads). */
83
+ export interface CursorReplayGenericFallbackDetails {
84
+ variant: "genericFallback";
85
+ sourceToolName: CursorReplayUnknownSourceToolName;
86
+ summary?: string;
87
+ expandedText?: string;
88
+ }
89
+
90
+ export type CursorReplayToolDetails =
91
+ | CursorReplayNativeEditDetails
92
+ | CursorReplayNativeWriteDetails
93
+ | CursorReplayGenerateImageDetails
94
+ | CursorReplayActivityDetails
95
+ | CursorReplayGenericFallbackDetails;
96
+
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
+ export type CursorReplayActivityDetailFields = Pick<
113
+ CursorReplayActivityDetails,
114
+ | "summary"
115
+ | "expandedText"
116
+ | "collapseDetailsByDefault"
117
+ | "path"
118
+ | "fileSize"
119
+ | "diffString"
120
+ | "diff"
121
+ | "linesAdded"
122
+ | "linesRemoved"
123
+ | "fileContentAfterWrite"
124
+ >;
125
+
126
+ export type CursorReplayGenerateImageDetailFields = Pick<
127
+ CursorReplayGenerateImageDetails,
128
+ "summary" | "expandedText" | "imagePath" | "imageDisplayPath" | "imageMimeType"
129
+ >;
130
+
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");
152
+ return sourceToolName?.trim() ? sourceToolName.trim() : undefined;
153
+ }
154
+
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");
164
+ return variant?.trim() ? variant.trim() : undefined;
165
+ }
166
+
167
+ function parseCursorReplayNativeEditDetails(record: Record<string, unknown>): CursorReplayNativeEditDetails {
168
+ return {
169
+ 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"),
178
+ };
179
+ }
180
+
181
+ function parseCursorReplayNativeWriteDetails(record: Record<string, unknown>): CursorReplayNativeWriteDetails {
182
+ return {
183
+ 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"),
190
+ };
191
+ }
192
+
193
+ function parseCursorReplayGenerateImageDetails(record: Record<string, unknown>): CursorReplayGenerateImageDetails {
194
+ const title = readOptionalString(record, "title");
195
+ const collapseDetailsByDefault = readOptionalBoolean(record, "collapseDetailsByDefault");
196
+ return {
197
+ 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 } : {}),
204
+ ...(collapseDetailsByDefault !== undefined ? { collapseDetailsByDefault } : {}),
205
+ };
206
+ }
207
+
208
+ function parseCursorReplayActivityDetails(
209
+ record: Record<string, unknown>,
210
+ sourceToolName: CursorReplayActivitySourceToolName,
211
+ title: string,
212
+ ): CursorReplayActivityDetails {
213
+ return {
214
+ variant: "activity",
215
+ sourceToolName,
216
+ 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"),
227
+ };
228
+ }
229
+
230
+ function brandCursorReplayUnknownSourceToolName(sourceToolName: string): CursorReplayUnknownSourceToolName {
231
+ return sourceToolName as CursorReplayUnknownSourceToolName;
232
+ }
233
+
234
+ function parseCursorReplayGenericFallbackDetails(
235
+ record: Record<string, unknown>,
236
+ sourceToolName: string,
237
+ ): CursorReplayGenericFallbackDetails {
238
+ return {
239
+ variant: "genericFallback",
240
+ sourceToolName: brandCursorReplayUnknownSourceToolName(sourceToolName),
241
+ summary: readOptionalString(record, "summary"),
242
+ expandedText: readOptionalString(record, "expandedText"),
243
+ };
244
+ }
245
+
246
+ function isCursorReplayActivitySourceToolName(name: string): name is CursorReplayActivitySourceToolName {
247
+ if (name === CURSOR_REPLAY_UNREGISTERED_ACTIVITY_TOOL_NAME) return true;
248
+ if (name === "generateImage") return false;
249
+ return isCursorReplayActivitySourceName(name);
250
+ }
251
+
252
+ function resolveParseActivitySourceToolName(sourceToolName: string): CursorReplayActivitySourceToolName {
253
+ return isCursorReplayActivitySourceToolName(sourceToolName)
254
+ ? sourceToolName
255
+ : CURSOR_REPLAY_UNREGISTERED_ACTIVITY_TOOL_NAME;
256
+ }
257
+
258
+ /** Maps incomplete or non-activity replay source names onto activity-card source tool names. */
259
+ export function resolveIncompleteReplayActivitySourceToolName(
260
+ sourceToolName: string,
261
+ ): CursorReplayActivitySourceToolName {
262
+ if (sourceToolName === "generateImage") return CURSOR_REPLAY_UNREGISTERED_ACTIVITY_TOOL_NAME;
263
+ return resolveParseActivitySourceToolName(sourceToolName);
264
+ }
265
+
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();
298
+ if (!title) return undefined;
299
+ return parseCursorReplayActivityDetails(
300
+ record,
301
+ resolveParseActivitySourceToolName(readSourceToolName(record) ?? CURSOR_REPLAY_UNREGISTERED_ACTIVITY_TOOL_NAME),
302
+ title,
303
+ );
304
+ }
305
+
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
+ 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 };
374
+ }
375
+
376
+ /** @deprecated Prefer {@link buildCursorReplayNativeWriteDetails}. */
377
+ export const buildCursorReplayWriteDetails = buildCursorReplayNativeWriteDetails;
378
+
379
+ export function assembleCursorReplayActivityDetails(
380
+ sourceToolName: CursorReplayActivitySourceToolName,
381
+ title: string,
382
+ fields: CursorReplayActivityDetailFields,
383
+ contentText: string,
384
+ isError: boolean,
385
+ activitySummary: string | undefined,
386
+ ): CursorReplayActivityDetails {
387
+ const summary = isError ? fields.summary : (fields.summary ?? activitySummary);
388
+ return {
389
+ variant: "activity",
390
+ sourceToolName,
391
+ title,
392
+ summary,
393
+ expandedText: fields.expandedText ?? contentText,
394
+ ...(fields.collapseDetailsByDefault !== undefined ? { collapseDetailsByDefault: fields.collapseDetailsByDefault } : {}),
395
+ ...(fields.path !== undefined ? { path: fields.path } : {}),
396
+ ...(fields.fileSize !== undefined ? { fileSize: fields.fileSize } : {}),
397
+ ...(fields.diffString !== undefined ? { diffString: fields.diffString } : {}),
398
+ ...(fields.diff !== undefined ? { diff: fields.diff } : {}),
399
+ ...(fields.linesAdded !== undefined ? { linesAdded: fields.linesAdded } : {}),
400
+ ...(fields.linesRemoved !== undefined ? { linesRemoved: fields.linesRemoved } : {}),
401
+ ...(fields.fileContentAfterWrite !== undefined ? { fileContentAfterWrite: fields.fileContentAfterWrite } : {}),
402
+ };
403
+ }
404
+
405
+ /** @deprecated Prefer {@link assembleCursorReplayActivityDetails}. */
406
+ export const assembleCursorReplayTitledActivityDetails = assembleCursorReplayActivityDetails;
407
+
408
+ export const CURSOR_REPLAY_GENERATE_IMAGE_RESULT_TITLE = "Cursor generateImage" as const;
409
+
410
+ export function assembleCursorReplayGenerateImageDetails(
411
+ fields: CursorReplayGenerateImageDetailFields,
412
+ contentText: string,
413
+ isError: boolean,
414
+ activitySummary: string | undefined,
415
+ ): CursorReplayGenerateImageDetails {
416
+ const summary = isError ? fields.summary : (fields.summary ?? activitySummary);
417
+ return {
418
+ variant: "generateImage",
419
+ imagePath: fields.imagePath,
420
+ imageDisplayPath: fields.imageDisplayPath,
421
+ imageMimeType: fields.imageMimeType,
422
+ summary,
423
+ expandedText: fields.expandedText ?? contentText,
424
+ };
425
+ }
426
+
427
+ export function isCursorReplayNativeEditDetails(
428
+ details: CursorReplayToolDetails,
429
+ ): details is CursorReplayNativeEditDetails {
430
+ return details.variant === "nativeEdit";
431
+ }
432
+
433
+ /** @deprecated Prefer {@link isCursorReplayNativeEditDetails}. */
434
+ export const isCursorReplayEditDetails = isCursorReplayNativeEditDetails;
435
+
436
+ export function isCursorReplayNativeWriteDetails(
437
+ details: CursorReplayToolDetails,
438
+ ): details is CursorReplayNativeWriteDetails {
439
+ return details.variant === "nativeWrite";
440
+ }
441
+
442
+ /** @deprecated Prefer {@link isCursorReplayNativeWriteDetails}. */
443
+ export const isCursorReplayWriteDetails = isCursorReplayNativeWriteDetails;
444
+
445
+ export function isCursorReplayGenerateImageDetails(
446
+ details: CursorReplayToolDetails,
447
+ ): details is CursorReplayGenerateImageDetails {
448
+ return details.variant === "generateImage";
449
+ }
450
+
451
+ export function isCursorReplayActivityDetails(
452
+ details: CursorReplayToolDetails,
453
+ ): details is CursorReplayActivityDetails {
454
+ return details.variant === "activity";
455
+ }
456
+
457
+ /** @deprecated Prefer {@link isCursorReplayActivityDetails}. */
458
+ export const isCursorReplayTitledActivityDetails = isCursorReplayActivityDetails;
459
+
460
+ export function isCursorReplayGenericFallbackDetails(
461
+ details: CursorReplayToolDetails,
462
+ ): details is CursorReplayGenericFallbackDetails {
463
+ return details.variant === "genericFallback";
464
+ }
@@ -0,0 +1,56 @@
1
+ import { hasUsableText } from "./cursor-record-utils.js";
2
+
3
+ function isCursorTextBoundary(text: string, index: number): boolean {
4
+ if (index <= 0 || index >= text.length) return true;
5
+ const before = text[index - 1];
6
+ const after = text[index];
7
+ return !/[\p{L}\p{N}_]/u.test(before) || !/[\p{L}\p{N}_]/u.test(after);
8
+ }
9
+
10
+ function trimAlreadyEmittedCursorText(text: string, emittedText: string, options?: { allowPartialPrefix?: boolean }): string {
11
+ if (!text || !emittedText) return text;
12
+ if (text === emittedText) return "";
13
+ if (text.startsWith(emittedText) && (options?.allowPartialPrefix || isCursorTextBoundary(text, emittedText.length))) {
14
+ return text.slice(emittedText.length);
15
+ }
16
+ if (emittedText.endsWith(text) && isCursorTextBoundary(emittedText, emittedText.length - text.length)) return "";
17
+ const trimmedText = text.trim();
18
+ const trimmedEmittedText = emittedText.trim();
19
+ if (trimmedText === trimmedEmittedText) return "";
20
+ if (trimmedText && trimmedEmittedText.endsWith(trimmedText)) {
21
+ const suffixStart = trimmedEmittedText.length - trimmedText.length;
22
+ if (isCursorTextBoundary(trimmedEmittedText, suffixStart)) return "";
23
+ }
24
+ return text;
25
+ }
26
+
27
+ export function trimCurrentTurnAlreadyEmittedCursorText(
28
+ text: string,
29
+ currentTurnEmittedText: string,
30
+ emittedText = currentTurnEmittedText,
31
+ ): string {
32
+ if (!currentTurnEmittedText) return trimAlreadyEmittedCursorText(text, emittedText);
33
+ const currentTurnTrimmedText = trimAlreadyEmittedCursorText(text, currentTurnEmittedText, { allowPartialPrefix: true });
34
+ if (currentTurnTrimmedText !== text) return currentTurnTrimmedText;
35
+ if (emittedText.endsWith(currentTurnEmittedText)) {
36
+ const emittedTextTrimmedText = trimAlreadyEmittedCursorText(text, emittedText, { allowPartialPrefix: true });
37
+ if (emittedTextTrimmedText !== text) return emittedTextTrimmedText;
38
+ }
39
+ return trimAlreadyEmittedCursorText(text, emittedText);
40
+ }
41
+
42
+ export function selectCursorFinalText(
43
+ resultText: unknown,
44
+ textDeltas: readonly string[],
45
+ emittedText: string,
46
+ fallbackText?: string,
47
+ options?: { allowPartialPrefix?: boolean },
48
+ ): string {
49
+ const candidates = [typeof resultText === "string" ? resultText : undefined, fallbackText, textDeltas.join("")];
50
+ for (const candidate of candidates) {
51
+ if (!hasUsableText(candidate)) continue;
52
+ const trimmedCandidate = trimAlreadyEmittedCursorText(candidate, emittedText, options);
53
+ if (hasUsableText(trimmedCandidate)) return trimmedCandidate;
54
+ }
55
+ return "";
56
+ }
@@ -88,6 +88,10 @@ function uninstallProcessEmitPatchIfIdle(): void {
88
88
  originalProcessEmit = undefined;
89
89
  }
90
90
 
91
+ export const __testUtils = {
92
+ activeSuppressionCount: (): number => activeSuppressions.size,
93
+ };
94
+
91
95
  export function installCursorSdkAbortErrorSuppression(): CursorSdkAbortErrorSuppression {
92
96
  installProcessEmitPatch();
93
97
  const token: CursorSdkAbortErrorSuppressionToken = { suppress: false };
@@ -1,10 +1,19 @@
1
1
  import { resolve } from "node:path";
2
+ import {
3
+ CURSOR_SDK_EVENT_DEBUG_DIR_ENV,
4
+ CURSOR_SDK_EVENT_DEBUG_ENV,
5
+ CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV,
6
+ CURSOR_SDK_EVENT_DEBUG_SESSION_DIR_ENV,
7
+ CURSOR_SDK_EVENT_DEBUG_STDERR_ENV,
8
+ } from "../shared/cursor-sdk-event-debug-env.mjs";
2
9
 
3
- export const CURSOR_SDK_EVENT_DEBUG_ENV = "PI_CURSOR_SDK_EVENT_DEBUG";
4
- export const CURSOR_SDK_EVENT_DEBUG_DIR_ENV = "PI_CURSOR_SDK_EVENT_DEBUG_DIR";
5
- export const CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV = "PI_CURSOR_SDK_EVENT_DEBUG_RUN_DIR";
6
- export const CURSOR_SDK_EVENT_DEBUG_SESSION_DIR_ENV = "PI_CURSOR_SDK_EVENT_DEBUG_SESSION_DIR";
7
- export const CURSOR_SDK_EVENT_DEBUG_STDERR_ENV = "PI_CURSOR_SDK_EVENT_DEBUG_STDERR";
10
+ export {
11
+ CURSOR_SDK_EVENT_DEBUG_DIR_ENV,
12
+ CURSOR_SDK_EVENT_DEBUG_ENV,
13
+ CURSOR_SDK_EVENT_DEBUG_RUN_DIR_ENV,
14
+ CURSOR_SDK_EVENT_DEBUG_SESSION_DIR_ENV,
15
+ CURSOR_SDK_EVENT_DEBUG_STDERR_ENV,
16
+ };
8
17
  export const CURSOR_SDK_EVENT_DEBUG_LOG_PREFIX = "[pi-cursor-sdk:sdk-events]";
9
18
 
10
19
  export const SESSION_MANIFEST = "session.json";
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { copyFileSync, existsSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import type { AssistantMessageEventStream } from "@earendil-works/pi-ai";
5
- import type { InteractionUpdate } from "@cursor/sdk";
5
+ import type { AgentModeOption, InteractionUpdate } from "@cursor/sdk";
6
6
  import type { CursorPiToolBridgeDiagnosticEvent } from "./cursor-pi-tool-bridge-diagnostics.js";
7
7
  import { serializeCursorPiToolBridgeDiagnostic } from "./cursor-pi-tool-bridge-diagnostics.js";
8
8
  import type { CursorPiBridgeToolRequest } from "./cursor-pi-tool-bridge-types.js";
@@ -72,6 +72,7 @@ export interface CursorSdkEventDebugSendMeta {
72
72
  bridgeEnabled: boolean;
73
73
  nativeReplayId: string;
74
74
  promptInputTokens: number;
75
+ agentMode: AgentModeOption;
75
76
  }
76
77
 
77
78
  export interface CursorSdkEventDebugRunMeta {
@@ -1,42 +1,9 @@
1
1
  import type { CursorPiToolDisplay } from "./cursor-transcript-utils.js";
2
-
3
- function escapeRegExp(value: string): string {
4
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
- }
6
-
7
- const BRIDGE_ENDPOINT_ROOT = "/cursor-pi-tool-bridge";
8
- const BRIDGE_ENDPOINT_TOKEN_PATTERN = "[^/\\s\"'<>]+";
9
- const BRIDGE_LOOPBACK_HOST_PATTERN = "127\\.0\\.0\\.1(?::\\d+)?";
10
- const BRIDGE_ENDPOINT_PATH_PATTERN = `${escapeRegExp(BRIDGE_ENDPOINT_ROOT)}/${BRIDGE_ENDPOINT_TOKEN_PATTERN}/mcp`;
11
-
12
- function scrubBridgeEndpointMaterial(text: string): string {
13
- return text
14
- .replace(
15
- new RegExp(`https?://${BRIDGE_LOOPBACK_HOST_PATTERN}${BRIDGE_ENDPOINT_PATH_PATTERN}`, "gi"),
16
- "[redacted-bridge-endpoint]",
17
- )
18
- .replace(
19
- new RegExp(`${BRIDGE_LOOPBACK_HOST_PATTERN}${BRIDGE_ENDPOINT_PATH_PATTERN}`, "gi"),
20
- "[redacted-bridge-endpoint]",
21
- )
22
- .replace(new RegExp(BRIDGE_ENDPOINT_PATH_PATTERN, "gi"), "[redacted-bridge-endpoint]");
23
- }
2
+ /** Provider-facing wrapper; canonical scrubbing lives in shared/cursor-sensitive-text.mjs. */
3
+ import { scrubSensitiveText as scrubSensitiveTextJs } from "../shared/cursor-sensitive-text.mjs";
24
4
 
25
5
  export function scrubSensitiveText(text: string, apiKey?: string): string {
26
- let scrubbed = text;
27
- const trimmedKey = apiKey?.trim();
28
- if (trimmedKey) {
29
- scrubbed = scrubbed.replace(new RegExp(escapeRegExp(trimmedKey), "g"), "[redacted]");
30
- }
31
- return scrubBridgeEndpointMaterial(
32
- scrubbed
33
- .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]")
34
- .replace(/((?:^|[\s,{])cookie["']?\s*[:=]\s*["']?)[^\n]+/gi, "$1[redacted]")
35
- .replace(
36
- /((?:authorization|api[_-]?key|apiKey|token|session(?:[_-]?id)?)["']?\s*[:=]\s*["']?)[^"'\s,;}]+/gi,
37
- "$1[redacted]",
38
- ),
39
- );
6
+ return scrubSensitiveTextJs(text, apiKey);
40
7
  }
41
8
 
42
9
  function scrubDisplayValue(value: unknown, apiKey?: string): unknown {
@@ -7,7 +7,7 @@ import type {
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import { createHash } from "node:crypto";
9
9
  import { Agent } from "@cursor/sdk";
10
- import type { ModelSelection, SDKAgent, SettingSource } from "@cursor/sdk";
10
+ import type { AgentModeOption, ModelSelection, SDKAgent, SettingSource } from "@cursor/sdk";
11
11
  import type { Context } from "@earendil-works/pi-ai";
12
12
  import {
13
13
  getRegisteredCursorPiToolBridge,
@@ -103,6 +103,7 @@ function rethrowSupersededWhenReplacedByDifferentPoolKey(scopeKey: string, poolK
103
103
 
104
104
  interface SessionCursorAgentCreateParams {
105
105
  apiKey: string;
106
+ agentMode: AgentModeOption;
106
107
  cwd: string;
107
108
  modelSelection: ModelSelection;
108
109
  settingSources?: SettingSource[];
@@ -382,6 +383,7 @@ async function createSessionAgentEntry(
382
383
  agent = await createAgent({
383
384
  apiKey: params.apiKey,
384
385
  model: params.modelSelection,
386
+ mode: params.agentMode,
385
387
  local: params.settingSources ? { cwd: params.cwd, settingSources: params.settingSources } : { cwd: params.cwd },
386
388
  ...(bridgeRun?.mcpServers ? { mcpServers: bridgeRun.mcpServers } : {}),
387
389
  });
@@ -0,0 +1,19 @@
1
+ import { cursorLiveRuns } from "./cursor-provider-live-run-drain.js";
2
+ import { resetSessionCursorAgent } from "./cursor-session-agent.js";
3
+ import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
4
+
5
+ /**
6
+ * Prepare the pooled Cursor session agent for pi compaction summarization.
7
+ * Releases any scoped live-run drain state still tied to the pooled agent, then
8
+ * disposes the pool entry so summarization acquires a clean SDK agent.
9
+ */
10
+ export async function prepareCursorSessionForCompaction(
11
+ scopeKey: string = getCursorSessionScopeKey(),
12
+ ): Promise<void> {
13
+ while (true) {
14
+ const run = cursorLiveRuns.getActiveForScope(scopeKey);
15
+ if (!run || run.disposed) break;
16
+ await cursorLiveRuns.release(run);
17
+ }
18
+ await resetSessionCursorAgent(scopeKey);
19
+ }