pi-cursor-sdk 0.1.19 → 0.1.21

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 +52 -0
  2. package/README.md +72 -11
  3. package/docs/cursor-dogfood-checklist.md +57 -0
  4. package/docs/cursor-live-smoke-checklist.md +116 -10
  5. package/docs/cursor-model-ux-spec.md +60 -19
  6. package/docs/cursor-native-tool-replay.md +21 -11
  7. package/docs/cursor-native-tool-visual-audit.md +104 -59
  8. package/docs/cursor-testing-lessons.md +10 -5
  9. package/docs/cursor-tool-surfaces.md +69 -0
  10. package/package.json +37 -11
  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 +226 -0
  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 +72 -49
  43. package/src/cursor-mcp-timeout-override.ts +66 -11
  44. package/src/cursor-native-tool-display-registration.ts +63 -27
  45. package/src/cursor-native-tool-display-replay.ts +246 -143
  46. package/src/cursor-native-tool-display-state.ts +2 -0
  47. package/src/cursor-native-tool-display-tools.ts +149 -41
  48. package/src/cursor-provider-live-run-drain.ts +1 -52
  49. package/src/cursor-provider-run-finalizer.ts +235 -0
  50. package/src/cursor-provider-run-outcome.ts +149 -0
  51. package/src/cursor-provider-turn-api-key.ts +8 -0
  52. package/src/cursor-provider-turn-coordinator.ts +113 -440
  53. package/src/cursor-provider-turn-display-router.ts +216 -0
  54. package/src/cursor-provider-turn-emit.ts +59 -0
  55. package/src/cursor-provider-turn-finalize.ts +119 -0
  56. package/src/cursor-provider-turn-lifecycle-emitter.ts +97 -0
  57. package/src/cursor-provider-turn-message-offset.ts +15 -0
  58. package/src/cursor-provider-turn-prepare.ts +216 -0
  59. package/src/cursor-provider-turn-runner.ts +138 -0
  60. package/src/cursor-provider-turn-sdk-normalizer.ts +88 -0
  61. package/src/cursor-provider-turn-send.ts +103 -0
  62. package/src/cursor-provider-turn-shell-output.ts +107 -0
  63. package/src/cursor-provider-turn-tool-ledger.ts +126 -0
  64. package/src/cursor-provider-turn-types.ts +87 -0
  65. package/src/cursor-provider.ts +16 -482
  66. package/src/cursor-replay-activity-builders.ts +276 -0
  67. package/src/cursor-replay-source-names.ts +33 -0
  68. package/src/cursor-replay-summary-args.ts +191 -0
  69. package/src/cursor-replay-tool-details.ts +464 -0
  70. package/src/cursor-run-final-text.ts +56 -0
  71. package/src/cursor-sdk-abort-error-guard.ts +4 -0
  72. package/src/cursor-sdk-event-debug-constants.ts +14 -5
  73. package/src/cursor-sdk-event-debug.ts +8 -2
  74. package/src/cursor-sensitive-text.ts +3 -36
  75. package/src/cursor-session-agent.ts +265 -88
  76. package/src/cursor-setting-sources.ts +7 -10
  77. package/src/cursor-state.ts +232 -28
  78. package/src/cursor-tool-lifecycle.ts +17 -42
  79. package/src/cursor-tool-manifest.ts +41 -0
  80. package/src/cursor-tool-names.ts +18 -79
  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 +39 -0
  84. package/src/cursor-transcript-tool-formatters.ts +0 -59
  85. package/src/cursor-transcript-tool-specs.ts +169 -232
  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 +4 -10
