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
@@ -18,23 +18,15 @@ import {
18
18
  withActivitySummaryFallback,
19
19
  type CursorReplayActivitySummaryOverride,
20
20
  type CursorReplayGenerateImageSummaryArgs,
21
- type CursorReplayPathSummaryArgs,
22
- type CursorReplayPlanSummaryArgs,
23
- type CursorReplayReadLintsSummaryArgs,
24
- type CursorReplayRecordScreenSummaryArgs,
25
- type CursorReplaySemSearchSummaryArgs,
26
21
  type CursorReplaySummaryArgs,
27
- type CursorReplayTaskSummaryArgs,
28
- type CursorReplayTodoSummaryArgs,
29
22
  type CursorReplayWebFetchSummaryArgs,
30
23
  type CursorReplayWebSearchSummaryArgs,
31
24
  } from "./cursor-replay-summary-args.js";
25
+ import { truncateCursorDisplayLine } from "./cursor-display-text.js";
32
26
  import {
33
- buildCollapsedReplayDetailFields,
34
27
  buildCreatePlanReplaySummaryArgs,
35
28
  buildDeleteReplayDetailFields,
36
29
  buildDeleteReplaySummaryArgs,
37
- buildEmptyReplayDetailFields,
38
30
  buildGenerateImageReplayDetailFields,
39
31
  buildGenerateImageReplaySummaryArgs,
40
32
  buildMcpReplaySummaryArgs,
@@ -55,9 +47,10 @@ import type {
55
47
 
56
48
  export const CURSOR_REPLAY_ACTIVITY_TOOL_NAME = "cursor" as const;
57
49
 
58
- export type CursorWebToolKind = "webSearch" | "webFetch";
50
+ const EMPTY_REPLAY_DETAIL_FIELDS = (): Record<string, never> => ({});
51
+ const COLLAPSED_REPLAY_DETAIL_FIELDS = (): { collapseDetailsByDefault: true } => ({ collapseDetailsByDefault: true });
59
52
 
60
- export type CursorReplaySideEffectCategory = "file_mutations" | "real_tool_work";
53
+ export type CursorWebToolKind = "webSearch" | "webFetch";
61
54
 
62
55
  export type CursorToolLifecycleLabelKind =
63
56
  | "task"
@@ -96,10 +89,6 @@ export interface CursorToolPresentationSpec {
96
89
  normalizedName: CursorReplaySourceToolName;
97
90
  /** Raw SDK/host names that resolve to this tool via {@link normalizeCursorToolName}. */
98
91
  nameAliases?: readonly string[];
99
- replayLegacyName?: string;
100
- /** Human-readable SDK operation label for replay-only tool descriptions when it differs from {@link normalizedName}. */
101
- replayOperationLabel?: string;
102
- promptLabel: string;
103
92
  displayLabel: string;
104
93
  visibility: CursorToolVisibilityPolicy;
105
94
  webKind?: CursorWebToolKind;
@@ -107,52 +96,43 @@ export interface CursorToolPresentationSpec {
107
96
  webNamePatterns?: readonly RegExp[];
108
97
  lifecycleLabelKind?: CursorToolLifecycleLabelKind;
109
98
  replayCallSummary?: CursorReplayCallSummaryBuilder;
110
- /** Short label for replay-only tool definitions (for example `edit` for `cursor_edit`). */
111
- replayWrapperLabel?: string;
112
- /** Whether replay-only wrappers describe file mutations or other recorded tool work. */
113
- replaySideEffectCategory?: CursorReplaySideEffectCategory;
114
99
  activityReplay?: CursorToolActivityReplaySpec;
115
100
  generateImageReplay?: CursorToolGenerateImageReplaySpec;
116
101
  }
117
102
 
118
103
  const WEB_SEARCH_NAME_PATTERN =
119
- /^(?:web[-_ ]?search|search[-_ ]?web|websearch|browser[-_ ]?search|cursor[-_ ]?web[-_ ]?search)$/i;
104
+ /^(?:web[-_ ]?search|search[-_ ]?web|websearch|browser[-_ ]?search)$/i;
120
105
  const WEB_FETCH_NAME_PATTERN =
121
- /^(?:web[-_ ]?fetch|fetch[-_ ]?web|webfetch|browser[-_ ]?fetch|fetch[-_ ]?url|cursor[-_ ]?web[-_ ]?fetch)$/i;
106
+ /^(?:web[-_ ]?fetch|fetch[-_ ]?web|webfetch|browser[-_ ]?fetch|fetch[-_ ]?url)$/i;
122
107
 
123
108
  export const CURSOR_TOOL_PRESENTATION_SPECS = [
124
109
  {
125
110
  normalizedName: "read",
126
111
  nameAliases: ["read_file"],
127
- promptLabel: "read",
128
112
  displayLabel: "read",
129
113
  visibility: { incompleteTitle: "Cursor read", fastLocalDiscovery: true },
130
114
  },
131
115
  {
132
116
  normalizedName: "grep",
133
117
  nameAliases: ["grep_search", "search"],
134
- promptLabel: "grep",
135
118
  displayLabel: "grep",
136
119
  visibility: { incompleteTitle: "Cursor grep", fastLocalDiscovery: true },
137
120
  },
138
121
  {
139
122
  normalizedName: "glob",
140
123
  nameAliases: ["file_search"],
141
- promptLabel: "glob",
142
124
  displayLabel: "glob",
143
125
  visibility: { incompleteTitle: "Cursor find", fastLocalDiscovery: true },
144
126
  },
145
127
  {
146
128
  normalizedName: "ls",
147
129
  nameAliases: ["list_dir"],
148
- promptLabel: "ls",
149
130
  displayLabel: "ls",
150
131
  visibility: { incompleteTitle: "Cursor ls", fastLocalDiscovery: true },
151
132
  },
152
133
  {
153
134
  normalizedName: "shell",
154
135
  nameAliases: ["run_terminal_cmd", "terminal", "bash"],
155
- promptLabel: "shell",
156
136
  displayLabel: "shell",
157
137
  visibility: {
158
138
  incompleteTitle: "Cursor shell",
@@ -174,29 +154,19 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
174
154
  "notebook_edit",
175
155
  "notebookedit",
176
156
  ],
177
- replayLegacyName: "cursor_edit",
178
- promptLabel: "Cursor edit",
179
157
  displayLabel: "Cursor edit",
180
158
  visibility: {},
181
- replayWrapperLabel: "edit",
182
- replaySideEffectCategory: "file_mutations",
183
159
  replayCallSummary: withActivitySummaryFallback(summarizeReplayPath),
184
160
  },
185
161
  {
186
162
  normalizedName: "write",
187
163
  nameAliases: ["write_file", "writefile"],
188
- replayLegacyName: "cursor_write",
189
- promptLabel: "Cursor write",
190
164
  displayLabel: "Cursor write",
191
165
  visibility: {},
192
- replayWrapperLabel: "write",
193
- replaySideEffectCategory: "file_mutations",
194
166
  replayCallSummary: withActivitySummaryFallback(summarizeReplayPath),
195
167
  },
196
168
  {
197
169
  normalizedName: "delete",
198
- replayLegacyName: "cursor_delete",
199
- promptLabel: "Cursor delete",
200
170
  displayLabel: "Cursor delete",
201
171
  visibility: {},
202
172
  replayCallSummary: withActivitySummaryFallback(summarizeReplayPath),
@@ -207,59 +177,49 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
207
177
  },
208
178
  {
209
179
  normalizedName: "readLints",
210
- replayLegacyName: "cursor_read_lints",
211
- promptLabel: "Cursor diagnostics",
212
180
  displayLabel: "Cursor diagnostics",
213
181
  visibility: {},
214
182
  replayCallSummary: withActivitySummaryFallback(summarizeReplayReadLints),
215
183
  activityReplay: {
216
184
  buildActivityArgs: buildReadLintsReplaySummaryArgs,
217
- buildDetails: buildEmptyReplayDetailFields,
185
+ buildDetails: EMPTY_REPLAY_DETAIL_FIELDS,
218
186
  },
219
187
  },
220
188
  {
221
189
  normalizedName: "updateTodos",
222
- replayLegacyName: "cursor_update_todos",
223
- promptLabel: "Cursor todos",
224
190
  displayLabel: "Cursor todos",
225
191
  visibility: { lifecycleEligible: true },
226
192
  lifecycleLabelKind: "updateTodos",
227
193
  replayCallSummary: withActivitySummaryFallback(summarizeReplayTodoCount),
228
194
  activityReplay: {
229
195
  buildActivityArgs: ({ args, result }) => buildTodoReplaySummaryArgs(args, result),
230
- buildDetails: buildEmptyReplayDetailFields,
196
+ buildDetails: EMPTY_REPLAY_DETAIL_FIELDS,
231
197
  },
232
198
  },
233
199
  {
234
200
  normalizedName: "createPlan",
235
- replayLegacyName: "cursor_create_plan",
236
- promptLabel: "Cursor plan",
237
201
  displayLabel: "Cursor plan",
238
202
  visibility: { lifecycleEligible: true },
239
203
  lifecycleLabelKind: "createPlan",
240
204
  replayCallSummary: withActivitySummaryFallback(summarizeReplayPlan),
241
205
  activityReplay: {
242
206
  buildActivityArgs: buildCreatePlanReplaySummaryArgs,
243
- buildDetails: buildEmptyReplayDetailFields,
207
+ buildDetails: EMPTY_REPLAY_DETAIL_FIELDS,
244
208
  },
245
209
  },
246
210
  {
247
211
  normalizedName: "task",
248
- replayLegacyName: "cursor_task",
249
- promptLabel: "Cursor task",
250
212
  displayLabel: "Cursor task",
251
213
  visibility: { lifecycleEligible: true },
252
214
  lifecycleLabelKind: "task",
253
215
  replayCallSummary: withActivitySummaryFallback(summarizeReplayTask),
254
216
  activityReplay: {
255
217
  buildActivityArgs: buildTaskReplaySummaryArgs,
256
- buildDetails: buildEmptyReplayDetailFields,
218
+ buildDetails: EMPTY_REPLAY_DETAIL_FIELDS,
257
219
  },
258
220
  },
259
221
  {
260
222
  normalizedName: "generateImage",
261
- replayLegacyName: "cursor_generate_image",
262
- promptLabel: "Cursor image generation",
263
223
  displayLabel: "Cursor image generation",
264
224
  visibility: { lifecycleEligible: true },
265
225
  lifecycleLabelKind: "generateImage",
@@ -273,50 +233,40 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
273
233
  },
274
234
  {
275
235
  normalizedName: "mcp",
276
- replayLegacyName: "cursor_mcp",
277
- replayOperationLabel: "MCP",
278
- promptLabel: "Cursor MCP",
279
236
  displayLabel: "Cursor MCP",
280
237
  visibility: { lifecycleEligible: true },
281
238
  lifecycleLabelKind: "mcp",
282
239
  replayCallSummary: withActivitySummaryFallback(summarizeReplayMcp),
283
240
  activityReplay: {
284
241
  buildActivityArgs: buildMcpReplaySummaryArgs,
285
- buildDetails: buildEmptyReplayDetailFields,
242
+ buildDetails: EMPTY_REPLAY_DETAIL_FIELDS,
286
243
  },
287
244
  },
288
245
  {
289
246
  normalizedName: "semSearch",
290
- replayLegacyName: "cursor_sem_search",
291
- promptLabel: "Cursor semantic search",
292
247
  displayLabel: "Cursor semantic search",
293
248
  visibility: { lifecycleEligible: true },
294
249
  lifecycleLabelKind: "semSearch",
295
250
  replayCallSummary: withActivitySummaryFallback(formatReplaySemSearchQuery),
296
251
  activityReplay: {
297
252
  buildActivityArgs: buildSemSearchReplaySummaryArgs,
298
- buildDetails: buildEmptyReplayDetailFields,
253
+ buildDetails: EMPTY_REPLAY_DETAIL_FIELDS,
299
254
  },
300
255
  },
301
256
  {
302
257
  normalizedName: "recordScreen",
303
- replayLegacyName: "cursor_record_screen",
304
- promptLabel: "Cursor screen recording",
305
258
  displayLabel: "Cursor screen recording",
306
259
  visibility: { lifecycleEligible: true },
307
260
  lifecycleLabelKind: "recordScreen",
308
261
  replayCallSummary: withActivitySummaryFallback(summarizeReplayRecordScreen),
309
262
  activityReplay: {
310
263
  buildActivityArgs: buildRecordScreenReplaySummaryArgs,
311
- buildDetails: buildEmptyReplayDetailFields,
264
+ buildDetails: EMPTY_REPLAY_DETAIL_FIELDS,
312
265
  },
313
266
  },
314
267
  {
315
268
  normalizedName: "webSearch",
316
269
  nameAliases: ["websearch", "web_search", "web-search"],
317
- replayLegacyName: "cursor_web_search",
318
- replayOperationLabel: "web search",
319
- promptLabel: "Cursor web search",
320
270
  displayLabel: "Cursor web search",
321
271
  visibility: { lifecycleEligible: true },
322
272
  webKind: "webSearch",
@@ -327,15 +277,12 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
327
277
  ),
328
278
  activityReplay: {
329
279
  buildActivityArgs: buildWebSearchReplaySummaryArgs,
330
- buildDetails: buildCollapsedReplayDetailFields,
280
+ buildDetails: COLLAPSED_REPLAY_DETAIL_FIELDS,
331
281
  },
332
282
  },
333
283
  {
334
284
  normalizedName: "webFetch",
335
285
  nameAliases: ["webfetch", "web_fetch", "web-fetch"],
336
- replayLegacyName: "cursor_web_fetch",
337
- replayOperationLabel: "web fetch",
338
- promptLabel: "Cursor web fetch",
339
286
  displayLabel: "Cursor web fetch",
340
287
  visibility: { lifecycleEligible: true },
341
288
  webKind: "webFetch",
@@ -346,7 +293,7 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
346
293
  ),
347
294
  activityReplay: {
348
295
  buildActivityArgs: buildWebFetchReplaySummaryArgs,
349
- buildDetails: buildCollapsedReplayDetailFields,
296
+ buildDetails: COLLAPSED_REPLAY_DETAIL_FIELDS,
350
297
  },
351
298
  },
352
299
  ] as const satisfies readonly CursorToolPresentationSpec[];
@@ -354,56 +301,19 @@ export const CURSOR_TOOL_PRESENTATION_SPECS = [
354
301
  type CursorToolPresentationSpecEntry = (typeof CURSOR_TOOL_PRESENTATION_SPECS)[number];
355
302
 
356
303
  export type CursorNormalizedToolName = CursorReplaySourceToolName;
304
+ export type CursorReplayToolName = typeof CURSOR_REPLAY_ACTIVITY_TOOL_NAME;
357
305
 
358
- export type CursorReplayLegacyToolName = Extract<
359
- CursorToolPresentationSpecEntry,
360
- { readonly replayLegacyName: string }
361
- >["replayLegacyName"];
362
-
363
- export type CursorReplayToolName = typeof CURSOR_REPLAY_ACTIVITY_TOOL_NAME | CursorReplayLegacyToolName;
364
-
365
- type CursorToolPresentationSpecWithReplayLegacy = Extract<
306
+ type CursorToolPresentationSpecWithNeutralActivity = Extract<
366
307
  CursorToolPresentationSpecEntry,
367
- { readonly replayLegacyName: string }
368
- >;
308
+ { readonly activityReplay: CursorToolActivityReplaySpec } | { readonly generateImageReplay: CursorToolGenerateImageReplaySpec }
309
+ > | Extract<CursorToolPresentationSpecEntry, { readonly normalizedName: "edit" | "write" }>;
369
310
 
370
- const CURSOR_REPLAY_ACTIVITY_SIDE_EFFECT_CATEGORY: CursorReplaySideEffectCategory = "file_mutations";
311
+ export type CursorReplayActivityToolName = CursorToolPresentationSpecWithNeutralActivity["normalizedName"];
371
312
 
372
- function hasReplayLegacyName(spec: CursorToolPresentationSpecEntry): spec is CursorToolPresentationSpecWithReplayLegacy {
373
- return "replayLegacyName" in spec;
313
+ function hasNeutralActivityTitle(spec: CursorToolPresentationSpec): boolean {
314
+ return Boolean(spec.activityReplay || spec.generateImageReplay || spec.normalizedName === "edit" || spec.normalizedName === "write");
374
315
  }
375
316
 
376
- type ReplayActivityLabelKeysFromSpecs<Specs extends readonly CursorToolPresentationSpecEntry[]> = {
377
- [K in Extract<Specs[number], { readonly replayLegacyName: string }>["normalizedName"]]: Extract<
378
- Specs[number],
379
- { readonly replayLegacyName: string; normalizedName: K }
380
- >["replayLegacyName"];
381
- };
382
-
383
- function buildReplayActivityLabelKeysByToolName<const Specs extends readonly CursorToolPresentationSpecEntry[]>(
384
- specs: Specs,
385
- ): ReplayActivityLabelKeysFromSpecs<Specs> {
386
- const labelKeys: Record<string, CursorReplayLegacyToolName> = {};
387
- for (const spec of specs) {
388
- if (!hasReplayLegacyName(spec)) continue;
389
- labelKeys[spec.normalizedName] = spec.replayLegacyName;
390
- }
391
- return labelKeys as ReplayActivityLabelKeysFromSpecs<Specs>;
392
- }
393
-
394
- const CURSOR_REPLAY_SPECS = CURSOR_TOOL_PRESENTATION_SPECS.filter(hasReplayLegacyName);
395
-
396
- /** Stable registration order for native replay tool wrappers (registry declaration order). */
397
- export const CURSOR_REPLAY_LEGACY_TOOL_NAMES: readonly CursorReplayLegacyToolName[] = CURSOR_REPLAY_SPECS.map(
398
- (spec) => spec.replayLegacyName,
399
- );
400
-
401
- export const CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME = buildReplayActivityLabelKeysByToolName(
402
- CURSOR_TOOL_PRESENTATION_SPECS,
403
- );
404
-
405
- export type CursorReplayActivityToolName = keyof typeof CURSOR_REPLAY_ACTIVITY_LABEL_KEYS_BY_TOOL_NAME;
406
-
407
317
  const SPECS_BY_NORMALIZED_NAME = new Map<string, CursorToolPresentationSpec>(
408
318
  CURSOR_TOOL_PRESENTATION_SPECS.map((spec) => [spec.normalizedName, spec]),
409
319
  );
@@ -412,10 +322,6 @@ const SPECS_BY_NORMALIZED_KEY = new Map<string, CursorToolPresentationSpec>(
412
322
  CURSOR_TOOL_PRESENTATION_SPECS.map((spec) => [spec.normalizedName.toLowerCase(), spec]),
413
323
  );
414
324
 
415
- const SPECS_BY_REPLAY_LEGACY_NAME = new Map<CursorReplayLegacyToolName, CursorToolPresentationSpec>(
416
- CURSOR_REPLAY_SPECS.map((spec) => [spec.replayLegacyName, spec]),
417
- );
418
-
419
325
  const ALIAS_TO_NORMALIZED_NAME = new Map<string, CursorNormalizedToolName>(
420
326
  CURSOR_TOOL_PRESENTATION_SPECS.flatMap((spec) => {
421
327
  const aliases = "nameAliases" in spec ? spec.nameAliases : undefined;
@@ -437,11 +343,7 @@ export function getCursorToolPresentationSpec(
437
343
  ): CursorToolPresentationSpec | undefined {
438
344
  const trimmed = name.trim();
439
345
  if (!trimmed) return undefined;
440
- return (
441
- SPECS_BY_NORMALIZED_NAME.get(trimmed) ??
442
- SPECS_BY_NORMALIZED_KEY.get(trimmed.toLowerCase()) ??
443
- (isCursorReplayLegacyToolName(trimmed) ? SPECS_BY_REPLAY_LEGACY_NAME.get(trimmed) : undefined)
444
- );
346
+ return SPECS_BY_NORMALIZED_NAME.get(trimmed) ?? SPECS_BY_NORMALIZED_KEY.get(trimmed.toLowerCase());
445
347
  }
446
348
 
447
349
  export function normalizeCursorToolName(name: string): string {
@@ -464,65 +366,36 @@ export function classifyCursorWebToolKind(name: string | undefined): CursorWebTo
464
366
  return spec?.webKind;
465
367
  }
466
368
 
467
- const CURSOR_REPLAY_LEGACY_TOOL_NAME_SET: ReadonlySet<string> = new Set(CURSOR_REPLAY_LEGACY_TOOL_NAMES);
468
-
469
- export function isCursorReplayLegacyToolName(toolName: string): toolName is CursorReplayLegacyToolName {
470
- return CURSOR_REPLAY_LEGACY_TOOL_NAME_SET.has(toolName);
471
- }
472
-
473
369
  export function isCursorReplayToolName(toolName: string): toolName is CursorReplayToolName {
474
- return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME || isCursorReplayLegacyToolName(toolName);
370
+ return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME;
475
371
  }
476
372
 
477
373
  export function isExcludedFromCursorBridgeExposure(toolName: string): boolean {
478
- return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME || isCursorReplayLegacyToolName(toolName);
479
- }
480
-
481
- export function getCursorReplayWrapperLabel(toolName: CursorReplayToolName): string {
482
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return getCursorReplayDisplayLabel(toolName);
483
- const spec = SPECS_BY_REPLAY_LEGACY_NAME.get(toolName);
484
- return spec?.replayWrapperLabel ?? getCursorReplayDisplayLabel(toolName);
485
- }
486
-
487
- export function getCursorReplaySideEffectCategory(toolName: CursorReplayToolName): CursorReplaySideEffectCategory {
488
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return CURSOR_REPLAY_ACTIVITY_SIDE_EFFECT_CATEGORY;
489
- return SPECS_BY_REPLAY_LEGACY_NAME.get(toolName)?.replaySideEffectCategory ?? "real_tool_work";
490
- }
491
-
492
- export function getCursorReplaySideEffectDescription(toolName: CursorReplayToolName): string {
493
- return getCursorReplaySideEffectCategory(toolName) === "file_mutations" ? "file mutations" : "real tool work";
494
- }
495
-
496
- export function getCursorReplayOperationLabel(toolName: CursorReplayLegacyToolName): string {
497
- const spec = SPECS_BY_REPLAY_LEGACY_NAME.get(toolName);
498
- return spec?.replayOperationLabel ?? spec?.normalizedName ?? toolName;
374
+ return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME;
499
375
  }
500
376
 
501
377
  export function getCursorReplayPromptLabel(toolName: string): string {
502
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
503
- if (isCursorReplayLegacyToolName(toolName)) {
504
- const spec = SPECS_BY_REPLAY_LEGACY_NAME.get(toolName);
505
- return spec?.promptLabel ?? toolName;
506
- }
507
- return toolName;
378
+ return toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME ? "Cursor activity" : toolName;
508
379
  }
509
380
 
510
- export function getCursorReplayDisplayLabel(toolName: CursorReplayToolName): string {
511
- if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) return "Cursor activity";
512
- const spec = SPECS_BY_REPLAY_LEGACY_NAME.get(toolName);
513
- return spec?.displayLabel ?? toolName;
381
+ export function getCursorReplayActivityTitle(toolName: string): string | undefined {
382
+ const spec = getCursorToolPresentationSpec(toolName);
383
+ if (!spec || !hasNeutralActivityTitle(spec)) return undefined;
384
+ return spec.displayLabel;
514
385
  }
515
386
 
516
- export function getCursorReplayActivityLabelKey(toolName: string): CursorReplayLegacyToolName | undefined {
517
- const spec = getCursorToolPresentationSpec(toolName);
518
- if (!spec?.replayLegacyName || !isCursorReplayLegacyToolName(spec.replayLegacyName)) return undefined;
519
- return spec.replayLegacyName;
387
+ function buildCursorGenericActivityTitle(displayName: string): string {
388
+ if (!displayName || displayName === "unknown") return "Cursor tool";
389
+ return `Cursor ${truncateCursorDisplayLine(displayName, 120)}`;
520
390
  }
521
391
 
522
- export function getCursorReplayActivityTitle(toolName: string): string | undefined {
523
- const spec = getCursorToolPresentationSpec(toolName);
524
- if (!spec?.replayLegacyName) return undefined;
525
- return spec.displayLabel;
392
+ /** Canonical activity title: registry label when known, otherwise neutral fallback. */
393
+ export function getCursorToolActivityTitle(toolName: string): string {
394
+ const normalized = normalizeCursorToolName(toolName);
395
+ const known = getCursorReplayActivityTitle(normalized);
396
+ if (known) return known;
397
+ const label = toolName.trim() || normalized;
398
+ return buildCursorGenericActivityTitle(label);
526
399
  }
527
400
 
528
401
  function getCursorToolPresentationSpecByNormalizedKey(normalizedKey: string): CursorToolPresentationSpec | undefined {
@@ -546,11 +419,11 @@ export function getCursorToolGenerateImageReplaySpec(normalizedKey: string): Cur
546
419
  }
547
420
 
548
421
  export function getCursorReplayCallSummary(
549
- toolName: CursorReplayToolName,
422
+ toolName: CursorReplayToolName | CursorReplaySourceToolName,
550
423
  args: CursorReplaySummaryArgs | undefined,
551
424
  ): string | undefined {
552
425
  if (toolName === CURSOR_REPLAY_ACTIVITY_TOOL_NAME) {
553
426
  return readCursorReplaySummaryString(args, "activitySummary") ?? summarizeReplayGenericActivity(args);
554
427
  }
555
- return SPECS_BY_REPLAY_LEGACY_NAME.get(toolName)?.replayCallSummary?.(args);
428
+ return getCursorToolPresentationSpec(toolName)?.replayCallSummary?.(args);
556
429
  }
@@ -0,0 +1,185 @@
1
+ import { asRecord, getArray, getNumber, getRecord, getString, stringifyUnknown } from "./cursor-record-utils.js";
2
+ import { scrubSensitiveText } from "./cursor-sensitive-text.js";
3
+ import { firstNonEmptyLine, formatDisplayPath, truncateArg } from "./cursor-transcript-utils.js";
4
+
5
+ export interface CursorToolResultLike {
6
+ status: string | undefined;
7
+ value: unknown;
8
+ }
9
+
10
+ export interface CursorToolResultReaderOptions {
11
+ cwd?: string;
12
+ }
13
+
14
+ export interface CursorTodoItem {
15
+ content: string;
16
+ status?: string;
17
+ }
18
+
19
+ export function getReadLintPaths(
20
+ args: Record<string, unknown>,
21
+ result: CursorToolResultLike,
22
+ options: CursorToolResultReaderOptions,
23
+ ): string[] {
24
+ const explicitPaths = Array.isArray(args.paths)
25
+ ? args.paths.filter((entry): entry is string => typeof entry === "string")
26
+ : typeof args.path === "string"
27
+ ? [args.path]
28
+ : [];
29
+ const resultPaths = (getArray(asRecord(result.value), "fileDiagnostics") ?? [])
30
+ .map((file) => getString(asRecord(file), "path"))
31
+ .filter((entry): entry is string => Boolean(entry));
32
+ return [...new Set([...explicitPaths, ...resultPaths].map((entry) => formatDisplayPath(entry, options.cwd)))];
33
+ }
34
+
35
+ export function getReadLintDiagnostics(result: CursorToolResultLike, options: CursorToolResultReaderOptions): string[] {
36
+ const value = asRecord(result.value);
37
+ const files = getArray(value, "fileDiagnostics") ?? [];
38
+ const lines: string[] = [];
39
+ for (const file of files) {
40
+ const fileRecord = asRecord(file);
41
+ const pathValue = getString(fileRecord, "path");
42
+ const path = pathValue ? formatDisplayPath(pathValue, options.cwd) : "unknown";
43
+ const diagnostics = getArray(fileRecord, "diagnostics") ?? [];
44
+ for (const diagnostic of diagnostics) {
45
+ const diagnosticRecord = asRecord(diagnostic);
46
+ const severity = getString(diagnosticRecord, "severity") ?? "diagnostic";
47
+ const message = getString(diagnosticRecord, "message") ?? "";
48
+ const source = getString(diagnosticRecord, "source");
49
+ lines.push(`${path}: ${severity}${source ? ` ${source}` : ""}: ${message}`);
50
+ }
51
+ }
52
+ return lines;
53
+ }
54
+
55
+ export function getTodoItems(args: Record<string, unknown>, result: CursorToolResultLike): CursorTodoItem[] {
56
+ const value = asRecord(result.value);
57
+ const rawTodos = getArray(value, "todos") ?? getArray(args, "todos") ?? [];
58
+ const todos: CursorTodoItem[] = [];
59
+ for (const todo of rawTodos) {
60
+ const record = asRecord(todo);
61
+ const content = getString(record, "content");
62
+ if (!content) continue;
63
+ const status = getString(record, "status");
64
+ todos.push(status ? { content, status } : { content });
65
+ }
66
+ return todos;
67
+ }
68
+
69
+ export function getTodoTotalCount(args: Record<string, unknown>, result: CursorToolResultLike, todos: CursorTodoItem[]): number {
70
+ return getNumber(asRecord(result.value), "totalCount") ?? getNumber(args, "totalCount") ?? todos.length;
71
+ }
72
+
73
+ export function getTaskDescription(args: Record<string, unknown>, result: CursorToolResultLike): string {
74
+ return getString(args, "description") ?? getString(asRecord(result.value), "description") ?? "task";
75
+ }
76
+
77
+ function getNestedRecord(record: Record<string, unknown> | undefined, ...keys: string[]): Record<string, unknown> | undefined {
78
+ let current = record;
79
+ for (const key of keys) {
80
+ current = getRecord(current, key);
81
+ if (!current) return undefined;
82
+ }
83
+ return current;
84
+ }
85
+
86
+ export function collectTaskText(result: CursorToolResultLike): string {
87
+ const value = asRecord(result.value);
88
+ const success = getNestedRecord(value, "result", "success");
89
+ const command = getString(success, "command");
90
+ const stdout = getString(success, "stdout");
91
+ const interleavedOutput = getString(success, "interleavedOutput");
92
+ const assistantMessages = (getArray(value, "conversationSteps") ?? [])
93
+ .map((step) => getString(getRecord(asRecord(step), "assistantMessage"), "text"))
94
+ .filter((entry): entry is string => Boolean(entry));
95
+ const parts = [command ? `$ ${command}` : undefined, stdout || interleavedOutput, ...assistantMessages].filter((part): part is string => Boolean(part));
96
+ return parts.join("\n");
97
+ }
98
+
99
+ export function getGenerateImagePath(args: Record<string, unknown>, result: CursorToolResultLike): string | undefined {
100
+ const value = asRecord(result.value);
101
+ return getString(value, "filePath") ?? getString(args, "filePath") ?? getString(args, "path");
102
+ }
103
+
104
+ export function getGenerateImageDisplayPath(
105
+ args: Record<string, unknown>,
106
+ result: CursorToolResultLike,
107
+ options: CursorToolResultReaderOptions,
108
+ ): string | undefined {
109
+ const path = getGenerateImagePath(args, result);
110
+ return path ? formatDisplayPath(path, options.cwd) : undefined;
111
+ }
112
+
113
+ export function inferImageMimeType(path: string | undefined): string | undefined {
114
+ const lower = path?.toLowerCase();
115
+ if (!lower) return undefined;
116
+ if (lower.endsWith(".png")) return "image/png";
117
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
118
+ if (lower.endsWith(".gif")) return "image/gif";
119
+ if (lower.endsWith(".webp")) return "image/webp";
120
+ return undefined;
121
+ }
122
+
123
+ function getMcpContentText(entry: unknown): string | undefined {
124
+ const record = asRecord(entry);
125
+ const directText = getString(record, "text");
126
+ if (directText) return directText;
127
+ const nestedText = getRecord(record, "text");
128
+ return getString(nestedText, "text");
129
+ }
130
+
131
+ function describeNonTextMcpContent(entry: unknown): string {
132
+ const record = asRecord(entry);
133
+ const type = getString(record, "type") ?? "content";
134
+ if (type === "image") {
135
+ const mimeType = getString(record, "mimeType") ?? getString(record, "mime") ?? getString(record, "mediaType");
136
+ return `[image${mimeType ? ` ${mimeType}` : ""} omitted]`;
137
+ }
138
+ if (type === "audio") return "[audio omitted]";
139
+ if (type === "resource") return "[resource omitted]";
140
+ return `[${type} omitted]`;
141
+ }
142
+
143
+ export interface McpDisplayResult {
144
+ isToolError: boolean;
145
+ text: string;
146
+ nonTextSummary: string;
147
+ body: string;
148
+ preview?: string;
149
+ }
150
+
151
+ export function readMcpDisplayResult(result: CursorToolResultLike): McpDisplayResult {
152
+ if (result.status === "error") {
153
+ return { isToolError: false, text: "", nonTextSummary: "", body: "" };
154
+ }
155
+
156
+ const value = asRecord(result.value);
157
+ const isToolError = value?.isError === true;
158
+ const content = getArray(value, "content") ?? [];
159
+ const textParts: string[] = [];
160
+ const nonTextParts: string[] = [];
161
+ let preview: string | undefined;
162
+
163
+ for (const entry of content) {
164
+ const text = getMcpContentText(entry);
165
+ if (text) {
166
+ textParts.push(text);
167
+ const line = firstNonEmptyLine(text);
168
+ if (!preview && line) preview = truncateArg(scrubSensitiveText(line), 120);
169
+ continue;
170
+ }
171
+ nonTextParts.push(describeNonTextMcpContent(entry));
172
+ }
173
+
174
+ if (!preview) preview = nonTextParts[0];
175
+ const text = textParts.join("\n");
176
+ const nonTextSummary = nonTextParts.join("\n");
177
+ const body = text || nonTextSummary || scrubSensitiveText(stringifyUnknown(result.value), undefined);
178
+ return {
179
+ isToolError,
180
+ text,
181
+ nonTextSummary,
182
+ body: `${isToolError ? "[tool error]\n" : ""}${body}`.trim(),
183
+ ...(preview ? { preview } : {}),
184
+ };
185
+ }