@@ -0,0 +1,276 @@
1
+ import { scrubSensitiveText } from "./cursor-sensitive-text.js";
2
+ import type {
3
+ CursorReplayGenerateImageSummaryArgs,
4
+ CursorReplayMcpSummaryArgs,
5
+ CursorReplayPathSummaryArgs,
6
+ CursorReplayPlanSummaryArgs,
7
+ CursorReplayReadLintsSummaryArgs,
8
+ CursorReplayRecordScreenSummaryArgs,
9
+ CursorReplaySemSearchSummaryArgs,
10
+ CursorReplayTaskSummaryArgs,
11
+ CursorReplayTodoSummaryArgs,
12
+ CursorReplayWebFetchSummaryArgs,
13
+ CursorReplayWebSearchSummaryArgs,
14
+ } from "./cursor-replay-summary-args.js";
15
+ import type { CursorReplayGenerateImageDetailFields } from "./cursor-replay-tool-details.js";
16
+ import { asRecord } from "./cursor-record-utils.js";
17
+ import {
18
+ firstNonEmptyLine,
19
+ formatDisplayPath,
20
+ getArray,
21
+ getNumber,
22
+ getRecord,
23
+ getString,
24
+ truncateArg,
25
+ } from "./cursor-transcript-utils.js";
26
+ import { extractWebFetchTarget, extractWebSearchQuery } from "./cursor-web-tool-args.js";
27
+
28
+ export interface CursorReplayActivityBuildContext {
29
+ args: Record<string, unknown>;
30
+ result: { status: string | undefined; value: unknown; error: unknown };
31
+ options: { cwd?: string };
32
+ }
33
+
34
+ export function buildDeleteReplaySummaryArgs({ args, options }: CursorReplayActivityBuildContext): CursorReplayPathSummaryArgs {
35
+ const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
36
+ return displayPath ? { path: displayPath } : {};
37
+ }
38
+
39
+ export function buildDeleteReplayDetailFields({ args, result, options }: CursorReplayActivityBuildContext): {
40
+ path?: string;
41
+ fileSize?: number;
42
+ } {
43
+ const displayPath = typeof args.path === "string" ? formatDisplayPath(args.path, options.cwd) : undefined;
44
+ const value = asRecord(result.value);
45
+ return {
46
+ path: displayPath,
47
+ fileSize: getNumber(value, "fileSize"),
48
+ };
49
+ }
50
+
51
+ export function buildEmptyReplayDetailFields(): Record<string, never> {
52
+ return {};
53
+ }
54
+
55
+ export function buildCollapsedReplayDetailFields(): { collapseDetailsByDefault: true } {
56
+ return { collapseDetailsByDefault: true };
57
+ }
58
+
59
+ export function buildReadLintsReplaySummaryArgs({
60
+ args,
61
+ result,
62
+ options,
63
+ }: CursorReplayActivityBuildContext): CursorReplayReadLintsSummaryArgs {
64
+ const paths = getReadLintPaths(args, result, options);
65
+ const diagnosticCount = getReadLintDiagnostics(result, options).length;
66
+ return {
67
+ ...(paths.length > 0 ? { paths } : {}),
68
+ ...(paths.length === 1 ? { path: paths[0] } : {}),
69
+ ...(paths.length > 0 ? { diagnosticCount } : {}),
70
+ };
71
+ }
72
+
73
+ export function buildTodoReplaySummaryArgs(
74
+ args: Record<string, unknown>,
75
+ result: CursorReplayActivityBuildContext["result"],
76
+ ): CursorReplayTodoSummaryArgs {
77
+ const todos = getTodoItems(args, result);
78
+ const totalCount = getNumber(asRecord(result.value), "totalCount") ?? getNumber(args, "totalCount") ?? todos.length;
79
+ const completedCount = todos.filter((todo) => todo.status === "completed").length;
80
+ const inProgressCount = todos.filter((todo) => todo.status === "inProgress").length;
81
+ const pendingCount = todos.filter((todo) => todo.status === "pending").length;
82
+ return todos.length > 0
83
+ ? { totalCount, completedCount, inProgressCount, pendingCount }
84
+ : { totalCount };
85
+ }
86
+
87
+ export function buildCreatePlanReplaySummaryArgs({ args, result }: CursorReplayActivityBuildContext): CursorReplayPlanSummaryArgs {
88
+ const plan = getString(args, "plan") ?? getString(asRecord(result.value) ?? {}, "plan");
89
+ const planTitle = plan ? firstNonEmptyLine(plan) : undefined;
90
+ return {
91
+ ...buildTodoReplaySummaryArgs(args, result),
92
+ ...(planTitle ? { planTitle: truncateArg(planTitle) } : {}),
93
+ };
94
+ }
95
+
96
+ export function buildTaskReplaySummaryArgs({ args, result }: CursorReplayActivityBuildContext): CursorReplayTaskSummaryArgs {
97
+ const description = getString(args, "description") ?? getString(asRecord(result.value), "description") ?? "task";
98
+ const preview = firstNonEmptyLine(collectTaskText(result));
99
+ return {
100
+ description: truncateArg(description),
101
+ ...(preview ? { preview: truncateArg(preview) } : {}),
102
+ };
103
+ }
104
+
105
+ export function buildGenerateImageReplaySummaryArgs({
106
+ args,
107
+ result,
108
+ options,
109
+ }: CursorReplayActivityBuildContext): CursorReplayGenerateImageSummaryArgs {
110
+ const prompt = getString(args, "prompt") ?? getString(args, "description") ?? "image";
111
+ const imageDisplayPath = getGenerateImageDisplayPath(args, result, options);
112
+ return {
113
+ prompt: truncateArg(prompt),
114
+ ...(imageDisplayPath ? { path: imageDisplayPath } : {}),
115
+ };
116
+ }
117
+
118
+ export function buildMcpReplaySummaryArgs({ args, result }: CursorReplayActivityBuildContext): CursorReplayMcpSummaryArgs {
119
+ const toolName = getString(args, "toolName") ?? "mcp";
120
+ const preview = getMcpResultPreview(result);
121
+ return {
122
+ toolName: truncateArg(toolName),
123
+ ...(preview ? { preview } : {}),
124
+ };
125
+ }
126
+
127
+ export function buildSemSearchReplaySummaryArgs({ args }: CursorReplayActivityBuildContext): CursorReplaySemSearchSummaryArgs {
128
+ const query = getString(args, "query") ?? "semantic search";
129
+ const targetDirectories = (getArray(args, "targetDirectories") ?? []).filter((entry): entry is string => typeof entry === "string");
130
+ return {
131
+ query: truncateArg(query),
132
+ ...(targetDirectories.length > 0 ? { targetDirectories } : {}),
133
+ };
134
+ }
135
+
136
+ export function buildRecordScreenReplaySummaryArgs({
137
+ args,
138
+ result,
139
+ options,
140
+ }: CursorReplayActivityBuildContext): CursorReplayRecordScreenSummaryArgs {
141
+ const mode = getString(args, "mode");
142
+ const value = asRecord(result.value) ?? {};
143
+ const path = getString(value, "path");
144
+ const recordingDurationMs = getNumber(value, "recordingDurationMs");
145
+ return {
146
+ ...(mode ? { mode } : {}),
147
+ ...(path ? { path: formatDisplayPath(path, options.cwd) } : {}),
148
+ ...(recordingDurationMs !== undefined ? { recordingDurationMs } : {}),
149
+ };
150
+ }
151
+
152
+ export function buildWebSearchReplaySummaryArgs({ args }: CursorReplayActivityBuildContext): CursorReplayWebSearchSummaryArgs {
153
+ const query = extractWebSearchQuery(args);
154
+ return query ? { query: truncateArg(query) } : {};
155
+ }
156
+
157
+ export function buildWebFetchReplaySummaryArgs({ args }: CursorReplayActivityBuildContext): CursorReplayWebFetchSummaryArgs {
158
+ const url = extractWebFetchTarget(args);
159
+ return url ? { url: truncateArg(url) } : {};
160
+ }
161
+
162
+ export function buildGenerateImageReplayDetailFields(
163
+ context: CursorReplayActivityBuildContext,
164
+ contentText: string,
165
+ ): CursorReplayGenerateImageDetailFields {
166
+ const { args, result, options } = context;
167
+ const imagePath = getGenerateImagePath(args, result);
168
+ const imageDisplayPath = getGenerateImageDisplayPath(args, result, options);
169
+ return {
170
+ imagePath,
171
+ imageDisplayPath,
172
+ imageMimeType: inferImageMimeType(imagePath),
173
+ expandedText: contentText,
174
+ };
175
+ }
176
+
177
+ function getReadLintPaths(args: Record<string, unknown>, result: CursorReplayActivityBuildContext["result"], options: CursorReplayActivityBuildContext["options"]): string[] {
178
+ const explicitPaths = Array.isArray(args.paths)
179
+ ? args.paths.filter((entry): entry is string => typeof entry === "string")
180
+ : typeof args.path === "string"
181
+ ? [args.path]
182
+ : [];
183
+ const resultPaths = (getArray(asRecord(result.value), "fileDiagnostics") ?? [])
184
+ .map((file) => getString(asRecord(file), "path"))
185
+ .filter((entry): entry is string => Boolean(entry));
186
+ return [...new Set([...explicitPaths, ...resultPaths].map((entry) => formatDisplayPath(entry, options.cwd)))];
187
+ }
188
+
189
+ function getReadLintDiagnostics(result: CursorReplayActivityBuildContext["result"], options: CursorReplayActivityBuildContext["options"]): string[] {
190
+ const value = asRecord(result.value);
191
+ const files = getArray(value, "fileDiagnostics") ?? [];
192
+ const lines: string[] = [];
193
+ for (const file of files) {
194
+ const fileRecord = asRecord(file);
195
+ const pathValue = getString(fileRecord, "path");
196
+ const path = pathValue ? formatDisplayPath(pathValue, options.cwd) : "unknown";
197
+ const diagnostics = getArray(fileRecord, "diagnostics") ?? [];
198
+ for (const diagnostic of diagnostics) {
199
+ const diagnosticRecord = asRecord(diagnostic);
200
+ const severity = getString(diagnosticRecord, "severity") ?? "diagnostic";
201
+ const message = getString(diagnosticRecord, "message") ?? "";
202
+ const source = getString(diagnosticRecord, "source");
203
+ lines.push(`${path}: ${severity}${source ? ` ${source}` : ""}: ${message}`);
204
+ }
205
+ }
206
+ return lines;
207
+ }
208
+
209
+ function getTodoItems(args: Record<string, unknown>, result: CursorReplayActivityBuildContext["result"]): Array<{ content: string; status?: string }> {
210
+ const value = asRecord(result.value);
211
+ const rawTodos = getArray(value, "todos") ?? getArray(args, "todos") ?? [];
212
+ const todos: Array<{ content: string; status?: string }> = [];
213
+ for (const todo of rawTodos) {
214
+ const record = asRecord(todo);
215
+ const content = getString(record, "content");
216
+ if (!content) continue;
217
+ const status = getString(record, "status");
218
+ todos.push(status ? { content, status } : { content });
219
+ }
220
+ return todos;
221
+ }
222
+
223
+ function collectTaskText(result: CursorReplayActivityBuildContext["result"]): string {
224
+ const value = asRecord(result.value);
225
+ const success = getRecord(getRecord(value, "result"), "success");
226
+ const command = getString(success, "command");
227
+ const stdout = getString(success, "stdout");
228
+ const interleavedOutput = getString(success, "interleavedOutput");
229
+ const assistantMessages = (getArray(value, "conversationSteps") ?? [])
230
+ .map((step) => getString(getRecord(asRecord(step), "assistantMessage"), "text"))
231
+ .filter((entry): entry is string => Boolean(entry));
232
+ const parts = [command ? `$ ${command}` : undefined, stdout || interleavedOutput, ...assistantMessages].filter((part): part is string => Boolean(part));
233
+ return parts.join("\n");
234
+ }
235
+
236
+ function getGenerateImagePath(args: Record<string, unknown>, result: CursorReplayActivityBuildContext["result"]): string | undefined {
237
+ const value = asRecord(result.value);
238
+ return getString(value, "filePath") ?? getString(args, "filePath") ?? getString(args, "path");
239
+ }
240
+
241
+ function getGenerateImageDisplayPath(args: Record<string, unknown>, result: CursorReplayActivityBuildContext["result"], options: CursorReplayActivityBuildContext["options"]): string | undefined {
242
+ const path = getGenerateImagePath(args, result);
243
+ return path ? formatDisplayPath(path, options.cwd) : undefined;
244
+ }
245
+
246
+ function inferImageMimeType(path: string | undefined): string | undefined {
247
+ const lower = path?.toLowerCase();
248
+ if (!lower) return undefined;
249
+ if (lower.endsWith(".png")) return "image/png";
250
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
251
+ if (lower.endsWith(".gif")) return "image/gif";
252
+ if (lower.endsWith(".webp")) return "image/webp";
253
+ return undefined;
254
+ }
255
+
256
+ function getMcpContentText(entry: unknown): string | undefined {
257
+ const record = asRecord(entry);
258
+ const directText = getString(record, "text");
259
+ if (directText) return directText;
260
+ const nestedText = getRecord(record, "text");
261
+ return getString(nestedText, "text");
262
+ }
263
+
264
+ function getMcpResultPreview(result: CursorReplayActivityBuildContext["result"]): string | undefined {
265
+ if (result.status === "error") return undefined;
266
+ const value = asRecord(result.value);
267
+ const content = getArray(value, "content") ?? [];
268
+ for (const entry of content) {
269
+ const text = getMcpContentText(entry);
270
+ if (text) {
271
+ const line = firstNonEmptyLine(text);
272
+ if (line) return truncateArg(scrubSensitiveText(line), 120);
273
+ }
274
+ }
275
+ return undefined;
276
+ }
@@ -0,0 +1,33 @@
1
+ export const CURSOR_REPLAY_SOURCE_TOOL_NAMES = [
2
+ "read",
3
+ "grep",
4
+ "glob",
5
+ "ls",
6
+ "shell",
7
+ "edit",
8
+ "write",
9
+ "delete",
10
+ "readLints",
11
+ "updateTodos",
12
+ "createPlan",
13
+ "task",
14
+ "generateImage",
15
+ "mcp",
16
+ "semSearch",
17
+ "recordScreen",
18
+ "webSearch",
19
+ "webFetch",
20
+ ] as const;
21
+
22
+ export type CursorReplaySourceToolName = (typeof CURSOR_REPLAY_SOURCE_TOOL_NAMES)[number];
23
+ export type CursorReplayActivitySourceName = Exclude<CursorReplaySourceToolName, "generateImage">;
24
+
25
+ const CURSOR_REPLAY_SOURCE_TOOL_NAME_SET: ReadonlySet<string> = new Set(CURSOR_REPLAY_SOURCE_TOOL_NAMES);
26
+
27
+ export function isCursorReplaySourceToolName(name: string): name is CursorReplaySourceToolName {
28
+ return CURSOR_REPLAY_SOURCE_TOOL_NAME_SET.has(name);
29
+ }
30
+
31
+ export function isCursorReplayActivitySourceName(name: string): name is CursorReplayActivitySourceName {
32
+ return name !== "generateImage" && isCursorReplaySourceToolName(name);
33
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Typed replay summary argument payloads shared by the presentation registry
3
+ * and transcript replay builders.
4
+ */
5
+
6
+ export interface CursorReplayActivitySummaryOverride {
7
+ activitySummary?: string;
8
+ }
9
+
10
+ export interface CursorReplayPathSummaryArgs extends CursorReplayActivitySummaryOverride {
11
+ path?: string;
12
+ }
13
+
14
+ export interface CursorReplayReadLintsSummaryArgs extends CursorReplayActivitySummaryOverride {
15
+ paths?: string[];
16
+ path?: string;
17
+ diagnosticCount?: number;
18
+ }
19
+
20
+ export interface CursorReplayTodoSummaryArgs extends CursorReplayActivitySummaryOverride {
21
+ totalCount?: number;
22
+ completedCount?: number;
23
+ inProgressCount?: number;
24
+ pendingCount?: number;
25
+ }
26
+
27
+ export interface CursorReplayPlanSummaryArgs extends CursorReplayTodoSummaryArgs {
28
+ planTitle?: string;
29
+ }
30
+
31
+ export interface CursorReplayTaskSummaryArgs extends CursorReplayActivitySummaryOverride {
32
+ description?: string;
33
+ preview?: string;
34
+ }
35
+
36
+ export interface CursorReplayGenerateImageSummaryArgs extends CursorReplayActivitySummaryOverride {
37
+ path?: string;
38
+ prompt?: string;
39
+ }
40
+
41
+ export interface CursorReplayMcpSummaryArgs extends CursorReplayActivitySummaryOverride {
42
+ toolName?: string;
43
+ preview?: string;
44
+ }
45
+
46
+ export interface CursorReplaySemSearchSummaryArgs extends CursorReplayActivitySummaryOverride {
47
+ query?: string;
48
+ targetDirectories?: string[];
49
+ }
50
+
51
+ export interface CursorReplayRecordScreenSummaryArgs extends CursorReplayActivitySummaryOverride {
52
+ mode?: string;
53
+ path?: string;
54
+ recordingDurationMs?: number;
55
+ }
56
+
57
+ export interface CursorReplayWebSearchSummaryArgs extends CursorReplayActivitySummaryOverride {
58
+ query?: string;
59
+ }
60
+
61
+ export interface CursorReplayWebFetchSummaryArgs extends CursorReplayActivitySummaryOverride {
62
+ url?: string;
63
+ }
64
+
65
+ export interface CursorReplayNeutralActivitySummaryArgs extends CursorReplayActivitySummaryOverride {
66
+ path?: string;
67
+ toolName?: string;
68
+ query?: string;
69
+ targetDirectories?: string[];
70
+ }
71
+
72
+ export type CursorReplaySummaryArgs =
73
+ | CursorReplayPathSummaryArgs
74
+ | CursorReplayReadLintsSummaryArgs
75
+ | CursorReplayTodoSummaryArgs
76
+ | CursorReplayPlanSummaryArgs
77
+ | CursorReplayTaskSummaryArgs
78
+ | CursorReplayGenerateImageSummaryArgs
79
+ | CursorReplayMcpSummaryArgs
80
+ | CursorReplaySemSearchSummaryArgs
81
+ | CursorReplayRecordScreenSummaryArgs
82
+ | CursorReplayWebSearchSummaryArgs
83
+ | CursorReplayWebFetchSummaryArgs
84
+ | CursorReplayNeutralActivitySummaryArgs;
85
+
86
+ export function readCursorReplaySummaryString(
87
+ args: CursorReplaySummaryArgs | undefined,
88
+ key: string,
89
+ ): string | undefined {
90
+ const value = args?.[key as keyof CursorReplaySummaryArgs];
91
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
92
+ }
93
+
94
+ export function readCursorReplaySummaryNumber(
95
+ args: CursorReplaySummaryArgs | undefined,
96
+ key: string,
97
+ ): number | undefined {
98
+ const value = args?.[key as keyof CursorReplaySummaryArgs];
99
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
100
+ }
101
+
102
+ export function readCursorReplaySummaryStringArray(
103
+ args: CursorReplaySummaryArgs | undefined,
104
+ key: string,
105
+ ): string[] {
106
+ const value = args?.[key as keyof CursorReplaySummaryArgs];
107
+ return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [];
108
+ }
109
+
110
+ function formatReplayRecordingDurationMs(ms: number | undefined): string | undefined {
111
+ if (ms === undefined || !Number.isFinite(ms) || ms < 0) return undefined;
112
+ if (ms < 1000) return `${Math.round(ms)}ms`;
113
+ const seconds = ms / 1000;
114
+ return seconds < 60 ? `${seconds.toFixed(1)}s` : `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
115
+ }
116
+
117
+ export function summarizeReplayPath(args: CursorReplayPathSummaryArgs | undefined): string | undefined {
118
+ return readCursorReplaySummaryString(args, "path") ?? "unknown";
119
+ }
120
+
121
+ export function summarizeReplayReadLints(args: CursorReplayReadLintsSummaryArgs | undefined): string | undefined {
122
+ const paths = readCursorReplaySummaryStringArray(args, "paths");
123
+ const path = readCursorReplaySummaryString(args, "path");
124
+ const diagnosticCount = readCursorReplaySummaryNumber(args, "diagnosticCount");
125
+ const target = paths.length > 0 ? paths.join(", ") : path;
126
+ if (target && diagnosticCount !== undefined) {
127
+ return `${diagnosticCount} diagnostic${diagnosticCount === 1 ? "" : "s"} in ${target}`;
128
+ }
129
+ return target;
130
+ }
131
+
132
+ export function summarizeReplayTodoCount(args: CursorReplayTodoSummaryArgs | undefined): string | undefined {
133
+ const totalCount = readCursorReplaySummaryNumber(args, "totalCount");
134
+ const completedCount = readCursorReplaySummaryNumber(args, "completedCount");
135
+ const inProgressCount = readCursorReplaySummaryNumber(args, "inProgressCount");
136
+ const pendingCount = readCursorReplaySummaryNumber(args, "pendingCount");
137
+ if (totalCount !== undefined && completedCount !== undefined) {
138
+ const parts = [`${completedCount}/${totalCount} completed`];
139
+ if (inProgressCount && inProgressCount > 0) parts.push(`${inProgressCount} in progress`);
140
+ if (pendingCount && pendingCount > 0) parts.push(`${pendingCount} pending`);
141
+ return parts.join(", ");
142
+ }
143
+ return totalCount !== undefined ? `${totalCount} item${totalCount === 1 ? "" : "s"}` : undefined;
144
+ }
145
+
146
+ export function summarizeReplayPlan(args: CursorReplayPlanSummaryArgs | undefined): string | undefined {
147
+ return readCursorReplaySummaryString(args, "planTitle") ?? summarizeReplayTodoCount(args);
148
+ }
149
+
150
+ export function summarizeReplayTask(args: CursorReplayTaskSummaryArgs | undefined): string | undefined {
151
+ const description = readCursorReplaySummaryString(args, "description");
152
+ const preview = readCursorReplaySummaryString(args, "preview");
153
+ if (description && preview && preview !== description) return `${description}: ${preview}`;
154
+ return description ?? preview;
155
+ }
156
+
157
+ export function summarizeReplayMcp(args: CursorReplayMcpSummaryArgs | undefined): string | undefined {
158
+ const toolName = readCursorReplaySummaryString(args, "toolName") ?? "mcp";
159
+ const preview = readCursorReplaySummaryString(args, "preview");
160
+ return preview && preview !== toolName ? `${toolName} · ${preview}` : toolName;
161
+ }
162
+
163
+ export function summarizeReplayRecordScreen(args: CursorReplayRecordScreenSummaryArgs | undefined): string | undefined {
164
+ const path = readCursorReplaySummaryString(args, "path");
165
+ const duration = formatReplayRecordingDurationMs(readCursorReplaySummaryNumber(args, "recordingDurationMs"));
166
+ if (path && duration) return `${path} · ${duration}`;
167
+ return path ?? readCursorReplaySummaryString(args, "mode");
168
+ }
169
+
170
+ export function formatReplaySemSearchQuery(args: CursorReplaySemSearchSummaryArgs | undefined): string | undefined {
171
+ const query = readCursorReplaySummaryString(args, "query");
172
+ if (!query) return undefined;
173
+ const targetDirectories = readCursorReplaySummaryStringArray(args, "targetDirectories");
174
+ const dirHint =
175
+ targetDirectories.length > 0 ? ` (${targetDirectories.length} dir${targetDirectories.length === 1 ? "" : "s"})` : "";
176
+ return `${query}${dirHint}`;
177
+ }
178
+
179
+ export function summarizeReplayGenericActivity(args: CursorReplayNeutralActivitySummaryArgs | undefined): string | undefined {
180
+ return (
181
+ readCursorReplaySummaryString(args, "path")
182
+ ?? readCursorReplaySummaryString(args, "toolName")
183
+ ?? formatReplaySemSearchQuery(args)
184
+ );
185
+ }
186
+
187
+ export function withActivitySummaryFallback<T extends CursorReplayActivitySummaryOverride>(
188
+ buildSummary: (args: T | undefined) => string | undefined,
189
+ ): (args: T | undefined) => string | undefined {
190
+ return (args) => readCursorReplaySummaryString(args, "activitySummary") ?? buildSummary(args);
191
+ }